最近尝试往category中添加实例变量失败,但是可以添加方法。于是,研究了一下runtime的实现和使用。runtime的原理展开来分析的话还是挺复杂的。下载了最新的源码(objc4-680),发现和现在网上的多数教程还是有出入的,跟进起来很困难。
所以尝试自己整理了一下,加了一些自己的理解,具体细节可能有些小出入。参考文章:Objective-C Runtime
Runtime基础
在Objective-C中,使用[receiver message]语法并不会马上执行receiver对象的message方法的代码,而是向receiver发送一条message消息。
其实[receiver message]被编译器转化为:
1 | id objc_msgSend ( id self, SEL op, ... ); |
现在可以看出[receiver message]真的不是一个简简单单的方法调用。因为这只是在编译阶段确定了要向接收者发送message这条消息,而receive将要如何响应这条消息,那就要看运行时发生的情况来决定了。如果消息的接收者能够找到对应的selector,那么就相当于直接执行了接收者这个对象的特定方法;否则,消息要么被转发,或是临时向接收者动态添加这个selector对应的实现内容,要么就干脆玩完崩溃掉。
id
objc_msgSend第一个参数类型为id,它是一个指向objc_object结构体指针,它包含一个Class isa指针(完整的定义在objc_private.h中):1
2
3
4
5typedef struct objc_object *id;
struct objc_object {
Class isa OBJC_ISA_AVAILABILITY;
};
所有对象都是继承于
objc_object的,所以所有对象都有isa指针,即都有一个objc_class结构体的指针。(注意,这和 superclass 是不同的)
SEL
objc_msgSend函数第二个参数类型为SEL,它是selector在Objc中的表示类型.selector是方法选择器,可以理解为区分方法的 ID,而这个 ID 的数据结构是SEL:1
typedef struct objc_selector *SEL;
Class
之所以说isa是指针是因为Class其实是一个指向objc_class结构体的指针:1
typedef struct objc_class *Class;
这个结构体在runtime.h中有定义:1
2
3
4
5
6
7
8
9
10
11
12
13
14struct objc_class {
Class isa OBJC_ISA_AVAILABILITY;
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;
} OBJC2_UNAVAILABLE;
其中OBJC2_UNAVAILABLE表示:在objc2中这些属性已经不在此定义。
在objc2中,objc_class的完整定义在objc-runtime-new.h中:1
2
3
4
5
6
7
8
9
10
11
12
13struct objc_object {
Class isa OBJC_ISA_AVAILABILITY;
};
struct objc_class : objc_object {
Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
class_rw_t *data() {
return bits.data();
}
};
和runtime.h中的定义对比,少了许多属性,多出一个bits属性。这些属性都包含在了class_rw_t的data对象中。
注意到objc_class中也有一个isa对象(继承自objc_object),这是因为一个ObjC类本身同时也是一个对象,为了处理类和对象的关系,runtime库创建了一种叫做 元类 (Meta Class)的东西,类对象所属类型就叫做元类,它用来表述类对象本身所具备的元数据。类方法就定义于此处,因为这些方法可以理解成类对象的实例方法。每个类仅有一个类对象,而每个类对象仅有一个与之相关的元类。
当你发出一个类似[NSObject alloc]的消息时,你事实上是把这个消息发给了一个类对象 (Class Object) ,这个类对象必须是一个元类的实例,而这个元类同时也是一个根元类 (root meta class)的实例。所有的元类最终都指向根元类为其超类。所有的元类的方法列表都有能够响应消息的类方法。所以当 [NSObject alloc]这条消息发给类对象的时候,objc_msgSend()会去它的元类里面去查找能够响应消息的方法,如果找到了,然后对这个类对象执行方法调用。
看明白这个图上面就都明白了。
class_rw_t
bits 中包含了一个指向 class_rw_t 结构体的指针,它的定义如下:1
2
3
4
5
6
7
8
9
10struct class_rw_t {
uint32_t flags;
uint32_t version;
const class_ro_t *ro;
method_array_t methods;
property_array_t properties;
protocol_array_t protocols;
}
其中,const class_ro_t *ro包含了所有成员变量的一维数组以及各个基础的(base)属性、方法和协议method_array_t methods、property_array_t properties、protocol_array_t protocols则包含了所有拓展的属性方法和协议。注意:这里的属性只是getset方法,不包含成员变量。
基础的属性和方法和拓展的方法没有存在一个对象中,所以不能在 category 中添加成员变量。但是完全用 runtime 创建的类是可以动态添加的。
具体如何应用可看下一篇。
部分类的声明
这部分将上面涉及的类的声明贴出以供参照,可以直接跳过。
class_ro_t:
1 | struct class_ro_t { |
method_t
method_list_t和method_array_t内都是method_t类型:
1 | struct method_t { |
- 方法名类型为
SEL - 方法类型
types是个char指针,其实存储着方法的参数类型和返回值类型。 imp指向了方法的实现,本质上是一个函数指针
ivar_t
ivar_list_t 内是ivar_t类型:
1 | struct ivar_t { |
访问私有变量可以通过 kvc 和 runtime 的
class_getInstanceVariable,两种方式获取。
property_t
property_list_t和property_array_t内都是property_t类型:
1 | struct property_t { |
protocol_t
protocol_list_t和protocol_array_t内都是protocol_t类型:1
2
3
4
5
6
7
8
9
10
11struct protocol_t : objc_object {
const char *mangledName;
struct protocol_list_t *protocols;
method_list_t *instanceMethods;
method_list_t *classMethods;
method_list_t *optionalInstanceMethods;
method_list_t *optionalClassMethods;
property_list_t *instanceProperties;
uint32_t size; // sizeof(protocol_t)
uint32_t flags;
};
Ivar,Method,Category,objc_property_t
外部可以通过一系列的方法获得类的信息,诸如:class_copyIvarList,class_copyPropertyList等方法,可以获得类的实例变量和属性,对应的类型是Ivar、objc_property_t,它们分别是ivar_t,property_t类型的指针。
1 | typedef struct method_t *Method; |
消息
几种函数派发方式
函数的派发方式,常见的有三种:
- 直接派发
- 函数表派发
- 消息派发
直接派发速度最快,例如 c++ 中的函数调用,编译时直接拿到函数的地址。
函数表派发回味每一个类维护一个函数表,大部分语言称之为虚函数表,里面记录着所有的函数,如果父类函数被 override 的话,表里面只保存被 override 之后的函数。运行时会根据这个表去决定要调用的函数。如 Java。
c++ 中要在父类方法中用 virtual 修饰,才会将直接派发转为函数表派发,实现多态。Java 可以通过 final 修饰指定为直接派发。
消息派发就是 OC 的实现方式。
objc_msgSend函数
看起来像是objc_msgSend返回了数据,其实objc_msgSend从不返回数据而是你的方法被调用后返回了数据。下面详细叙述下消息发送步骤:
- 检测这个
selector是不是要忽略的。比如有了垃圾回收就不理会retain,release这些函数了。 - 检测这个
target是不是nil对象。ObjC 的特性是允许对一个nil对象执行任何一个方法不会 Crash,因为会被忽略掉。 - 如果上面两个都过了,那就开始查找这个类的
IMP,先从cache里面找,完了找得到就跳到对应的函数去执行。 - 如果
cache找不到就找一下方法分发表。 - 如果分发表找不到就到超类的分发表去找,一直找,直到找到
NSObject类为止。 - 如果还找不到就要开始进入动态方法解析了.
编译器会根据情况在objc_msgSend, objc_msgSend_stret, objc_msgSendSuper, 或 objc_msgSendSuper_stret四个方法中选择一个来调用。如果消息是传递给超类,那么会调用名字带有Super的函数;如果消息返回值是数据结构而不是简单值时,那么会调用名字带有stret的函数。
当我们调用[super selector] 时,Runtime 会调用 objc_msgSendSuper方法,objc_msgSendSuper 方法有两个参数,super 和 op,Runtime 会把 selector 方法选择器赋值给 op。而 super 是一个 objc_super 结构体指针,objc_super 结构体定义如下:1
struct objc_super { id receiver; Class class; };
需要注意的是:这里的receiver仍然是self本身。当我们想通过[super class]获取超类时,编译器只是将指向self的id指针和class的SEL传递给了objc_msgSendSuper函数,因为只有在NSObject类才能找到class方法,然后class方法调用object_getClass(),接着调用objc_msgSend(objc_super->receiver, @selector(class)),传入的第一个参数是指向self的id指针,与调用[self class]相同,所以我们得到的永远都是self的类型。
举个栗子,问下面的代码输出什么:
1 | @implementation Son : Father |
答案是全部输出 Son.
当调用 [super class] 时,会转换成 objc_msgSendSuper 函数:
- 先构造
objc_super结构体,结构体第一个成员就是self。第二个成员是(id)class_getSuperclass(objc_getClass(“Son”)). - 去
Father这个类里去找- (Class)class,没有,然后去NSObject类去找,找到了。最后内部是使用objc_msgSend(objc_super->receiver, @selector(class))去调用,此时已经和[self class]调用相同了,所以两个输出结果都是Son。
这里调用class只是举例,所有调用super的方法,最后都变成了self的调用。
其实就是即使是 [super class] 调用的是父类的 class 方法,但是接收对象永远还是 self
objc_super -> receiver永远是子类
Category 的实现原理
概述
runtime 依赖于 dyld 动态加载,在 objc-os.mm 文件中可以找到入口,它的调用栈简单整理如下:1
2
3
4
5
6void _objc_init(void)
└──const char \*map_2_images(...)
└──const char \*map_images_nolock(...)
└──void _read_images(header_info **hList, uint32_t hCount)
└──static void remethodizeClass(Class cls)
└──static void attachCategories(Class cls, category_list *cats, bool flush_caches)
Category 相关的数据结构
首先来了解一下一个 Category 是如何存储的,在 objc-runtime-new.h 中可以看到如下定义:1
2
3
4
5
6
7
8struct 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;
};
从category的定义也可以看出category的可为(可以添加实例方法,类方法,甚至可以实现协议,添加属性)和不可为(无法添加实例变量)。
处理 Category
对 Category 中方法的解析并不复杂,首先来看一下 attachCategories 的简化版代码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24static void attachCategories(Class cls, category_list *cats, bool flush_caches) {
if (!cats) return;
bool isMeta = cls->isMetaClass();
method_list_t **mlists = (method_list_t **)malloc(cats->count * sizeof(*mlists));
// Count backwards through cats to get newest categories first
int mcount = 0;
int i = cats->count;
while (i--) {
auto& entry = cats->list[i];
method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
if (mlist) {
mlists[mcount++] = mlist;
}
}
auto rw = cls->data();
prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
rw->methods.attachLists(mlists, mcount);
free(mlists);
if (flush_caches && mcount > 0) flushCaches(cls);
}
首先,通过 while 循环,我们遍历所有的 category,也就是参数 cats 中的 list 属性。对于每一个 category,得到它的方法列表 mlist 并存入 mlists 中。换句话说,我们将所有 category 中的方法拼接到了一个大的二维数组中,数组的每一个元素都是装有一个 category 所有方法的容器。
编译好之后 ivar 的结构就已经固定了,可以通过偏移地址来直接获取成员变量。但是对于方法,它在 Macho-O 中只保存了
class_ro_t的部分,在 runtime 初始化的时候,会通过 runtime 到 mach-o 中的__data,_objc_catlist中取出所有的分类信息,然后将分类中的方法属性和协议插到class_rw_t的数组中(class_rw_t应该是保存在堆上的),它的各个属性是指向 data 段的。因为
class_rw_t是堆上的,它的属性方法列表是一个二维数组,可以将 data 段中的各个方法数组添加到这个二维数组中,并且class_addMethod也可以用相同的方法添加到二维数组里,所以方法是可以动态添加的。但是成员变量是保存在
class_ro_t中的,它是保存在 data 段中有固定的空间的,无法扩大。因此成员变量无法动态添加。
另外需要注意一点,不是所有分类都是放到
__data,_objc_catlist中的。如果是自己的类的分类,编译后直接就把方法和原始类的方法丢在一起了。如果是其他动态库的分类,比如给 UIViewController 创建一个分类,那么是会保存在__data,_objc_catlist段中,通过 runtime 动态添加给 UIViewController的。
在 while 循环外,我们得到了拼接成的方法,此时需要与类原来的方法合并:1
2auto rw = cls->data();
rw->methods.attachLists(mlists, mcount);
rw 是一个 class_rw_t类型的结构体指针。根据 runtime 中的数据结构,它有一个 methods 结构体成员,并从父类继承了 attachLists 方法,用来合并 category 中的方法:1
2
3
4
5
6
7
8
9void attachLists(List* const * addedLists, uint32_t addedCount) {
if (addedCount == 0) return;
uint32_t oldCount = array()->count;
uint32_t newCount = oldCount + addedCount;
setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));
array()->count = newCount;
memmove(array()->lists + addedCount, array()->lists, oldCount * sizeof(array()->lists[0]));
memcpy(array()->lists, addedLists, addedCount * sizeof(array()->lists[0]));
}
需要注意的是,无论执行哪种逻辑,参数列表中的方法都会被添加到二维数组的前面。因此 category 中定义的同名方法不会替换类中原有的方法,但是对原方法的调用实际上会调用 category 中的方法。
load 方法的执行
+load 方法在 Class 和 Category 中都会有一个数组保存。在 runtime load_image 的时候,会一次调用 Class 和 Category 的 load 方法。
load 方法的调用顺序是先调用父类的再调用子类的,先调用 Class 的,再调用 Category 的。
和 +initialize 方法的异同
+load方法会被特殊对待全部执行,而+initialize方法则只会像普通方法一样会被分类覆盖。+initialize方法在类传递第一个消息之前执行。+load在分类添加到 runtime 时执行。+initialize方法先执行父类方法,再执行子类方法。+load也是这样
问题
- 什么是消息机制?
- id 是个什么类型?
- isa 是个什么类型?
- objc_class 中的 isa 指向什么?有什么用?
- 子类,父类,NSObject,之间 super class 与 isa 指向的关系图是怎样?
- objc_msgSend 的执行过程?
- 为什么调用
[super class]还是返回的还是 self 的类名? - category 为什么只能存方法,不能存实例变量?
- category 会替换原来的方法吗?
- 动态方法解析与消息转发的三步各是什么作用?
- 转发和多继承有什么关系?