本篇文章主要解决以下问题
- 说说你对 runtime 的理解。
- 你了解 isa 指针吗?
- 类的结构是怎样的?
- class_rw_t 与 class_ro_t 的区别?
- runtime 中,SEL 和 IMP 的区别?
- runtime 如何通过 selector 找到对应 IMP 的地址?
- objc 的消息发送机制是怎样的?
- objc 中向一个 nil 对象发送消息,会发生什么?
- 消息转发到 forwordinginvacation 方法,如何拿到返回值?
- 什么时候会报 unrecognized selector 异常?
- runtime 中常用的方法。
- runtime怎么添加属性、方法等
- 使用runtime Associate方法关联的对象,需要在主对象dealloc的时候释放么?
- 什么是method swizzling?
- 能否向编译后得到的类中增加实例变量?能否向运行时创建的类中添加实例变量?为什么?
- 你在项目中用过runtime吗?举个例子。
1. 说说你对 runtime 的理解。
答:
Runtime 提供了一套 C/C++ 的 API,使 OC 程序可以在运行期改变其结构,因此 OC 是一门动态性比较强的语言。
在 iOS 系统中 KVO、Category、weak 等都是基于 runtime 实现的。
自己也经常利用 runtime 的特性来实现一些东西。(详见本篇第 6 问)
****** 低调的分割线 ******
我觉得上面第一部分的答案还是不够口语化,也不太好,显得空洞,感觉面试答起来比较像是背的,听起来比较尴尬,我重新反思了一下,这是新的第一部分的答案:
像其它的语言,比如说 C 语言,在编译的时候就已经知道了程序应该执行的代码。但是在 OC 中,会尽可能地把决策去推迟到运行的时候再去做。比如到运行的时候再去确定对象的类型、确定方法的接收者,给对象添加方法等等。这些都是可以通过 runtime 的 api 来实现的,用书面一点的话来说就是:runtime 的 API 使 OC 程序可以在它运行到时候改变结构,所以 OC 是一门动态性比较强的语言。
对于 runtime 来说,最重要的就是消息传递机制,要理解清楚消息传递机制就需要弄明白 isa 的作用,再弄清楚类的的结构,再去理解消息传递机制的流程,才能理解的透彻。(扯到这一步就好说了,这些问题下边都有讲到)
分析:
这种开放的问题难以把握如何回答好,笔者认为首先要答到 objc 是一门动态性比较强的语言(为什么说比较强,因为还有更强的),然后再说说系统的一些基于 runtime 实现的东西,再说说自己项目中基于 runtime 实现的一些东西。
2. 你了解 isa 指针吗?
答:
在 64bit 之前 isa 是一个普通的指针,指向 Class/Meta-Class。
在 64bit 之后,苹果对其进行了优化,把它做成了一个共用体,运用了位域的技术存储了更多的信息。(isa 还是只占用 8 个字节,用 8 个字节存储了更多的信息,但是要找到相应了 class 或者 meta-class 需要与 ISA_MASK 进行一次位运算,要更详细的了解可以点击这里。)
我们可以简单地认为,instance 的 isa 指向 class,class 的 isa 指向 meta-class,meta-class 的 isa 指向基类的 meta-class。
在使用 objc_msgSend 的时候,是通过 receiver 的 isa 找到它的类或者元类,然后去找方法的。
分析:
回顾一下之前的内容,OC 中的对象分为 instance、class与meta-class,它们的 isa 与 superclass 的指向关系如图所示。

这个问题主要也是谈三点:
- isa 是什么
- 怎么指向的
- 在消息发送中的作用
3. 类的结构是怎样的?
答:

分析:
前面的文章在讲 OC 对象的存储结构时说到 class 对象中存储的有 isa、superclass 指针、方法列表、属性列表、协议列表。meta-class 与 class 都是 Class 类型的,所以他们的结构其实是一样的,只不过存储的东西有区别,meta-class 的方法列表中存储的是类方法,class 的方法列表中存储的是对象方法。
具体的代码结构如上图所示。(将就看一下吧,作图工具:网页版美图秀秀....)
这是在源码中找到的结构,笔者对其做了精简,保留了现在我们需要关注的信息,源码可以点击这里下载。
现在分析一下这些"美图"。过程比较长,但这是这年头出去面试毫无疑问必须肯定百分之百要掌握的,建议准备面试的同学每天默写一遍笔者下边敲出来的代码。
struct objc_class : objc_object {
// Class ISA;
Class superclass;
cache_t cache;
class_data_bits_t bits;
}
在源码中可以找到这样一句代码 typedef struct objc_class *Class;,所以 Class 其实本质上就是 objc_class结构体。
我们都知道 Class 中有 isa 和 superclass,但是这里成员变量 ISA 前面有两个斜线,它被注释掉了,很奇怪,这是因为 objc_class 是继承于 objc_object 的,来看一下 objc_object 的结构就明白了。

