刨根问底Objective-C Runtime
http://chun.tips/blog/2014/11/05/bao-gen-wen-di-objective%5Bnil%5Dc-runtime-(2)%5Bnil%5D-object-and-class-and-meta-class/
刨根问底Objective-C Runtime(1)- Self & Super
刨根问底Objective-C Runtime(2)- Object & Class & Meta Class
刨根问底Objective-C Runtime(3)- 消息和Category
刨根问底Objective-C Runtime(4)- 成员变量与属性
刨根问底Objective-C Runtime(1)- Self & Super
下面的代码输出什么?
| 
 1 
2 
3 
4 
5 
6 
7 
8 
9 
10 
11 
12 
 | 
@implementation Son : Father- (id)init{    self = [super init];    if (self)    {        NSLog(@"%@", NSStringFromClass([self class]));        NSLog(@"%@", NSStringFromClass([super class]));    }    return self;}@end | 
答案:都输出 Son
| 
 1 
2 
 | 
2014-11-05 11:06:18.060 Test[8566:568584] NSStringFromClass([self class]) = Son2014-11-05 11:06:18.061 Test[8566:568584] NSStringFromClass([super class]) = Son | 
解惑:这个题目主要是考察关于objc中对 self 和 super 的理解。
self 是类的隐藏参数,指向当前调用方法的这个类的实例。而 super 是一个 Magic Keyword, 它本质是一个编译器标示符,和 self 是指向的同一个消息接受者。上面的例子不管调用[self class]还是[super class],接受消息的对象都是当前 Son *xxx 这个对象。而不同的是,super是告诉编译器,调用 class 这个方法时,要去父类的方法,而不是本类里的。
当使用 self 调用方法时,会从当前类的方法列表中开始找,如果没有,就从父类中再找;而当使用 super 时,则从父类的方法列表中开始找。然后调用父类的这个方法。
真的是这样吗?继续看:
使用clang重写命令:
| 
 1 
 | 
$ clang -rewrite-objc test.m | 
发现上述代码被转化为:
| 
 1 
2 
 | 
