Runtime

Message-Passing is the important part of Smalltalk (from which Objective-C derives), not objects. - Alan Kay

Runtime是一个实现Objc动态特性的C库(AppleGNU分别维护并开源自己的实现),不像C++那样在编译期间就必须严格确定函数与对象之间的关系,Objc在运行期间才能确定具体的执行函数。可以说Runtime就是对象生成的规范(内存模型)和消息执行的判断(动态绑定)。程序运行的时候,类型系统可以动态删减,对象父类可以动态改变,对象类型可以动态改变,对象方法可以动态改变,对象方法可以动态删减,对象成员可以动态删减,对象能力可以动态自省。看起来好像一切,都能够改变,都可以改变,因为如此灵活,很多第三方库使用Runtime实现了强大的功能。但是,如果没有深刻的理解和经验的积累,语言的灵活性往往会成为程序的不稳定,所以在不得已的情况下请谨慎使用Runtime。

SEL

//objc_selector未给出定义
typedef struct objc_selector *SEL;

SEL sel1 = @selector(test);
NSLog(@"%s %p", (char *)sel1, sel1);
SEL sel2 = sel_registerName([@"test" UTF8String]);
NSLog(@"%s %p", (char *)sel2, sel2);

//输出如下:
2015-10-05 17:20:26.195 Demo[1840:142119] test 0x7fff90c12140
2015-10-05 17:20:26.195 Demo[1840:142119] test 0x7fff90c12140

编译Objective-C时,编译器根据方法名生成唯一一个表示该方法的ID。方法名与ID一一对应。所有方法的SEL构成一个集合,查找某个方法时只需要去找到对应SEL就可以。SEL指向的机构里面应该就存储着一个字符串。

id

typedef struct objc_object *id;

struct objc_object { 
    Class isa;
};

id指向结构体objc_object,结构体有一个指向Class的指针isa,Runtime根据对象的isa找到它的类型信息。甚至可以这么说,一个结构体,不管里面有没有数据,有多少数据,只要第一个变量是Class指针且能通过它找到对应的类型信息,那么这个结构体就可以看做是一个对象(当然结构体的数据布局要和类型要求的一样才合法)。isa即是一个,代表抽象关系:一只猫是一个动物,一个对象是一个类。动物提供了猫的基本的信息(脑身腿脚)和抽象的行为(吃喝玩睡)。

Class

typedef struct objc_class *Class;

struct objc_class {
Class isa  OBJC_ISA_AVAILABILITY;
#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
} OBJC2_UNAVAILABLE;

objc_class结构体里存储着父类结构体指针,名称,版本,实例大小,成员变量列表,方法列表,方法缓存,协议列表。因为objc_class结构体里还有isa,由此可见**类也是一个”对象”,Runtime称之为类对象,而类对象的抽象Runtime称之为元类(MetaClass),而元类的抽象Runtime称之为根元类(Root Meta Class)**,他们都属于”对象”。对象其实是由类对象的实例方法-对象的类方法-创建出来的。

Ivar

typedef struct objc_ivar *Ivar;