objc_object 中有 isa 这个共用体,所以objc_class中也有它。
superclass 指向父类。(回忆:基类的元类对象有什么特殊的地方?)
cache 是方法缓存,用于存储先前已经调用过的方法的信息。
使用 objc_msgSend 向接受者发送消息时,会先根据 isa 找到 class/meta-class,然后去 class/meta-class 的成员 cache 中找方法,如果没有找到,才会去方法列表中找。这样可以提升效率。
bits 是一串很长的数字,它存储了多项信息,把它和 FAST_DATA_MASK 进行与运算,可以得到一个新的数字,这个数字是一个地址,指向 class_rw_t 结构体。
objc_class 的成员变量已经简单地介绍完了,现在来看一看更具体的。
先来看一看 cache_t,cache 的类型是 cache_t。
struct cache_t {
struct bucket_t *_buckets;
mask_t _mask;
mask_t _occupied;
}
_buckets 是一个数组,用于存放方法信息,数组中元素的类型是 struct bucket_t。
_mask 翻译过来就是掩码,看见 mask 就要意识到它是用来做位运算的,比如 ISA_MASK、FAST_DATA_MASK。先记住一个结论,_mask 的长度是 _buckets 的长度减一。
_oppcupied 表示的是 _buckets 中存储的方法的数量。
bucket_t 的结构是这样的:
struct bucket_t {
SEL _key; //笔者这里与源码不同,因为这里实际上就是以 @selector(xxx) 作为 key 的,这样写方便记忆一些
IMP _imp; //函数的内存地址
}
方法缓存的机制并不是简单的调用一个就往 _buckets 中添加一个,否则这里也就不需要有 _mask 与 _occupied 这两个成员了。它是一个散列表。
当创建一个类之后,系统自动会给 _buckets 分配一段内存空间,它里面有 N 个元素,都用 NULL 填充,_mask 的值是 N-1。
当调用 objc_msgSend(obj, foo) 的时候
- 在
objClass中找到了cache - 然后得到
@selector(foo)的地址p,计算index = p & _mask - 使这个
index做为_buckets的索引,找到_buckets[index]- 两个数相与
C = A & B,那么C必定不大于 A 和 B 中最小的数。所以_buckets的索引最大的就是_mask,所以_mask的值是_buckets的长度减一。
- 两个数相与
- 判断
if (_bucket[index]._key == @selector(foo))- 如果相等的话就证明找到了,就可以直接使用了。
- 如果
_bucket[index]._key == 0,证明这个方法没有存入数组中并且这个位置还没有被别的方法占用,将@selector(foo)存入_bucket[index]做为_key,将函数foo的地址存入_imp。 - 如果
_bucket[index]._key != 0 && _bucket[index]._key != @selector(foo)- 不同的两个数和
_mask相与,是有可能得到一个结果的。 - 出现这种情况说明这个位置被比
foo先调用的一个函数占用了。现在还不能确定foo在不在_buckets中。 - 这时让
index -= 1,然后又进入上一步:使index作为索引... - 如果
index已经减到零了,还是没有空位,就令index = _mask,然后继续找 - 如果找完一圈之后,还是没有找到
- 那么就证明
foo确实不在散列表中 - 也说明散列表已经满了
- 散列表会扩容变成原来的两倍,然后修改
_mask的值 - 将散列表清空。因为
_mask已经变了,对于之前的元素来说,已经不能通过_mask准确地计算出索引了 - 计算
@selector(foo)的地址,与新的_mask相与得到新的索引index,然后将foo存入散列表
- 那么就证明
- 查询过程的代码如下
bucket_t * cache_t::find(cache_key_t k, id receiver) { assert(k != 0); bucket_t *b = buckets(); mask_t m = mask(); mask_t begin = cache_hash(k, m); mask_t i = begin; do { if (b[i].key() == 0 || b[i].key() == k) { return &b[i]; } } while ((i = cache_next(i, m)) != begin); // hack Class cls = (Class)((uintptr_t)this - offsetof(objc_class, cache)); cache_t::bad_cache(receiver, (SEL)k, cls); } static inline mask_t cache_next(mask_t i, mask_t mask) { return i ? i-1 : mask; }
- 不同的两个数和
cache 缓存以及方法查找方式是要重点掌握的,至少得知道是以散列表的方式,然后以什么为索引,通过什么进行比较。这里已经介绍完毕,我先去抽根烟,呆会说说 bits。
bits 是一串 64 位的二进制数。不同的位上写着不同的数,控制它和不同的掩码 A_MASK、B_MASK、C_MASK 相与,就可以得到我们想要的位上的数据。bits & FAST_DATA_MASK 就可以得到 class_rw_t 的地址。

class_rw_t 中我们关注有一个指向 struct class_ro_t 的指针 ro,以及存放类信息的二维数组们。这里的代码就不敲了,要记住的是 class_rw_t 中有 ro 指针,还有存放方法列表、属性列表、协议列表的二维数组。
method_array_t 是一个二维数组,它里面存放的是 method_list_t,method_list_t 中存放的是 method_t。(property_arry_t 与 protocol_array_t 也是,它们类似。)
method_t 结构如下
struct method_t {
SEL name;
const char * types;
IMP imp;
}
name 就代表的是方法的名称,imp 代表的是方法实现的地址。types 是方法的类型编码,主要就是返回值以及各个参数的类型。当我们想找一个方法的时候,我们找到了 method_t,也就知道了它的名字,地址,以及返回值、参数的类型,不就是找到它了吗?当你想报复一个人的时候,你有他的名字、地址以及钥匙......
class_rw_t 与 class_ro_t
- 从名字上来看,一个是
readwrite、一个是readonly。所以你懂的,前者是可读写的,后者是只读的。 class_ro_t包含的是类的初始信息,class_rw_t会包含分类中的信息。- 前面介绍过了,
category编译之后是一个结构体,里面有各种信息。运行过程中runtime会把这些信息合并到class/meta-class中去,其实就是合并到class/meta-class中通过bits找到的class_rw_t的那些二维数组中来。而类初始信息只有一份,所以class_ro_t结构体中的那些数组用一维的就可以了。 - 你可能注意到了(我知道你没有),
class_ro_t中有一个ivars在class_rw_t中是没有的。这个是类的成员变量。ivars存在于一个readonly数组中,这也可以作为解释以下两个问题的一个角度- 为啥
category不能添加成员变量呢?- 为啥能添加方法呢?
- 为啥不能向编译后的类中添加实例变量呢?
- 为啥
- 前面介绍过了,
类的结构说完了,你能否回答这两道面试题?
- runtime 中,SEL 和 IMP 的区别?
- runtime 如何通过 selector 找到对应 IMP 的地址?
4. objc 的消息发送机制是怎样的?
ObjC 的动态特性是基于 Runtime 的消息传递机制的,在 ObjC 中,消息的传递都是动态的。
ObjC - 基于 Runtime 的语言,它会尽可能地把决策从编译时和连接时推迟到运行时(简单来说,就是编译后的文件不全是机器指令,还有一部分中间代码,在运行的时候,通过 Runtime 再把需要转换的中间代码在翻译成机器指令)这使得 ObjC 有着很大的灵活性。比如:
1、动态的确定类型
2、我们可以动态的确定消息传递的对象
3、动态的给对象增加方法的实现 等等
什么是消息传递?和 C语言 的调用函数有什么区别?
- 函数调用就是直接跳到地址执行。代码在编译、优化之后生成了汇编代码,然后连接各种库,完了就生成了可以执行的代码。
C语言在编译时就已经决定了程序所应执行的代码。Objc中向receiver发送消息,receiver并不一定调用这个方法,而是到了运行时才会去看receiver是否响应这个消息,再决策是执行这个方法还是其它方法,或者转发给其它对象。
Tips: 编译时,编译器只是简单的进行语法分析。比如 NSData *obj = [[NSObject alloc] init];,在编译时 obj 是 NSData 类型的,在运行时它是 NSObject 类型的。
接下来对消息机制进行讲述。
当我们程序执行 [obj foo]的时候,你可能会和笔者一样去想foo是个什么鬼东西?。
当我们程序执行 [obj foo] 的时候,这句代码会被编译成 objc_msgSend(obj, @selector(foo)),即向 obj 发送消息 foo。然后就会进入消息机制的三大阶段,消息发送、动态解析、消息转发。
- 消息发送:
runtime会先找到obj的isa,然后通过isa找到class/meta-class,在class/meta-class以及他们的父类的cache和方法列表中去找foo。 - 动态解析:如果在上阶段没有找到就会进入该阶段。
runtime就会给class/meta-class发送resolveInstanceMethod:/resolveClassMethod:消息,在这里可以给接受者增加执行方法。 - 消息转发:如果没有在动态解析中进行处理,就会进入到该阶段。
下边这张图描述了消息发送的具体流程:

这张图注意两个地方:
- 可以向
nil发送消息,不会崩溃,也不会有结果。 - 就算是从父类找到的方法,最后也会缓存到
消息接收者的 class/meta-class 的cache中。
这一张是动态方法解析的流程图:

我们已经提到了好几遍动态解析这个词语,你可能还不明白它是什么意思。我们来看一个实际的例子:
@interface Person : NSObject
- (void)run;
@end
@implementation Person
@end
实现一个 Person 类,声明一个 -(void)run 方法,但并不实现它。然后在某个 main 函数中让 person 实例对象调用 run,嗯,你知道的它会崩溃。
现在,我们在 Person 的 implementation 中添加代码:
@implementation Person
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
return [super resolveInstanceMethod:sel];
}
@end
然后在方法内打上断点,再运行程序,通过断点可以发现,程序会进入这个方法。