NSLog((NSString *)&__NSConstantStringImpl__var_folders_gm_0jk35cwn1d3326x0061qym280000gn_T_main_a5cecc_mi_0, NSStringFromClass(((Class (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("class"))));NSLog((NSString *)&__NSConstantStringImpl__var_folders_gm_0jk35cwn1d3326x0061qym280000gn_T_main_a5cecc_mi_1, NSStringFromClass(((Class (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){ (id)self, (id)class_getSuperclass(objc_getClass("Son")) }, sel_registerName("class")))); | 
从上面的代码中,我们可以发现在调用 [self class] 时,会转化成 objc_msgSend函数。看下函数定义:
| 
 1 
 | 
id objc_msgSend(id self, SEL op, ...) | 
我们把 self 做为第一个参数传递进去。
而在调用 [super class]时,会转化成 objc_msgSendSuper函数。看下函数定义:
| 
 1 
 | 
id objc_msgSendSuper(struct objc_super *super, SEL op, ...) | 
第一个参数是 objc_super 这样一个结构体,其定义如下:
| 
 1 
2 
3 
4 
 | 
struct objc_super {   __unsafe_unretained id receiver;   __unsafe_unretained Class super_class;}; | 
结构体有两个成员,第一个成员是 receiver, 类似于上面的 objc_msgSend函数第一个参数self 。第二个成员是记录当前类的父类是什么。
所以,当调用 [self class] 时,实际先调用的是 objc_msgSend函数,第一个参数是 Son当前的这个实例,然后在 Son 这个类里面去找 - (Class)class这个方法,没有,去父类 Father里找,也没有,最后在 NSObject类中发现这个方法。而 - (Class)class的实现就是返回self的类别,故上述输出结果为 Son。
objc Runtime开源代码对- (Class)class方法的实现:
| 
 1 
2 
3 
 | 
- (Class)class {    return object_getClass(self);} | 
而当调用 [super class]时,会转换成objc_msgSendSuper函数。第一步先构造 objc_super 结构体,结构体第一个成员就是 self 。第二个成员是 (id)class_getSuperclass(objc_getClass(“Son”)) , 实际该函数输出结果为 Father。第二步是去 Father这个类里去找- (Class)class,没有,然后去NSObject类去找,找到了。最后内部是使用 objc_msgSend(objc_super->receiver, @selector(class))去调用,此时已经和[self class]调用相同了,故上述输出结果仍然返回 Son。
刨根问底Objective-C Runtime(2)- Object & Class & Meta Clas
本篇笔记主要是讲述objc runtime中关于Object & Class & Meta Class的细节。
习题内容
下面代码的运行结果是?
| 
 1 
2 
3 
4 
5 
6 
7 
8 
9 
10 
11 
12 
13 
14 
 | 
@interface Sark : NSObject@end@implementation Sark@endint main(int argc, const char * argv[]) {    @autoreleasepool {        BOOL res1 = [(id)[NSObject class] isKindOfClass:[NSObject class]];        BOOL res2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]];        BOOL res3 = [(id)[Sark class] isKindOfClass:[Sark class]];        BOOL res4 = [(id)[Sark class] isMemberOfClass:[Sark class]];        NSLog(@"%d %d %d %d", res1, res2, res3, res4);    }    return 0;} | 
运行结果为:
| 
 1 
 | 
2014-11-05 14:45:08.474 Test[9412:721945] 1 0 0 0 | 
这里先看几个概念
什么是 id
id 在 objc.h 中定义如下:
| 
 1 
2 
 | 
/// A pointer to an instance of a class.typedef struct objc_object *id; | 
就像注释中所说的这样 id 是指向一个 objc_object 结构体的指针。
id 这个struct的定义本身就带了一个 *, 所以我们在使用其他NSObject类型的实例时需要在前面加上 *, 而使用 id 时却不用。
那么objc_object又是什么呢
objc_object 在 objc.h 中定义如下:
| 
 1 
2 
3 
4 
 | 
/// Represents an instance of a class.struct objc_object {    Class isa;}; | 
这个时候我们知道Objective-C中的object在最后会被转换成C的结构体,而在这个struct中有一个 isa 指针,指向它的类别 Class。
那么什么是Class呢
在 objc.h 中定义如下:
| 
 1 
2 
 | 
/// An opaque type that represents an Objective-C class.typedef struct objc_class *Class; | 
我们可以看到 Class本身指向的也是一个C的struct objc_class。
继续看在runtime.h中objc_class定义如下:
| 
 1 
2 
3 
4 
5 
6 
7 
8 
9 
10 
11 
12 
13 
14 
 | 
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; | 
该结构体中,isa 指向所属Class, super_class指向父类别。
继续看
下载objc源代码,在 objc-runtime-new.h 中,我们发现 objc_class有如下定义:
| 
 1 
2 
3 
4 
5 
6 
 | 
struct objc_class : objc_object {    // Class ISA;    Class superclass;       ...    ...} | 
豁然开朗,我们看到在Objective-C的设计哲学中,一切都是对象。Class在设计中本身也是一个对象。而这个Class对象的对应的类,我们叫它 Meta Class。即Class结构体中的 isa 指向的就是它的 Meta Class。
Meta Class
根据上面的描述,我们可以把Meta Class理解为 一个Class对象的Class。简单的说:
| 
 1 
2 
 | 
当我们发送一个消息给一个NSObject对象时,这条消息会在对象的类的方法列表里查找当我们发送一个消息给一个类时,这条消息会在类的Meta Class的方法列表里查找 | 
而 Meta Class本身也是一个Class,它跟其他Class一样也有自己的 isa 和 super_class 指针。看下图:

每个Class都有一个isa指针指向一个唯一的Meta Class
每一个Meta Class的isa指针都指向最上层的Meta Class(图中的NSObject的Meta Class)
最上层的Meta Class的isa指针指向自己,形成一个回路
每一个Meta Class的super class指针指向它原本Class的 Super Class的Meta Class。但是最上层的Meta Class的 Super Class指向NSObject Class本身
最上层的NSObject Class的super class指向 nil
解惑
为了更加清楚的知道整个函数调用过程,我们使用clang -rewrite-objc main.m重写,可获得如下代码:
| 
 1 
2 
3 
4 
 | 
BOOL res1 = ((BOOL (*)(id, SEL, Class))(void *)objc_msgSend)((id)((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("class")), sel_registerName("isKindOfClass:"), ((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("class")));BOOL res2 = ((BOOL (*)(id, SEL, Class))(void *)objc_msgSend)((id)((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("class")), sel_registerName("isMemberOfClass:"), ((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("class")));BOOL res3 = ((BOOL (*)(id, SEL, Class))(void *)objc_msgSend)((id)((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Sark"), sel_registerName("class")), sel_registerName("isMemberOfClass:"), ((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("class")));BOOL res4 = ((BOOL (*)(id, SEL, Class))(void *)objc_msgSend)((id)((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Sark"), sel_registerName("class")), sel_registerName("isMemberOfClass:"), ((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("class"))); | 
先看前两个调用:
最外层是 objc_msgSend函数,转发消息。
函数第一个参数是 (id)((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("class"))
函数第二个参数是转发的selector
函数第三个参数是 ((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("class"))
我们注意到第一个参数和第三个参数对应重写的是[NSObject class],即使用objc_msgSend向 NSObject Class 发送 @selector(class) 这个消息
打开objc源代码,在 Object.mm 中发现+ (Class)class实现如下:
| 
 1 
2 
3 
 | 
+ (Class)class {    return self;} | 
所以即返回Class类的对象本身。看如下输出:
| 
 1 
2 
3 
4 
 | 
NSLog(@"%p", [NSObject class]);NSLog(@"%p", [NSObject class]);2014-11-05 18:48:30.939 Test[11682:865988] 0x7fff768d40f02014-11-05 18:48:30.940 Test[11682:865988] 0x7fff768d40f0 | 
继续打开objc源代码,在 Object.mm 中,我们发现 isKindOfClass的实现如下:
| 
 1 
2 
3 
4 
5 
6 
7 
8 
 | 
- (BOOL)isKindOf:aClass{    Class cls;    for (cls = isa; cls; cls = cls->superclass)         if (cls == (Class)aClass)            return YES;    return NO;} | 
对着上面Meta Class的图和实现,我们可以看出
当 NSObject Class对象第一次进行比较时,得到它的isa为 NSObject的Meta Class, 这个时候 NSObject Meta Class 和 NSObject Class不相等。
然后取NSObject 的Meta Class 的Super class,这个时候又变成了 NSObject Class, 所以返回相等。
所以上述第一个输出结果是 YES 。
我们在看下 ‘isMemberOfClass’的实现:
| 
 1 
2 
3 
4 
 | 
- (BOOL)isMemberOf:aClass{    return isa == (Class)aClass;} | 
综上所述,当前的 isa 指向 NSObject 的 Meta Class, 所以和 NSObject Class不相等。
所以上述第二个输出结果为 NO 。
继续看后面两个调用:
Sark Class 的isa指向的是 Sark的Meta Class,和Sark Class不相等
Sark Meta Class的super class 指向的是 NSObject Meta Class, 和 Sark Class不相等
NSObject Meta Class的 super class 指向 NSObject Class,和 Sark Class 不相等
NSObject Class 的super class 指向 nil, 和 Sark Class不相等
所以后面两个调用的结果都输出为 NO 。
刨根问底Objective-C Runtime(3)- 消息 和 Category
本篇笔记主要是讲述objc runtime的 消息和Category。
习题内容
下面的代码会?Compile Error / Runtime Crash / NSLog…?
| 
 1 
2 
3 
4 
5 
6 
7 
8 
9 
10 
11 
12 
13 
14 
15 
16 
 | 
@interface NSObject (Sark)+ (void)foo;@end@implementation NSObject (Sark)- (void)foo{    NSLog(@"IMP: -[NSObject(Sark) foo]");}@endint main(int argc, const char * argv[]) {    @autoreleasepool {        [NSObject foo];        [[NSObject new] foo];    }    return 0;} | 
答案:代码正常输出,输出结果如下:
| 
 1 
2 
 | 
2014-11-06 13:11:46.694 Test[14872:1110786] IMP: -[NSObject(Sark) foo]2014-11-06 13:11:46.695 Test[14872:1110786] IMP: -[NSObject(Sark) foo] | 
使用clang -rewrite-objc main.m重写,我们可以发现 main 函数中两个方法调用被转换成如下代码:
| 
 1 
2 
 | 
 ((void (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("foo")); ((void (*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("new")), sel_registerName("foo")); | 
我们发现上述两个方法最终转换成使用 objc_msgSend 函数传递消息。
这里先看几个概念
objc_msgSend函数定义如下:
| 
 1 
 | 
id objc_msgSend(id self, SEL op, ...) | 
关于 id 的解释请看objc runtime系列第二篇博文: objc runtime中Object & Class & Meta Class的细节
什么是 SEL
打开objc.h文件,看下SEL的定义如下:
| 
 1 
 | 
typedef struct objc_selector *SEL; | 
SEL是一个指向objc_selector结构体的指针。而 objc_selector 的定义并没有在runtime.h中给出定义。我们可以尝试运行如下代码:
| 
 1 
2 
3 
4 
5 
6 
7 
 | 
SEL sel = @selector(foo);NSLog(@"%s", (char *)sel);NSLog(@"%p", sel);const char *selName = [@"foo" UTF8String];SEL sel2 = sel_registerName(selName);NSLog(@"%s", (char *)sel2);NSLog(@"%p", sel2); | 
输出如下:
| 
 1 
2 
3 
4 
 | 
2014-11-06 13:46:08.058 Test[15053:1132268] foo2014-11-06 13:46:08.058 Test[15053:1132268] 0x7fff8fde51142014-11-06 13:46:08.058 Test[15053:1132268] foo2014-11-06 13:46:08.058 Test[15053:1132268] 0x7fff8fde5114 | 
Objective-C在编译时,会根据方法的名字生成一个用来区分这个方法的唯一的一个ID。只要方法名称相同,那么它们的ID就是相同的。
两个类之间,不管它们是父类与子类的关系,还是之间没有这种关系,只要方法名相同,那么它的SEL就是一样的。每一个方法都对应着一个SEL。编译器会根据每个方法的方法名为那个方法生成唯一的SEL。这些SEL组成了一个Set集合,当我们在这个集合中查找某个方法时,只需要去找这个方法对应的SEL即可。而SEL本质是一个字符串,所以直接比较它们的地址即可。
当然,不同的类可以拥有相同的selector。不同类的实例对象执行相同的selector时,会在各自的方法列表中去根据selector去寻找自己对应的IMP。
那么什么是IMP呢
继续看定义:
| 
 1 
 | 
typedef id (*IMP)(id, SEL, ...); | 
IMP本质就是一个函数指针,这个被指向的函数包含一个接收消息的对象id,调用方法的SEL,以及一些方法参数,并返回一个id。因此我们可以通过SEL获得它所对应的IMP,在取得了函数指针之后,也就意味着我们取得了需要执行方法的代码入口,这样我们就可以像普通的C语言函数调用一样使用这个函数指针。
那么 objc_msgSend 到底是怎么工作的呢?
在Objective-C中,消息直到运行时才会绑定到方法的实现上。编译器会把代码中[target doSth]转换成 objc_msgSend消息函数,这个函数完成了动态绑定的所有事情。它的运行流程如下:
检查selector是否需要忽略。(ps: Mac开发中开启GC就会忽略retain,release方法。)
检查target是否为nil。如果为nil,直接cleanup,然后return。(这就是我们可以向nil发送消息的原因。)
然后在target的Class中根据Selector去找IMP
寻找IMP的过程:
先从当前class的cache方法列表(cache methodLists)里去找
找到了,跳到对应函数实现
没找到,就从class的方法列表(methodLists)里找
还找不到,就到super class的方法列表里找,直到找到基类(NSObject)为止
最后再找不到,就会进入动态方法解析和消息转发的机制。(这部分知识,下次再细谈)
那么什么是方法列表呢?
上一篇博文中提到了objc_class结构体定义,如下:
| 
 1 
2 
3 
4 
5 
6 
7 
8 
9 
10 
11 
12 
13 
14 
15 
16 
17 
18 
19 
20 
21 
22 
23 
 | 
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;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;} | 
1) objc_method_list 就是用来存储当前类的方法链表,objc_method存储了类的某个方法的信息。
Method
| 
 1 
 | 
typedef struct objc_method *Method; | 
Method 是用来代表类中某个方法的类型,它实际就指向objc_method结构体,如下:
| 
 1 
2 
3 
4 
5 
 | 
struct objc_method {    SEL method_name                                          OBJC2_UNAVAILABLE;    char *method_types                                       OBJC2_UNAVAILABLE;    IMP method_imp                                           OBJC2_UNAVAILABLE;}                                                            OBJC2_UNAVAILABLE; | 
method_types是个char指针,存储着方法的参数类型和返回值类型。
SEL 和 IMP 就是我们上文提到的,所以我们可以理解为objc_class中 method list保存了一组SEL<->IMP的映射。
2)objc_cache 用来缓存用过的方法,提高性能。
Cache
| 
 1 
 | 
typedef struct objc_cache *Cache                             OBJC2_UNAVAILABLE; | 
实际指向objc_cache结构体,如下:
| 
 1 
2 
3 
4 
5 
 | 
struct objc_cache {    unsigned int mask /* total = mask + 1 */                 OBJC2_UNAVAILABLE;    unsigned int occupied                                    OBJC2_UNAVAILABLE;    Method buckets[1]                                        OBJC2_UNAVAILABLE;}; | 
mask: 指定分配cache buckets的总数。在方法查找中,Runtime使用这个字段确定数组的索引位置
occupied: 实际占用cache buckets的总数
buckets: 指定Method数据结构指针的数组。这个数组可能包含不超过mask+1个元素。需要注意的是,指针可能是NULL,表示这个缓存bucket没有被占用,另外被占用的bucket可能是不连续的。这个数组可能会随着时间而增长。
objc_msgSend每调用一次方法后,就会把该方法缓存到cache列表中,下次的时候,就直接优先从cache列表中寻找,如果cache没有,才从methodLists中查找方法。
说完了 objc_msgSend, 那么题目中的Category又是怎么工作的呢?
继续看概念
我们知道Catagory可以动态地为已经存在的类添加新的方法。这样可以保证类的原始设计规模较小,功能增加时再逐步扩展。在runtime.h中查看定义:
| 
 1 
 | 
typedef struct objc_category *Category; | 
同样也是指向一个 objc_category 的C 结构体,定义如下:
| 
 1 
2 
3 
4 
5 
6 
7 
 | 
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源代码,在 objc-runtime-new.h中我们可以发现如下定义:
| 
 1 
2 
3 
4 
5 
6 
7 
8 
 | 
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 是指 class_name 而不是 category_name
cls是要扩展的类对象,编译期间是不会定义的,而是在Runtime阶段通过name对应到对应的类对象
instanceProperties表示Category里所有的properties,这就是我们可以通过objc_setAssociatedObject和objc_getAssociatedObject增加实例变量的原因,不过这个和一般的实例变量是不一样的
为了验证上述内容,我们使用clang -rewrite-objc main.m重写,题目中的Category被编译器转换成了这样:
| 
 1 
2 
3 
4 
5 
6 
7 
8 
9 
10 
11 
12 
13 
14 
15 
16 
17 
18 
19 
20 
 | 
// @interface NSObject (Sark)// + (void)foo;/* @end */// @implementation NSObject (Sark)static void _I_NSObject_Sark_foo(NSObject * self, SEL _cmd) {    NSLog((NSString *)&__NSConstantStringImpl__var_folders_gm_0jk35cwn1d3326x0061qym280000gn_T_main_dd1ee3_mi_0);}// @endstatic struct _category_t _OBJC_$_CATEGORY_NSObject_$_Sark __attribute__ ((used, section ("__DATA,__objc_const"))) = {    "NSObject",    0, // &OBJC_CLASS_$_NSObject,    (const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_NSObject_$_Sark,    0,    0,    0,};static struct _category_t *L_OBJC_LABEL_CATEGORY_$ [1] __attribute__((used, section ("__DATA, __objc_catlist,regular,no_dead_strip")))= {    &_OBJC_$_CATEGORY_NSObject_$_Sark,}; | 
_OBJC_$_CATEGORY_NSObject_$_Sark是按规则生成的字符串,我们可以清楚的看到是NSObject类,且Sark是NSObject类的Category
_category_t结构体第二项 classref_t 没有数据,验证了我们上面的说法
由于题目中只有 - (void)foo方法,所以结构体中存储的list只有第三项instanceMethods被填充。
_I_NSObject_Sark_foo代表了Category的foo方法,I表示实例方法
最后这个类的Category生成了一个数组,存在了__objc_catlist里,目前数组的内容只有一个&_OBJC_$_CATEGORY_NSObject_$_Sark
最终这些Category里面的方法是如何被加载的呢?
1.打开objc源代码,找到 objc-os.mm, 函数_objc_init为runtime的加载入口,由libSystem调用,进行初始化操作。
2.之后调用objc-runtime-new.mm -> map_images加载map到内存
3.之后调用objc-runtime-new.mm->_read_images初始化内存中的map, 这个时候将会load所有的类,协议还有Category。NSOBject的+load方法就是这个时候调用的
这里贴上Category被加载的代码:
| 
 1 
2 
3 
4 
5 
6 
7 
8 
9 
10 
11 
12 
13 
14 
15 
16 
17 
18 
19 
20 
21 
22 
23 
24 
25 
26 
27 
28 
29 
30 
31 
32 
33 
34 
35 
36 
37 
38 
39 
40 
41 
42 
43 
44 
45 
46 
47 
48 
49 
50 
51 
 | 
// Discover categories. for (EACH_HEADER) {    category_t **catlist =         _getObjc2CategoryList(hi, &count);    for (i = 0; i < count; i++) {        category_t *cat = catlist[i];        Class cls = remapClass(cat->cls);        if (!cls) {            // Category's target class is missing (probably weak-linked).            // Disavow any knowledge of this category.            catlist[i] = nil;            if (PrintConnecting) {                _objc_inform("CLASS: IGNORING category \?\?\?(%s) %p with "                             "missing weak-linked target class",                              cat->name, cat);            }            continue;        }        // Process this category.         // First, register the category with its target class.         // Then, rebuild the class's method lists (etc) if         // the class is realized.         BOOL classExists = NO;        if (cat->instanceMethods ||  cat->protocols              ||  cat->instanceProperties)         {            addUnattachedCategoryForClass(cat, cls, hi);            if (cls->isRealized()) {                remethodizeClass(cls);                classExists = YES;            }            if (PrintConnecting) {                _objc_inform("CLASS: found category -%s(%s) %s",                              cls->nameForLogging(), cat->name,                              classExists ? "on existing class" : "");            }        }        if (cat->classMethods  ||  cat->protocols              /* ||  cat->classProperties */)         {            addUnattachedCategoryForClass(cat, cls->ISA(), hi);            if (cls->ISA()->isRealized()) {                remethodizeClass(cls->ISA());            }            if (PrintConnecting) {                _objc_inform("CLASS: found category +%s(%s)",                              cls->nameForLogging(), cat->name);            }        }    }} | 
1) 循环调用了 _getObjc2CategoryList方法,这个方法的实现是:
| 
 1 
 | 
GETSECT(_getObjc2CategoryList,        category_t *,    "__objc_catlist"); | 
方法中最后一个参数__objc_catlist就是编译器刚刚生成的category数组
2) load完所有的categories之后,开始对Category进行处理。
从上面的代码中我们可以发现:实例方法被加入到了当前的类对象中, 类方法被加入到了当前类的Meta Class中 (cls->ISA)
Step 1. 调用addUnattachedCategoryForClass方法
Step 2. 调用remethodizeClass方法, 在remethodizeClass的实现里调用attachCategoryMethods
| 
 1 
2 
3 
4 
5 
6 
7 
8 
9 
10 
11 
12 
13 
14 
15 
16 
17 
18 
19 
20 
21 
22 
 | 
static void attachCategoryMethods(Class cls, category_list *cats, bool flushCaches){    if (!cats) return;    if (PrintReplacedMethods) printReplacements(cls, cats);    bool isMeta = cls->isMetaClass();    method_list_t **mlists = (method_list_t **)        _malloc_internal(cats->count * sizeof(*mlists));    // Count backwards through cats to get newest categories first    int mcount = 0;    int i = cats->count;    BOOL fromBundle = NO;    while (i--) {        method_list_t *mlist = cat_method_list(cats->list[i].cat, isMeta);        if (mlist) {            mlists[mcount++] = mlist;            fromBundle |= cats->list[i].fromBundle;        }    }    attachMethodLists(cls, mlists, mcount, NO, fromBundle, flushCaches);    _free_internal(mlists);} | 
这里把一个类的category_list的所有方法取出来生成了method list。这里是倒序添加的,也就是说,新生成的category的方法会先于旧的category的方法插入。
之后调用attachMethodLists将所有方法前序添加进类的method list中,如果原来类的方法列表是a,b,Category的方法列表是c,d。那么插入之后的方法列表将会是c,d,a,b。
小发现
看上面被编译器转换的代码,我们发现Category头文件被注释掉了,结合上面category的加载过程。这就是我们即使没有import category的头文件,都能够成功调用到Category方法的原因。
runtime加载完成后,Category的原始信息在类结构中将不会存在。
解惑
根据上面提到的知识,我们对题目中的代码进行分析。
1) objc runtime加载完后,NSObject的Sark Category被加载。而NSObject的Sark Category的头文件 + (void)foo 并没有实质参与到工作中,只是给编译器进行静态检查,所有我们编译上述代码会出现警告,提示我们没有实现 + (void)foo 方法。而在代码编译中,它已经被注释掉了。
2) 实际被加入到Class的method list的方法是 - (void)foo,它是一个实例方法,所以加入到当前类对象NSObject的方法列表中,而不是NSObject Meta class的方法列表中。
3) 当执行 [NSObject foo]时,我们看下整个objc_msgSend的过程:
结合上一篇Meta Class的知识:
objc_msgSend 第一个参数是 “(id)objc_getClass("NSObject")”,获得NSObject Class的对象。
类方法在Meta Class的方法列表中找,我们在load Category方法时加入的是- (void)foo实例方法,所以并不在NSOBject Meta Class的方法列表中
继续往 super class中找,在上一篇博客中我们知道,NSObject Meta Class的super class是NSObject本身。所以,这个时候我们能够找到- (void)foo 这个方法。
所以正常输出结果
4) 当执行[[NSObject new] foo],我们看下整个objc_msgSend的过程:
[NSObject new]生成一个NSObject对象。
直接在该对象的类(NSObject)的方法列表里找。
能够找到,所以正常输出结果。
刨根问底Objective-C Runtime(4)- 成员变量与属性
本篇笔记主要是讲述objc runtime的 成员变量和属性。
习题内容
下面代码会? Compile Error / Runtime Crash / NSLog…?
| 
 1 
2 
3 
4 
5 
6 
7 
8 
9 
10 
11 
12 
13 
14 
15 
16 
17 
18 
19 
20 
21 
22 
23 
24 
25 
26 
27 
28 
29 
 | 
@interface Sark : NSObject@property (nonatomic, copy) NSString *name;@end@implementation Sark- (void)speak{    NSLog(@"my name is %@", self.name);}@end@interface Test : NSObject@end@implementation Test- (instancetype)init{    self = [super init];    if (self) {        id cls = [Sark class];        void *obj = &cls;        [(__bridge id)obj speak];    }    return self;}@endint main(int argc, const char * argv[]) {    @autoreleasepool {        [[Test alloc] init];    }    return 0;} | 
答案:代码正常输出,输出结果为:
| 
 1 
 | 
2014-11-07 14:08:25.698 Test[1097:57255] my name is | 
为什么呢?
前几节博文中多次讲到了objc_class结构体,今天我们再拿出来看一下:
| 
 1 
2 
3 
4 
5 
6 
7 
8 
9 
10 
11 
12 
13 
14 
 | 
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_ivar_list结构体存储着objc_ivar数组列表,而objc_ivar结构体存储了类的单个成员变量的信息。
那么什么是Ivar呢?
Ivar 在objc中被定义为:
| 
 1 
 | 
typedef struct objc_ivar *Ivar; | 
它是一个指向objc_ivar结构体的指针,结构体有如下定义:
| 
 1 
2 
3 
4 
5 
6 
7 
8 
 | 
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; | 
这里我们注意第三个成员 ivar_offset。它表示基地址偏移字节。
在编译我们的类时,编译器生成了一个 ivar布局,显示了在类中从哪可以访问我们的 ivars 。看下图:

上图中,左侧的数据就是地址偏移字节,我们对 ivar 的访问就可以通过 对象地址 + ivar偏移字节的方法。但是这又引发一个问题,看下图:

我们增加了父类的ivar,这个时候布局就出错了,我们就不得不重新编译子类来恢复兼容性。
而Objective-C Runtime中使用了Non Fragile ivars,看下图:

使用Non Fragile ivars时,Runtime会进行检测来调整类中新增的ivar的偏移量。 这样我们就可以通过 对象地址 + 基类大小 + ivar偏移字节的方法来计算出ivar相应的地址,并访问到相应的ivar。
我们来看一个例子:
| 
 1 
2 
3 
4 
5 
6 
7 
8 
9 
10 
11 
12 
13 
14 
15 
16 
17 
18 
19 
 | 
@interface Student : NSObject{    @private    NSInteger age;}@end@implementation Student- (NSString *)description{    return [NSString stringWithFormat:@"age = %d", age];}@endint main(int argc, const char * argv[]) {    @autoreleasepool {        Student *student = [[Student alloc] init];        student->age = 24;    }    return 0;} | 
上述代码,Student有两个被标记为private的ivar,这个时候当我们使用 -> 访问时,编译器会报错。那么我们如何设置一个被标记为private的ivar的值呢?
通过上面的描述,我们知道ivar是通过计算字节偏量来确定地址,并访问的。我们可以改成这样:
| 
 1 
2 
3 
4 
5 
6 
7 
8 
9 
10 
11 
12 
13 
14 
15 
16 
17 
18 
19 
20 
21 
22 
23 
24 
25 
 | 
@interface Student : NSObject{    @private    int age;}@end@implementation Student- (NSString *)description{    NSLog(@"current pointer = %p", self);    NSLog(@"age pointer = %p", &age);    return [NSString stringWithFormat:@"age = %d", age];}@endint main(int argc, const char * argv[]) {    @autoreleasepool {        Student *student = [[Student alloc] init];        Ivar age_ivar = class_getInstanceVariable(object_getClass(student), "age");        int *age_pointer = (int *)((__bridge void *)(student) + ivar_getOffset(age_ivar));        NSLog(@"age ivar offset = %td", ivar_getOffset(age_ivar));        *age_pointer = 10;        NSLog(@"%@", student);    }    return 0;} | 
上述代码的输出结果为:
| 
 1 
2 
3 
4 
 | 
2014-11-08 18:24:38.892 Test[4143:466864] age ivar offset = 82014-11-08 18:24:38.893 Test[4143:466864] current pointer = 0x1001002d02014-11-08 18:24:38.893 Test[4143:466864] age pointer = 0x1001002d82014-11-08 18:24:38.894 Test[4143:466864] age = 10 | 
我们可以清晰的看到指针地址的变化和偏移量,和我们上述描述一致。
说完了Ivar, 那Property又是怎么样的呢?
使用clang -rewrite-objc main.m重写题目中的代码,我们发现Sark类中的name属性被转换成了如下代码:
| 
 1 
2 
3 
4 
5 
6 
7 
8 
9 
 | 
struct Sark_IMPL {    struct NSObject_IMPL NSObject_IVARS;    NSString *_name;};// @property (nonatomic, copy) NSString *name;/* @end */// @implementation Sarkstatic NSString * _I_Sark_name(Sark * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_Sark$_name)); }static void _I_Sark_setName_(Sark * self, SEL _cmd, NSString *name) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct Sark, _name), (id)name, 0, 1); } | 
类中的Property属性被编译器转换成了Ivar,并且自动添加了我们熟悉的Set和Get方法。
我们这个时候回头看一下objc_class结构体中的内容,并没有发现用来专门记录Property的list。我们翻开objc源代码,在objc-runtime-new.h中,发现最终还是会通过在class_ro_t结构体中使用property_list_t存储对应的propertyies。
而在刚刚重写的代码中,我们可以找到这个property_list_t:
| 
 1 
2 
3 
4 
5 
6 
7 
8 
9 
10 
11 
12 
13 
14 
15 
16 
17 
18 
19 
20 
 | 
static struct /*_prop_list_t*/ {    unsigned int entsize;  // sizeof(struct _prop_t)    unsigned int count_of_properties;    struct _prop_t prop_list[1];    } _OBJC_$_PROP_LIST_Sark __attribute__ ((used, section ("__DATA,__objc_const"))) = {        sizeof(_prop_t),        1,        name};static struct _class_ro_t _OBJC_CLASS_RO_$_Sark __attribute__ ((used, section ("__DATA,__objc_const"))) = {    0, __OFFSETOFIVAR__(struct Sark, _name), sizeof(struct Sark_IMPL),     (unsigned int)0,     0,     "Sark",    (const struct _method_list_t *)&_OBJC_$_INSTANCE_METHODS_Sark,    0,     (const struct _ivar_list_t *)&_OBJC_$_INSTANCE_VARIABLES_Sark,    0,     (const struct _prop_list_t *)&_OBJC_$_PROP_LIST_Sark,}; | 
解惑
1)为什么能够正常运行,并调用到speak方法?
| 
 1 
2 
3 
 | 
id cls = [Sark class];void *obj = &cls;[(__bridge id)obj speak]; | 
obj被转换成了一个指向Sark Class的指针,然后使用id转换成了objc_object类型。这个时候的obj已经相当于一个Sark的实例对象(但是和使用[Sark new]生成的对象还是不一样的),我们回想下Runtime的第二篇博文中objc_object结构体的构成就是一个指向Class的isa指针。
这个时候我们再回想下上一篇博文中objc_msgSend的工作流程,在代码中的obj指向的Sark Class中能够找到speak方法,所以代码能够正常运行。
2) 为什么self.name的输出为?
我们在测试代码中加入一些调试代码和Log如下:
| 
 1 
2 
3 
4 
5 
6 
7 
8 
9 
10 
11 
12 
13 
14 
15 
16 
17 
18 
19 
20 
21 
22 
23 
24 
25 
26 
27 
28 
29 
30 
 | 
- (void)speak{     unsigned int numberOfIvars = 0;    Ivar *ivars = class_copyIvarList([self class], &numberOfIvars);    for(const Ivar *p = ivars; p < ivars+numberOfIvars; p++) {        Ivar const ivar = *p;        ptrdiff_t offset = ivar_getOffset(ivar);        const char *name = ivar_getName(ivar);        NSLog(@"Sark ivar name = %s, offset = %td", name, offset);    }    NSLog(@"my name is %p", &_name);    NSLog(@"my name is %@", *(&_name));}@implementation Test- (instancetype)init{    self = [super init];    if (self) {        NSLog(@"Test instance = %@", self);        void *self2 = (__bridge void *)self;        NSLog(@"Test instance pointer = %p", &self2);        id cls = [Sark class];        NSLog(@"Class instance address = %p", cls);        void *obj = &cls;        NSLog(@"Void *obj = %@", obj);        [(__bridge id)obj speak];    }    return self;}@end | 
输出结果如下:
| 
 1 
2 
3 
4 
5 
 | 
2014-11-11 00:56:02.464 Test[10475:1071029] Test instance = 2014-11-11 00:56:02.464 Test[10475:1071029] Test instance pointer = 0x7fff5fbff7c82014-11-11 00:56:02.465 Test[10475:1071029] Class instance address = 0x1000023c82014-11-11 00:56:02.465 Test[10475:1071029] Void *obj = 2014-11-11 00:56:02.465 Test[10475:1071029] Sark ivar name = _name, offset = 82014-11-11 00:56:02.465 Test[10475:1071029] my name is 0x7fff5fbff7c82014-11-11 00:56:02.465 Test[10475:1071029] my name is | 
Sark中Propertyname最终被转换成了Ivar加入到了类的结构中,Runtime通过计算成员变量的地址偏移来寻找最终Ivar的地址,我们通过上述输出结果,可以看到 Sark的对象指针地址加上Ivar的偏移量之后刚好指向的是Test对象指针地址。
这里的原因主要是因为在C中,局部变量是存储到内存的栈区,程序运行时栈的生长规律是从地址高到地址低。C语言到头来讲是一个顺序运行的语言,随着程序运行,栈中的地址依次往下走。
看下图,可以清楚的展示整个计算的过程:

我们可以做一个另外的实验,把Test Class 的init方法改为如下代码:
| 
 1 
2 
3 
4 
5 
6 
7 
8 
9 
10 
11 
12 
13 
14 
15 
16 
17 
18 
19 
20 
21 
 | 
@interface Father : NSObject@end@implementation Father@end@implementation Test- (instancetype)init{    self = [super init];    if (self) {        NSLog(@"Test instance = %@", self);        id fatherCls = [Father class];        void *father;        father = (void *)&fatherCls;        id cls = [Sark class];        void *obj;        obj = (void *)&cls;        [(__bridge id)obj speak];    }    return self;}@end | 
你会发现这个时候的输出变成了:
| 
 1 
2 
3 
4 
 | 
2014-11-08 21:40:36.724 Test[4845:543231] Test instance = 2014-11-08 21:40:36.725 Test[4845:543231] ivar name = _name, offset = 82014-11-08 21:40:36.726 Test[4845:543231] Sark instance = 0x7fff5fbff7b82014-11-08 21:40:36.726 Test[4845:543231] my name is 0x7fff5fbff7c02014-11-08 21:40:36.726 Test[4845:543231] my name is | 
关于C语言内存分配和使用的问题可参考这篇文章 http://www.th7.cn/Program/c/201212/114923.shtml。
(via:Chun Tips)
刨根问底Objective-C Runtime的更多相关文章
- 刨根问底Objective-C Runtime(4)- 成员变量与属性
		
http://chun.tips/blog/2014/11/08/bao-gen-wen-di-objective[nil]c-runtime(4)[nil]-cheng-yuan-bian-lian ...
 - Objective C Runtime 开发介绍
		
简介 Objective c 语言尽可能的把决定从编译推迟到链接到运行时.只要可能,它就会动态的处理事情.这就意味着它不仅仅需要一个编译器,也需要一个运行时系统来执行变异好的代码.运行时系统就好像是O ...
 - Objective-C:runtime
		
@import url(http://i.cnblogs.com/Load.ashx?type=style&file=SyntaxHighlighter.css); @import url(/ ...
 - iOS开发 runtime实现原理以及实际开发中的应用
		
自己写了一个小例子:有一些相关知识点和博客文章 A: 首先现在控制器里面初始化一个对象,然后调用对象的方法: #import "ViewController.h" #import ...
 - Objective-C Runtime初探:self super
		
题目 上题目,已知A是爷爷,B是爸爸,C是孙子. @interface A : NSObject - (void)f; @end @interface B : A - (void)f; - (void ...
 - IOS-RunTime(刨根问底)
		
方法调用 让我们看一下方法调用在运行时的过程(参照前文类在runtime中的表示) 如果用实例对象调用实例方法,会到实例的isa指针指向的对象(也就是类对象)操作.如果调用的是类方法,就会到类对象的i ...
 - Objective-C Runtime(一)预备知识
		
很早就知道了Objective-C Runtime这个概念,「Objective-C奇技淫巧」「iOS黑魔法」各种看起来很屌的主题中总会有它的身影:但一直没有深入去学习,一来觉得目前在实际项目中还没有 ...
 - iOS面试题
		
一个区分度很大的面试题 考察一个面试者基础咋样,基本上问一个 @property 就够了: @property 后面可以有哪些修饰符? 线程安全的: atomic,nonatomic 访问权限的 re ...
 - 李洪强iOS经典面试题下
		
李洪强iOS经典面试题下 21. 下面的代码输出什么? @implementation Son : Father - (id)init { self = [super init]; if (self) ...
 
随机推荐
- flash
			
1. 1.这种方式已经比较旧了, 2. html.push('<div class="flash-ad" style = "position:relative&qu ...
 - android开发------初识Activity
			
之前我们简单说过,Activity实际上是一个窗体,用来存放我们的程序外观. 我们先来创建一个空的Activity,不加载任何layout.要做的是,定义自己的类,继承android的Activity ...
 - 作业4-两人编程<词频统计>
			
协作:苗中峰,刘鑫成 我主要攻克排序,成哥写了文件流的使用.整合工作由我完成,成哥帮我查阅资料,避免和解决语法错误. 这次任务较作业三的变化是: * ...
 - 前端构建工具   webpack
			
一.自我初级认知 (是什么? 能干什么,有卵用? 有选择为什么要选你?(比较优势在哪) ) 适合的才是最好的 模块打包器(module bundler) 根据项目 ...
 - PHP ElasticSearch的使用
			
系统是Windows server 2003. ElasticSearch是一个基于Lucene的稳定的.分布式.RESTFul的搜索引擎.其实所谓的RestFul就是它提供URL供你调用(建立索引和 ...
 - CentOS6.5下搭建NFS文件服务器
			
本文参考这里 CentOS下搭建NFS服务器总结 环境介绍: 1. 服务器: 192.168.0.100 2. 客户机: 192.168.0.101 安装软件包: 服务器和客户机都要安装nfs 和 r ...
 - java-原生爬虫机制源码
			
这是一个web搜索的基本程序,从命令行输入搜索条件(起始的URL.处理url的最大数.要搜索的字符串),它就会逐个对Internet上的URL进行实时搜索,查找并输出匹配搜索条件的页面. 这个程序的原 ...
 - const、static、extern三个关键字
			
默认情况下,C语言的全局变量是全世界都可以访问的,也就是全局变量可以跨文件访问. extern可以引用全局变量 例如,如果有一个全局变量int money = 100;extern int money ...
 - 二叉查找树 C++实现(含完整代码)
			
一般二叉树的查找是通过遍历整棵二叉树实现,效率较低.二叉查找树是一种特殊的二叉树,可以提高查找的效率.二叉查找树又称为二叉排序树或二叉搜索树. 二叉查找树的定义 二叉排序树(Binary Search ...
 - BZOJ 1124: [POI2008]枪战Maf
			
1124: [POI2008]枪战Maf Time Limit: 10 Sec Memory Limit: 162 MBSubmit: 617 Solved: 236[Submit][Status ...