struct objc_ivar {
    char *ivar_name                                          OBJC2_UNAVAILABLE;
    char *ivar_type                                          OBJC2_UNAVAILABLE;
    int ivar_offset                                          OBJC2_UNAVAILABLE;
#ifdef __LP64__
    int space                                                OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;

//objc_class
struct objc_ivar_list {
    int ivar_count                                           OBJC2_UNAVAILABLE;
#ifdef __LP64__
    int space                                                OBJC2_UNAVAILABLE;
#endif
    /* variable length structure */
    struct objc_ivar ivar_list[1]                            OBJC2_UNAVAILABLE;
} OBJC2_UNAVAILABLE;

Ivar是一个指向objc_ivar结构体的指针,objc_ivar结构体存储着变量名、变量类型编码和变量在该类中的字节偏移量,objc_class结构体(也就是类对象)里的objc_ivar_list存储着该类所有的变量。

健壮性实例变量(Non Fragile ivars)

Runtime目前有Modern和Legacy两个版本。Modern运行在OSX10.5之后和iOS的64位程序中,Legacy运行在OSX10.5之前的32位程序中。最大区别在于更改类的实例变量布局时,Legacy需要重新编译子类,Modern不需要。当一个类被编译时,该类会生成一个ivar布局,从isa开始依次根据实例变量大小产生位移,先是父类布局接着子类布局。访问变量的ivar地址=对象地址+ivar偏移字节。

但有个问题,当父类增加了ivar,原来的布局就出错了,在脆弱性实例变量(Fragile ivars)环境中不得不重新编译子类调整ivar布局。

在健壮性实例变量(Non Fragile ivars)环境中,编译器生成的子类实例变量布局跟以前一样,但程序启动后,如果Runtime检测到子类与父类布局有冲突便会调整子类布局。这样子类的成员变量就被保护起来了,以后不管父类变不变、怎么变都不会影响子类。访问变量的ivar地址=对象地址+基类大小+ivar偏移字节

Property

编译时,编译器将Property转换成Ivar,并且添加Setter和Getter。Runtime函数property_getAttributes能获取objc_property的名称和@encode类型字符串:

//objc_property未给出定义
typedef struct objc_property *objc_property_t;

typedef struct {
    const char *name;           /**< The name of the attribute */
    const char *value;          /**< The value of the attribute (usually empty) */
} objc_property_attribute_t;

Method

typedef struct objc_method *Method;

struct objc_method {
    SEL method_name                                          OBJC2_UNAVAILABLE;
    char *method_types                                       OBJC2_UNAVAILABLE;
    IMP method_imp                                           OBJC2_UNAVAILABLE;
} OBJC2_UNAVAILABLE;

//objc_class
struct objc_method_list {
    struct objc_method_list *obsolete                        OBJC2_UNAVAILABLE;
    int method_count                                         OBJC2_UNAVAILABLE;
#ifdef __LP64__
    int space                                                OBJC2_UNAVAILABLE;
#endif
    /* variable length structure */
    struct objc_method method_list[1]                        OBJC2_UNAVAILABLE;
}

Method代表类中某个方法,它是一个指向objc_method结构体的指针。objc_method结构体存储着一个SEL类型的名字,一个参数和返回值的类型信息,一个真正实现方法的函数地址。objc_class结构体(也就是类对象)的objc_method_list存储着该类所有的方法。

IMP

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

IMP是一个编译器编译生成的函数指针,该函数包含一个接收消息的对象id,对应映射的名字SEL,方法参数,返回值id。Objc函数都会编译成C函数,ObjC对象接收消息变成普通C函数调用一样。

Cache

typedef struct objc_cache *Cache

//objc_class
struct objc_cache {
    unsigned int mask /* total = mask + 1 */                 OBJC2_UNAVAILABLE;
    unsigned int occupied                                    OBJC2_UNAVAILABLE;
    Method buckets[1]                                        OBJC2_UNAVAILABLE;
} OBJC2_UNAVAILABLE;

每次调用方法后,Runtime会更新cache,根据算法留住最常用的方法,以提高代码执行效率。mask表示总共缓冲槽(Cache Bucket)数目。occupied表示有效缓冲槽数目。buckets表示缓冲槽,数目可能不超过mask+1,某槽可能为NULL(表示这个槽无效),有效槽们也可能是不连续的,缓冲槽可能会动态增加。

Protocol

typedef struct objc_object *Protocol;

//objc_class
struct objc_protocol_list {
    struct objc_protocol_list *next;
    long count;
    Protocol *list[1];
};

Protocol其实是一个对象结构体。类对象的Protocol列表里还有下一个列表的指针,从而让Protocol列表有了继承的能力。

Category

typedef struct objc_category *Category;

//A
struct objc_category {
    char *category_name                          OBJC2_UNAVAILABLE;
    char *class_name                             OBJC2_UNAVAILABLE;
    struct objc_method_list *instance_methods    OBJC2_UNAVAILABLE;
    struct objc_method_list *class_methods       OBJC2_UNAVAILABLE;
    struct objc_protocol_list *protocols         OBJC2_UNAVAILABLE;
} OBJC2_UNAVAILABLE;

objc_category存储了分类名、扩展的类、实例方法、类方法和协议列表。分类的实例方法列表是类的实例方法列表的子集,分类的类方法列表是类的类方法列表的子集。不过在objc-runtime-new.h的定义中:

//B
struct category_t {
    const char *name;
    classref_t cls;
    struct method_list_t *instanceMethods;
    struct method_list_t *classMethods;
    struct protocol_list_t *protocols;
    struct property_list_t *instanceProperties;
};

名字name是要扩展的类名而不是分类名。扩展的类cls不是在编译期而是在运行期赋值,由Runtime通过类名name找到对应的类。属性列表instanceProperties表示分类所有的properties,这就是可以增减关联变量的原因,不过分类的关联变量和类的实例变量是不一样的。

程序启动时,libSystem调用_objc_init(objc-os.mm)加载并初始化Runtime,map_images(objc-runtime-new.mm)加载map到内存,_read_images开始初始化map并加载所有Class,Protocol和Category(NSObject的+load在这时调用),Category的实例方法加入Class的方法列表,Category的类方法加入Meta Class的方法列表。

[receiver message]

编译器会根据语义和返回值选择objc_msgSend, objc_msgSend_stret(struc return), objc_msgSendSuper, objc_msgSendSuper_stret四个中的一个。传给父类的消息用带有Super的函数;返回值是数据结构不是简单类型的消息用带有stret的函数。objc_msgSend_fpret(floating point return)在i386平台代替返回类型为浮点数时使用的objc_msgSend,因为返回类型为浮点数的函数ABI(Application Binary Interface)与返回整型的函数ABI不兼容。

当objc_msgSend找到方法的实现后,Runtime将填充消息接收者receiver到第一位self,填充方法名字message到第二位_cmd,再依次将消息中所有参数按顺序填充。Objc发送的消息直到运行的时候才会动态绑定具体的实现:

  • 检测这个selector是否可以忽略。MacOSX有垃圾回收的话就不理会retain,release这些selector。
  • 检测这个target如果是nil则忽略。Objc允许对nil发送任何消息且不Crash。
  • 如果上面都通过便进入类中查询信息。先从cache里所有Method,找到相等selector匹配的IMP后执行。
  • 如果cache没找到,再查询方法列表所有Method,找到相等selector匹配的IMP后执行。
  • 如果方法列表没找到,再到父类以同样的过程找,直到找到NSObject为止。
  • 如果还没找到,就进入动态方法解析和消息转发的机制。

消息转发机制

不能确定对象是否能接收某个消息时,可以自省判断。

if ([reciver respondsToSelector:@selector(message)]) {
    [reciver performSelector:@selector(message)];
}

对象接收到无法接收的消息时,进入消息转发机制,告诉对象如何处理未知的消息。默认情况下,对象接收未知消息后,会触发NSObject的doesNotRecognizeSelector方法抛出异常导致程序崩溃。消息转发机制分为以下几步:

动态方法解析 对象接收未知消息时,首先会根据消息的类型调用+resolveInstanceMethod:(实例方法)或者+resolveClassMethod:(类方法)。通过Runtime函数class_addMethod动态添加能够接收未知消息的方法到类里,来实现接收该未知消息(主要用在实现@dynamic):

void methodForUnknownMessage(id self, SEL _cmd) {
   NSLog(@"%@, %p", self, _cmd);
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if ([NSStringFromSelector(sel) isEqualToString:@"methodForUnknownMessage"]) {
        class_addMethod(self.class, sel, (IMP)methodForUnknownMessage, "@:");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

备用接收对象 动态方法解析没能处理未知消息时,Runtime会继续调用forwardingTargetForSelector:返回备用接收对象去接收未知消息。

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

完整转发 如果没有备用接受对象(返回nil),Runtime会先用methodSignatureForSelector:获得未知消息的方法签名。

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    NSMethodSignature *signature = [super methodSignatureForSelector:aSelector];
    if (!signature) {
        if ([Helper instancesRespondToSelector:aSelector]) {
            signature = [Helper instanceMethodSignatureForSelector:aSelector];
        }
    }
    return signature;
}

Runtime再创建一个封装了全部细节(消息的接收对象target、消息的名字selector和消息附带的参数)的NSInvocation,调用forwardInvocation:(其中可以修改消息内容实现复杂功能,比如修改参数等)选择转发给其它对象。处理完未知消息后NSInvocation会保留结果,Runtime会提取结果并发送给原始接收对象。NSObject的forwardInvocation:只是简单调用doesNotRecognizeSelector:引发异常导致崩溃。

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    NSMethodSignature *signature = [super methodSignatureForSelector:aSelector];
    if (!signature) {
        if ([SUTRuntimeMethodHelper instancesRespondToSelector:aSelector]) {
            signature = [SUTRuntimeMethodHelper instanceMethodSignatureForSelector:aSelector];
        }
    }
    return signature;
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    if ([SUTRuntimeMethodHelper instancesRespondToSelector:anInvocation.selector]) {
        [anInvocation invokeWithTarget:_helper];
    }
}

消息转发和多继承

转发可以允许一个对象通过依赖其他对象去处理未知消息,让该对象表面上有处理未知消息的能力。虽然转发能模拟多继承让对象“继承”其它对象的能力,但区别在于多继承会让对象越来越大,转发只是建立对象间联系。虽然转发类似于继承,但Runtime对两者自省判断不一样,respondsToSelector:和isKindOfClass:和conformsToProtocol:能识别继承,不识别转发。如果想让消息转发看起来像继承,需要重写这些方法。

Runtime的应用

Category Ass

Methods Swizzling

KVO

NSZombie


延伸阅读


Runtime
https://hllovesgithub.github.io/2015/10/07/2015-10-07-Runtime/
作者
Hu Liang
发布于
2015年10月7日
许可协议