我们用 Runtime API 给类把添加一个
run 方法吧,在 OC 中,编译的时候你没添加方法的实现,没关系,运行的时候动态地添加一个也是 OK 的。
void aTmpMethod(id self, SEL _cmd)
{
NSLog(@"%s", __func__);
}
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
if (sel == @selector(run)) {
class_addMethod(self, sel, (IMP)aTmpMethod, "v@:");
return YES;
}
return [super resolveInstanceMethod:sel];
}
我们利用 class_addMethod 给 Person 动态地添加了一个方法,四个参数分类代表要添加给谁,你的名字是啥,你家在哪,钥匙呢。
如果 aTmpMethod 不是 C 语言的函数,而是一个 OC 方法。那么可以这样写:
Method method = class_getInstanceMethod(self, @selector(aTmpMethod));
class_addMethod(self, self, method_getImplementation(method), method_getTypeEncoding(method))
这个时候再运行程序,发现它不会崩溃了,并且调用了 aTmpMethod 函数。

上述过程我们就是动态地给 Person 添加了一个名为 run 的方法。如果给 Person meta-class 添加方法,流程类似。
现在我们再来回过头看这张流程图。
当消息机制在消息发送阶段没有找到方法的后,会来到动态方法解析阶段。
- 系统会判断,如果这个方法曾经来到过动态解析阶段,那么就直接进入下一个阶段-消息转发。
- 因为如果它之前到过动态解析阶段,然后有回到消息发送阶段,但是走完消息发送阶段又没有找到方法,说明它上次来的时候就没有给它动态添加方法。所以就直接进入下一阶段。
- 如果这是第一次来动态方法解析,那么就会进入
resolveInstanceMethod:/resolveClassMethod:。 - 然后将它标记为:已经动态解析过的。
- 最后再回到消息发送,再走一遍消息发送的流程。
最后一个阶段是消息转发:

如果第一二阶段都没有处理成功,就会来到这一阶段-消息转发。
- 消息转发会先调用
forwardingTargetForSelector:方法- 如果这个方法返回了一个对象,那么就让这个对象去处理这个消息
objc_msgSend(对象, sel)
- 返回值为nil,就会调用
methodSignatureForSelector:方法methodSignatureForSelector:要求返回一个方法的签名- 方法的签名指的是方法的返回值类型以及参数的类型,常用的生成方式如下:
[NSMethodSignature signatureWithObjCTypes:"v@:"];
- 如果返回nil,则会直接调用
doesNotRecognizeSelector:方法。
- 如果
methodSignatureForSelector:返回不为 nil,则会调用forwardInvocation:方法。- 来到这个方法后,就算什么都不处理,运行程序也不会崩溃了。
- 比如你这个方法里面什么都不写
- 比如你只写一句
NSLog(@"Hello world!");
- 还可以处理这个参数
anInvocation。 NSInvocation封装了方法调用者、方法名、方法的签名。- 比如可以修改方法的调用者
[anInvocation invokeWithTarget:[[Cat alloc] init]]
- 比如拿到方法的返回值类型
- 上边提到
NSInvocation中有方法、调用者、方法签名 - 所以只要拿到方法签名,就能找到返回值的类型
NSMethodSignature *sig = [anInvocation valueForKey:@"_signature"]; //拿到方法签名 const char *returnType = sig.methodReturnType;//这个字符串中的第一个字符就是返回值的类型 - 上边提到
- 来到这个方法后,就算什么都不处理,运行程序也不会崩溃了。
objc_msgSend 已经讲完了,相信你已经可以回答下边这三个问题了:
- objc 中向一个 nil 对象发送消息,会发生什么?
- 消息转发到 forwordinginvacation 方法,如何拿到返回值?
- 什么时候会报 unrecognized selector 异常?
讲一下 super 的问题,先看一个实际的例子:

