• 理解 Objective-C Runtime


    初学 Objective-C(以下简称ObjC) 的人很容易忽略一个 ObjC 特性 —— ObjC Runtime。这是因为这门语言很容易上手,几个小时就能学会怎么使用,所以程序员们往往会把时间都花在了解 Cocoa 框架以及调整自己的程序的表现上。然而 Runtime 应该是每一个 ObjC 都应该要了解的东西,至少要理解编译器会把

    [target doMethodWith:var1];
    

    编译成:

    objc_msgSend(target,@selector(doMethodWith:),var1);
    

    这样的语句。理解 ObjC Runtime 的工作原理,有助于你更深入地去理解 ObjC 这门语言,理解你的 App 是怎样跑起来的。我想所有的 Mac/iPhone 开发者,无论水平如何,都会从中获益的。

    ObjC Runtime 是开源的

    ObjC Runtime 的代码是开源的,可以从这个站点下载: opensource.apple.com

    这个是所有开源代码的链接: http://www.opensource.apple.com/source/

    这个是ObjC rumtime 的源代码: http://www.opensource.apple.com/source/objc4/
    4应该代表的是build版本而不是语言版本,现在是ObjC 2.0

    动态 vs 静态语言

    ObjC 是一种面向runtime(运行时)的语言,也就是说,它会尽可能地把代码执行的决策从编译和链接的时候,推迟到运行时。这给程序员写代码带来很大的灵活性,比如说你可以把消息转发给你想要的对象,或者随意交换一个方法的实现之类的。这就要求 runtime 能检测一个对象是否能对一个方法进行响应,然后再把这个方法分发到对应的对象去。我们拿 C 来跟 ObjC 对比一下。在 C 语言里面,一切从 main 函数开始,程序员写代码的时候是自上而下地,一个 C 的结构体或者说类吧,是不能把方法调用转发给其他对象的。举个栗子:

    #include < stdio.h >
    
    int main(int argc, const char **argv[])
    {
            printf("Hello World!");
            return 0;
    } 
    

    这段代码被编译器解析,优化后,会变成一堆汇编代码:

    .text
     .align 4,0x90
     .globl _main
    _main:
    Leh_func_begin1:
     pushq %rbp
    Llabel1:
     movq %rsp, %rbp
    Llabel2:
     subq $16, %rsp
    Llabel3:
     movq %rsi, %rax
     movl %edi, %ecx
     movl %ecx, -8(%rbp)
     movq %rax, -16(%rbp)
     xorb %al, %al
     leaq LC(%rip), %rcx
     movq %rcx, %rdi
     call _printf
     movl $0, -4(%rbp)
     movl -4(%rbp), %eax
     addq $16, %rsp
     popq %rbp
     ret
    Leh_func_end1:
     .cstring
    LC:
     .asciz "Hello World!"
    

    然后,再链接 include 的库,完了生成可执行代码。对比一下 ObjC,当我们初学这门语言的时候教程是这么说滴:用中括号括起来的语句,

    [self doSomethingWithVar:var1];
    

    被编译器编译之后会变成:

    objc_msgSend(self,@selector(doSomethingWithVar:),var1);
    

    一个 C 方法,传入了三个变量,self指针,要执行的方法 @selector(doSomethingWithVar:) 还有一个参数 var1。但是在这之后就不晓得发生什么了。

    什么是 Objective-C Runtime?

    ObjC Runtime 其实是一个 Runtime 库,基本上用 C 和汇编写的,这个库使得 C 语言有了面向对象的能力(脑中浮现当你乔帮主参观了施乐帕克的 SmallTalk 之后嘴角一抹浅笑)。这个库做的事前就是加载类的信息,进行方法的分发和转发之类的。

    Objective-C Runtime 术语

    再往下深谈之前咱先介绍几个术语。

    • 2 Runtimes

      目前说来Runtime有两种,一个 Modern Runtime 和一个 Legacy Runtime。Modern Runtime 覆盖了64位的Mac OS X Apps,还有 iOS Apps,Legacy Runtime 是早期用来给32位 Mac OS X Apps 用的,也就是可以不用管就是了。

    • 2 Basic types of Methods

      一种 Instance Method,还有 Class Method。instance method 就是带“-”号的,需要实例化才能用的,如 :

      -(void)doFoo; 
      
      [aObj doFoot];
      

      Class Method 就是带“+”号的,类似于静态方法可以直接调用:

      +(id)alloc;
      
      [ClassName alloc];
      

      这些方法跟 C 函数一样,就是一组代码,完成一个比较小的任务。

      -(NSString *)movieTitle
      {
          return @"Futurama: Into the Wild Green Yonder";
      }
      
    • Selector

      一个 Selector 事实上是一个 C 的结构体,表示的是一个方法。定义是:

      typedef struct objc_selector  *SEL; 
      

      使用起来就是:

      SEL aSel = @selector(movieTitle); 
      

      这样可以直接取一个selector,如果是传递消息(类似于C的方法调用)就是:

      [target getMovieTitleForObject:obj];
      

      在 ObjC 里面,用’[]‘括起来的表达式就是一个消息。包括了一个 target,就是要接收消息的对象,一个要被调用的方法还有一些你要传递的参数。类似于 C 函数的调用,但是又有所不同。事实上上面这个语句你仅仅是传递了 ObjC 消息,并不代表它就会一定被执行。target 这个对象会检测是谁发起的这个请求,然后决策是要执行这个方法还是其他方法,或者转发给其他的对象。

    • Class

      Class 的定义是这样的:

      typedef struct objc_class *Class;
      typedef struct objc_object {
          Class isa;
      } *id; 
      

      我们可以看到这里这里有两个结构体,一个类结构体一个对象结构体。所有的 objc_object 对象结构体都有一个 isa 指针,这个 isa 指向它所属的类,在运行时就靠这个指针来检测这个对象是否可以响应一个 selector。完了我们看到最后有一个 id 指针。这个指针其实就只是用来代表一个 ObjC 对象,有点类似于 C++ 的泛型。当你拿到一个 id 指针之后,就可以获取这个对象的类,并且可以检测其是否响应一个 selector。这就是对一个 delegate 常用的调用方式啦。这样说还有点抽象,我们看看 LLVM/Clang 的文档对 Blocks 的定义:

       struct Block_literal_1 {
          void *isa; // initialized to &_NSConcreteStackBlock or &_NSConcreteGlobalBlock
          int flags;
          int reserved; 
          void (*invoke)(void *, ...);
          struct Block_descriptor_1 {
       unsigned long int reserved; // NULL
           unsigned long int size;  // sizeof(struct Block_literal_1)
       // optional helper functions
           void (*copy_helper)(void *dst, void *src);
           void (*dispose_helper)(void *src); 
          } *descriptor;
          // imported variables
      };
      

      可以看到一个 block 是被设计成一个对象的,拥有一个 isa 指针,所以你可以对一个 block 使用 retain, release, copy 这些方法。

    • IMP (Method Implementations)

      接下来看看啥是IMP。

      typedef id (*IMP)(id self,SEL _cmd,...); 
      

      一个 IMP 就是一个函数指针,这是由编译器生成的,当你发起一个 ObjC 消息之后,最终它会执行的那个代码,就是由这个函数指针指定的。

    • Objective-C Classes

      OK,回过头来看看一个 ObjC 的类。举一个栗子:

      @interface MyClass : NSObject {
      //vars
      NSInteger counter;
      }
      //methods
      -(void)doFoo;
      @end
      

      定义一个类我们可以写成如上代码,而在运行时,一个类就不仅仅是上面看到的这些东西了:

      #if !__OBJC2__
          Class super_class                                        OBJC2_UNAVAILABLE;
          const char *name                                         OBJC2_UNAVAILABLE;
          long version                                             OBJC2_UNAVAILABLE;
          long info                                                OBJC2_UNAVAILABLE;
          long instance_size                                       OBJC2_UNAVAILABLE;
          struct objc_ivar_list *ivars                             OBJC2_UNAVAILABLE;
          struct objc_method_list **methodLists                    OBJC2_UNAVAILABLE;
          struct objc_cache *cache                                 OBJC2_UNAVAILABLE;
          struct objc_protocol_list *protocols                     OBJC2_UNAVAILABLE;
      #endif 
      

      可以看到运行时一个类还关联了它的父类指针,类名,成员变量,方法,cache 还有附属的 protocol。

    那么类定义了对象并且自己也是个对象?这是咋整滴?

    上面我提到过一个 ObjC 类同时也是一个对象,为了处理类和对象的关系,runtime 库创建了一种叫做 标签类 元类(Meta Class)的东西。当你发出一个消息的时候,比方说

    [NSObject alloc];
    

    你事实上是把这个消息发给了一个类对象(Class Object),这个类对象必须是一个 Meta Class 的实例,而这个 Meta Class 同时也是一个根 MetaClass 的实例。当你继承了 NSObject 成为其子类的时候,你的类指针就会指向 NSObject 为其父类。但是 Meta Class 不太一样,所有的 Meta Class 都指向根 Meta Class 为其父类。一个 Meta Class 持有所有能响应的方法。所以当 [NSObject alloc] 这条消息发出的时候,objc_msgSend() 这个方法会去 NSObject 它的 Meta Class 里面去查找是否有响应这个 selector 的方法,然后对 NSObject 这个类对象执行方法调用。

    为啥我们要继承 Apple Classes

    初学 Cocoa 开发的时候,多数教程都要我们继承一个类比方 NSObject,然后我们就开始 Coding 了。比方说:

    MyObject *object = [[MyObject alloc] init];
    

    这个语句用来初始化一个实例,类似于 C++ 的 new 关键字。这个语句首先会执行 MyObject 这个类的 +alloc 方法,Apple 的官方文档是这样说的:

    The isa instance variable of the new instance is initialized to a data structure that describes the class; memory for all other instance variables is set to 0.

    新建的实例中,isa 成员变量会变初始化成一个数据结构体,用来描述所指向的类。其他的成员变量的内存会被置为0.

    所以继承 Apple 的类我们不仅是获得了很多很好用的属性,而且也继承了这种内存分配的方法。

    那么啥是 Class Cache(objc_cache *cache)

    刚刚我们看到 runtime 里面有一个指针叫 objc_cache *cache,这是用来缓存方法调用的。现在我们知道一个实例对象被传递一个消息的时候,它会根据 isa 指针去查找能够响应这个消息的对象。但是实际上我们在用的时候,只有一部分方法是常用的,很多方法其实很少用或者根本用不到。比如一个object你可能从来都不用copy方法,那我要是每次调用的时候还去遍历一遍所有的方法那就太笨了。于是 cache 就应运而生了,每次你调用过一个方法,之后,这个方法就会被存到这个 cache 列表里面去,下次调用的时候 runtime 会优先去 cache 里面查找,提高了调用的效率。举一个栗子:

    MyObject *obj = [[MyObject alloc] init]; // MyObject 的父类是 NSObject
    
    @implementation MyObject
    -(id)init {
        if(self = [super init]){
            [self setVarA:@”blah”];
        }
        return self;
    }
    @end
    

    这段代码是这样执行的:

    1. [MyObject alloc] 先被执行。但是由于 MyObject 这个类没有 +alloc 这个方法,于是去父类 NSObject 查找。
    2. 检测 NSObject 是否响应 +alloc 方法,发现响应,于是检测 MyObject 类,根据其所需的内存空间大小开始分配内存空间,然后把 isa 指针指向 MyObject 类。那么 +alloc 就被加进 cache 列表里面了。
    3. 完了执行 -init 方法,因为 MyObject 响应该方法,直接加入 cache。
    4. 执行 self = [super init] 语句。这里直接通过 super 关键字调用父类的 init 方法,确保父类初始化成功,然后再执行自己的初始化逻辑。

    OK,这就是一个很简单的初始化过程,在 NSObject 类里面,alloc 和 init 没做什么特别重大的事情,但是,ObjC 特性允许你的 alloc 和 init 返回的值不同,也就是说,你可以在你的 init 函数里面做一些很复杂的初始化操作,但是返回出去一个简单的对象,这就隐藏了类的复杂性。再举个栗子:

    #import < Foundation/Foundation.h>
    
    @interface MyObject : NSObject
    {
     NSString *aString;
    }
    
    @property(retain) NSString *aString;
    
    @end
    
    @implementation MyObject
    
    -(id)init
    {
     if (self = [super init]) {
      [self setAString:nil];
     }
     return self;
    }
    
    @synthesize aString;
    
    @end
    
    
    
    int main (int argc, const char * argv[]) {
        NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
    
     id obj1 = [NSMutableArray alloc];
     id obj2 = [[NSMutableArray alloc] init];
    
     id obj3 = [NSArray alloc];
     id obj4 = [[NSArray alloc] initWithObjects:@"Hello",nil];
    
     NSLog(@"obj1 class is %@",NSStringFromClass([obj1 class]));
     NSLog(@"obj2 class is %@",NSStringFromClass([obj2 class]));
    
     NSLog(@"obj3 class is %@",NSStringFromClass([obj3 class]));
     NSLog(@"obj4 class is %@",NSStringFromClass([obj4 class]));
    
     id obj5 = [MyObject alloc];
     id obj6 = [[MyObject alloc] init];
    
     NSLog(@"obj5 class is %@",NSStringFromClass([obj5 class]));
     NSLog(@"obj6 class is %@",NSStringFromClass([obj6 class]));
    
     [pool drain];
        return 0;
    }
    

    如果你是ObjC的初学者,那么你很可能会认为这段代码执的输出会是:

    NSMutableArray
    NSMutableArray 
    NSArray
    NSArray
    MyObject
    MyObject
    

    但事实上是这样的:

    obj1 class is __NSPlaceholderArray
    obj2 class is NSCFArray
    obj3 class is __NSPlaceholderArray
    obj4 class is NSCFArray
    obj5 class is MyObject
    obj6 class is MyObject
    

    这是因为 ObjC 是允许运行 +alloc 返回一个特定的类,而 init 方法又返回一个不同的类的。可以看到 NSMutableArray 是对普通数组的封装,内部实现是复杂的,但是对外隐藏了复杂性。

    OK说回 objc_msgSend 这个方法

    这个方法做的事情不少,举个栗子:

    [self printMessageWithString:@"Hello World!"];
    

    这句语句被编译成这样:

    objc_msgSend(self,@selector(printMessageWithString:),@"Hello World!");
    

    这个方法先去查找 self 这个对象或者其父类是否响应 @selector(printMessageWithString:),如果从这个类的方法分发表或者 cache 里面找到了,就调用它对应的函数指针。如果找不到,那就会执行一些其他的东西。步骤如下:

    1. 检测这个 selector 是不是要忽略的。比如 Mac OS X 开发,有了垃圾回收就不理会 retain, release 这些函数了。
    2. 检测这个 target 是不是 nil 对象。ObjC 的特性是允许对一个 nil 对象执行任何一个方法不会 Crash,因为会被忽略掉。
    3. 如果上面两个都过了,那就开始查找这个类的 IMP,先从 cache 里面找,完了找得到就跳到对应的函数去执行。
    4. 如果 cache 找不到就找一下方法分发表。
    5. 如果还找不到就要开始消息转发逻辑了。

    在编译的时候,你定义的方法比如:

    -(int)doComputeWithNum:(int)aNum 
    

    会编译成:

    int aClass_doComputeWithNum(aClass *self,SEL _cmd,int aNum) 
    

    然后由 runtime 去调用指向你的这个方法的函数指针。那么之前我们说你发起消息其实不是对方法的直接调用,其实 Cocoa 还是提供了可以直接调用的方法的:

    // 首先定义一个 C 语言的函数指针
    int (computeNum *)(id,SEL,int);
    
    // 使用 methodForSelector 方法获取对应与该 selector 的杉树指针,跟 objc_msgSend 方法拿到的是一样的
    // **methodForSelector 这个方法是 Cocoa 提供的,不是 ObjC runtime 库提供的**
    computeNum = (int (*)(id,SEL,int))[target methodForSelector:@selector(doComputeWithNum:)];
    
    // 现在可以直接调用该函数了,跟调用 C 函数是一样的
    computeNum(obj,@selector(doComputeWithNum:),aNum); 
    

    如果你需要的话,你可以通过这种方式你来确保这个方法一定会被调用。

    消息转发机制

    在 ObjC 这门语言中,发送消息给一个并不响应这个方法的对象,是合法的,应该也是故意这么设计的。换句话说,我可以对任意一个对象传递任意一个消息(看起来有点像对任意一个类调用任意一个方法,当然事实上不是),当然如果最后找不到能调用的方法就会 Crash 掉。

    Apple 设计这种机制的原因之一就是——用来模拟多重继承(ObjC 原生是不支持多重继承的)。或者你希望把你的复杂设计隐藏起来。这种转发机制是 Runtime 非常重要的一个特性,大概的步骤如下:

    1. 查找该类及其父类的 cahce 和方法分发表,在找不到的情况下执行2。
    2. 执行 + (BOOL) resolveInstanceMethod:(SEL)aSEL 方法。

      这就给了程序员一次机会,可以告诉 runtime 在找不到改方法的情况下执行什么方法。举个栗子,先定义一个函数:

      void fooMethod(id obj, SEL _cmd)
      {
       NSLog(@"Doing Foo");
      }
      

      完了重载 resolveInstanceMethod 方法:

      +(BOOL)resolveInstanceMethod:(SEL)aSEL
      {
          if(aSEL == @selector(doFoo:)){
              class_addMethod([self class],aSEL,(IMP)fooMethod,"v@:");
              return YES;
          }
          return [super resolveInstanceMethod];
      }
      

      其中 “v@:” 表示返回值和参数,这个符号涉及 Type Encoding,可以参考Apple的文档 ObjC Runtime Guide

    3. 接下来 Runtime 会调用 – (id)forwardingTargetForSelector:(SEL)aSelector 方法。
      这就给了程序员第二次机会,如果你没办法在自己的类里面找到替代方法,你就重载这个方法,然后把消息转给其他的Object。

      - (id)forwardingTargetForSelector:(SEL)aSelector
      {
          if(aSelector == @selector(mysteriousMethod:)){
              return alternateObject;
          }
          return [super forwardingTargetForSelector:aSelector];
      }
      

      这样你就可以把消息转给别人了。当然这里你不能 return self,不然就死循环了=.=

    4. 最后,Runtime 会调用 – (void)forwardInvocation:(NSInvocation *)anInvocation 这个方法。NSInvocation 其实就是一条消息的封装。如果你能拿到 NSInvocation,那你就能修改这条消息的 target, selector 和 arguments。举个栗子:
      -(void)forwardInvocation:(NSInvocation *)invocation
      {
          SEL invSEL = invocation.selector;
      
          if([altObject respondsToSelector:invSEL]) {
              [invocation invokeWithTarget:altObject];
          } else {
              [self doesNotRecognizeSelector:invSEL];
          }
      }
      

      默认情况下 NSObject 对 forwardInvocation 的实现就是简单地执行 -doesNotRecognizeSelector: 这个方法,所以如果你想真正的在最后关头去转发消息你可以重载这个方法(好折腾-.-)。

      原文后面介绍了 Non Fragile ivars (Modern Runtime), Objective-C Associated Objects 和 Hybrid vTable Dispatch。鉴于一是底层的可以不用理会,一是早司空见惯的不用详谈,还有一个是很简单的,就是一个建立在方法分发表里面填入默认常用的 method,所以有兴趣的读者可以自行查阅原文,这里就不详谈鸟。

    References

    Objective-C Runtime Programming Guide

    Objective-C Runtime Reference

  • 相关阅读:
    Cactus详细讲解
    jexus处理静态文件(处理后缀)
    简单的html兼容(参考js和css的常规写法)
    IOHelper(自制常用的输入输出的帮助类)
    @Html.AntiForgeryToken() 源码分析,表单防伪码的生成
    缓存处理类(MemoryCache结合文件缓存)
    MVC过滤大法(过滤静态文件)
    C#文件监控工具(对追加内容的监控并输出)
    C#在Linux上的开发指南
    对java面试文章的技术漫谈的C#技术理解
  • 原文地址:https://www.cnblogs.com/fuland/p/3667872.html
Copyright © 2020-2023  润新知