super 和消息机制不了解的话,这道题你是不明白所以然的。
下边是打印的结果:

现在讲明白为什么是这样:
self是什么?self是run的隐藏参数- 每一个方法都有隐藏参数
self与_cmd - 比如
- (void)run编译后会变成- (void)run:(id)self sel: (SEL)_cmd,_cmd就是@selector(run) - 所以
self是一个对象,类型是方法调用者
[self class]发生了什么?- 向
self发送消息class - 通过
self的isa找到了Person,然而Person中找不到class的实现 - 就通过
Person的superclass指针找到了NSObject - 在
NSObject中找到class的实现并调用。
- 向
super是什么?super是编译器的一个标识、一个关键字- 它并不是一个对象
[super class]发生了什么?- 编译器看见这句代码的时候会把它编译成
struct __rw_objc_super arg = { self, class_getSuperclass(objc_getClass("Person")) } objc_msgSendSuper2(arg, @selector(class)) objc_msgSendSuper2的第一个参数是结构体,结构体的第一个成员是self,第二个成员是Person的父类- 当执行
objc_msgSendSuper2(arg, @selector(class))时- 会向
arg的第一个参数self发送消息class,即self是消息的接收者 - 但是会从第二个参数的缓存中开始查找方法
class - 制在
NSObject中找到了class方法,进行调用,由于消息的接收者是self,所以返回的是self的类,而不是NSObject。
- 会向
- 编译器看见这句代码的时候会把它编译成
5. runtime 中常用的方法。
/*
动态创建一个类
参数分别是要创建的类的父类、类名、额外的内存空间(一般传0)
常考问题:为什么是 Pair,它是是什么意思?
答:pair 的意思是一对。创建一个类就是创建 Class 和 Meta-Class
*/
objc_allocateClassPair(Class _Nullable __unsafe_unretained superclass, const char * _Nonnull name, size_t extraBytes)
//注册一个类(要在类注册之前添加成员变量)
void objc_registerClassPair(Class cls)
//销毁一个类
void objc_disposeClassPair(Class cls)
//获取isa指向的Class
Class object_getClass(id obj)
//设置isa指向的Class
Class object_setClass(id obj, Class cls)
//判断一个OC对象是否为Class
BOOL object_isClass(Class cls)
//判断一个Class是否为元类
BOOL class_isMetaClass(Class cls)
//获取父类
Class class_getSuperclass(Class cls)
//获取一个实例变量
Ivar class_getInstanceVariable(Class cls, const char *name)
//拷贝实例变量列表(最后需要调用free释放)
Ivar *class_copyIvarList(Class cls, unsigned int *outCount)
//动态添加成员变量(已经注册的类是不能动态添加成员变量的)
class_addIvar(Class _Nullable __unsafe_unretained cls, const char * _Nonnull name, size_t size, uint8_t alignment, const char * _Nullable types)
//获取成员变量的相关信息
const char *ivar_getName(Ivar v)
const char *ivar_getTypeEncoding(Ivar v)
//获取一个属性
objc_property_t class_getProperty(Class cls, const char *name)
//拷贝属性列表(最后需要调用free释放)
objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount)
//动态添加属性
class_addProperty(Class _Nullable __unsafe_unretained cls, const char * _Nonnull name, const objc_property_attribute_t * _Nullable attributes, unsigned int attributeCount)
//动态替换属性
class_replaceProperty(Class _Nullable __unsafe_unretained cls, const char * _Nonnull name, const objc_property_attribute_t * _Nullable attributes, unsigned int attributeCount)
//获取属性的一些信息
const char *property_getName(objc_property_t property)
const char *property_getAttributes(objc_property_t property)
//获得一个实例方法、类方法
Method class_getInstanceMethod(Class cls, SEL name)
Method class_getClassMethod(Class cls, SEL name)
//方法实现相关操作
IMP class_getMethodImplementation(Class cls, SEL name)
IMP method_setImplementation(Method m, IMP imp)
void method_exchangeImplementations(Method m1, Method m2)
//拷贝方法列表(最后需要调用free释放)
Method *class_copyMethodList(Class cls, unsigned int *outCount)
//动态添加方法
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types)
//动态替换方法
IMP class_replaceMethod(Class cls, SEL name, IMP imp, const char *types)
//获取方法相关的信息(带有copy的需要调用free去释放)
SEL method_getName(Method m)
IMP method_getImplementation(Method m)
const char *method_getTypeEncoding(Method m)
unsigned int method_getNumberOfArguments(Method m)
char *method_copyReturnType(Method m)
char *method_coayArgumentType(Method m, unsigned int index)