arm64之后isa是使用联合体使用更少的空间存储更多的数据,以及如何自定义和使用联合体,objc_class->cache_t cache是一个是缓存最近调用class的方法,当缓存剩余空间小余1/4则进行扩容,扩容为原来的两倍,扩容之后,已存储的method_t扩容之后之后被清空。今天我们在了解runtime的消息转发机制。
基础知识
OC中的方法调用,其实都是转换为objc_msgSend函数的调用
objc_msgSend的执行流程可以分为3大阶段
- 消息发送
 - 动态方法解析
 - 消息转发
 
那么我们根据这三块内容进行源码解读。源码执行的顺序大概如下
1  | objc-msg-arm64.s  | 
消息发送
objc_msgSend是汇编写的,在源码objc-msg-arm64.s304行,是objc_msgSend的开始,_objc_msgSend结束是351行,
进入到objc_msgSend函数内部一探究竟:
1  | ENTRY _objc_msgSend // _objc_msgSend 开始  | 
当objc_msgSend(id,SEL,arg)的id为空的时候,跳转标签LNilOrTagged,进入标签内,当等于0则跳转LReturnZero,进入到LReturnZero内,清除数据和return。不等于零,获取isa和class,调用CacheLookup NORMAL,进入到CacheLookup内部
1  | .macro CacheLookup //.macro 是一个宏 使用 _cmd&mask 查找缓存中的方法  | 
汇编代码左边是代码,右边是注释,大概都可以看懂的。
当命中则return imp,否则则跳转CheckMiss,进入到CheckMiss内部:
1  | .macro CheckMiss  | 
刚才传的值是NORMAL,则跳转__objc_msgSend_uncached,进入到__objc_msgSend_uncached内部(484行):
1  | STATIC_ENTRY __objc_msgSend_uncached  | 
调用MethodTableLookup,我们查看MethodTableLookup内部:
1  | .macro MethodTableLookup  | 
最终跳转到__class_lookupMethodAndLoadCache3,去掉一个下划线就是c函数,在runtime-class-new.mm 4856行,
调用了函数lookUpImpOrForward(cls, sel, obj, 
                              YES/*initialize*/, NO/*cache*/, YES/*resolver*/);,第一次会初始化cls和resolver的值,
中最终跳转到c/c++函数lookUpImpOrForward,该函数是最终能看到的c/c++,现在我们进入到lookUpImpOrForward内部查看:
1  | /***********************************************************************  | 
SUPPORT_INDEXED_ISA是在arm64和LP64 还有arm_arch_7k>2为1,iphone属于arm64、mac os属于LP64,所以SUPPORT_INDEXED_ISA = 1.
1  | // Define SUPPORT_INDEXED_ISA=1 on platforms that store the class in the isa  | 
lookUpImpOrForward函数的 大概思路如下:
首次已经从缓存中查过没有命中所以不再去缓存中查了,然后判断cls是否已经实现,cls->isRealized(),没有实现的话进行实现realizeClass(cls),主要是将初始化read-write data和其他的一些数据,后续会细讲。然后进行cls的初始化_class_initialize(),当cls需要初始化和没有初始化的时候进行cls初始化,初始化会加入到一个线程,同步执行,先初始化父类,再初始化子类,数据的大小最小是4,扩容规则是:n*2+1;然后再次获取impcache_getImp,然后在cls方法中查找该method,然后就是在superclass中查找方法,直到父类是nil,找到的话,获取imp并将cls和sel加入到cache中,否则进入到消息解析阶段_class_resolveMethod,在转发阶段,不是元类的话,进入到_class_resolveInstanceMethod是元类的话调用_class_resolveClassMethod,这两种分别都会进入到lookUpImpOrNil,再次查找IMP,当没找到的话就返回,找到的话用objc_msgSend发送消息实现调用SEL_resolveInstanceMethod并标记triedResolver为已动态解析标志。然后进入到消息动态转发阶段_objc_msgForward_impcache,至此runtime发送消息结束。
借用网上找一个图, 可以更直观的看出流程运转。

realizeClass()解析
realizeClass是初始化了很多数据,包括cls->ro赋值给cls->rw,添加元类version为7,cls->chooseClassArrayIndex()设置cls的索引,supercls = realizeClass(remapClass(cls->superclass));
    metacls = realizeClass(remapClass(cls->ISA()))初始化superclass和cls->isa,后边针对没有优化的结构进行赋值这里不多讲,然后协调实例变量偏移布局,设置cls->setInstanceSize,拷贝flags从ro到rw中,然后添加subclass和rootclass,最后添加类别的方法,协议,和属性。
1  | /***********************************************************************  | 
这里最后添加类别的数据是调用了methodizeClass函数,这个函数首先添加method_list_t *list = ro->baseMethods()到rw->methods.attachLists(&list, 1),然后将属性property_list_t *proplist=ro->baseProperties添加到rw->properties.attachLists(&proplist, 1),最后将协议列表protocol_list_t *protolist = ro->baseProtocols追加到rw->protocols.attachLists(&protolist, 1),如果是metaclass则添加SEL_initialize,然后从全局NXMapTable *category_map删除已经加载的category_list,最后调用attachCategories(cls, cats, false /*don't flush caches*/)将已经加载的cats的方法添加到cls->rw上面并且不刷新caches。
1  | /***********************************************************************  | 
attachCategories()解析
methodizeClass之前rw初始化的时候并没有将其他数据都都复制给rw,现在methodizeClass实现了将本来的ro数据拷贝给rw,然后attachCategories将
分类的方法,属性,协议追加到cls->data->rw,我们进入attachCategories内部
1  | static void attachCategories(Class cls, category_list *cats, bool flush_caches)  | 
rw->list->attachLists()解析
添加attachLists函数规则是后来的却添加到内存的前部分,这里就清楚为什么后编译类别能后边的类别覆盖前边的类别的相同名字的方法。
1  | void attachLists(List* const * addedLists, uint32_t addedCount) {  | 
class初始化完成了,然后再次尝试获取imp = cache_getImp,由于缓存没有中间也没添加进去,所以这里也是空的,然后从getMethodNoSuper_nolock获取该cls的方法列表中查找,没有的话再从superclass查找cache和method,找到的话,进行log_and_fill_cache至此消息发送完成。
消息动态解析
动态解析函数_class_resolveMethod(cls, sel, inst),如果不是元类调用_class_resolveInstanceMethod,如果是的话调用_class_resolveClassMethod
1  | /***********************************************************************  | 
在resolveInstanceMethod,查找SEL_resolveInstanceMethod,传值不用初始化,不用消息解析,但是cache要查找。没有找到的直接返回,找到的话使用objc_msgSend发送消息调用SEL_resolveInstanceMethod。
1  | /***********************************************************************  | 
在_class_resolveClassMethod中,第一步先去lookUpImpOrNil查找+SEL_resolveClassMethod方法,没找到的就结束,找到则调用objc_msgsend(id,sel)
1  | static void _class_resolveClassMethod(Class cls, SEL sel, id inst)  | 
动态解析至此完成。
消息转发
_objc_msgForward_impcache是转发的函数地址,在搜索框搜索发现,这个函数除了.s文件中有,其他地方均只是调用,说明这个函数是汇编实现,在objc-msg-arm64.s 531 行发现一点踪迹
1  | STATIC_ENTRY __objc_msgForward_impcache //开始__objc_msgForward_impcache  | 
当跳转到adrp    x17, __objc_forward_handler@PAGE这一行,搜搜索函数_objc_forward_handler,看到只是个打印函数,并没有其他函数来替代这个指针,那么我们用其他方法来探究。
1  | __attribute__((noreturn)) void  | 
网上有大神总结的点我们先参考下
1  | // 伪代码  | 
验证动态解析
我们简单定义一个test函数,然后并执行这个函数。
1  | @interface Person : NSObject  | 
[p test]在第一次执行的时候会走到消息动态解析的这一步,然后通过objc_msgsend调用了test,并且把test添加到了缓存中,所以输出了+[FYPerson resolveInstanceMethod:],在第二次调用的时候,会从缓存中查到imp,所以直接输出了-[FYPerson test3]。
在+resolveInstanceMethod可以拦截掉实例方法的动态解析,在+resolveClassMethod可以拦截类方法。
1  | 
  | 
拦截+resolveClassMethod,在条件为sel==@selector(test)的时候,将函数实现+test3()的IMP使用class_addMethod添加到Person上,待下次调用test的时候直接通过imp = cache_getImp(cls, sel);获取到imp函数指针并且执行。
我们也可以通过添加c函数的imp来实现给class添加函数实现。
1  | +(BOOL)resolveInstanceMethod:(SEL)sel{  | 
v16@0:8是返回值为void参数占用16字节大小,第一个是从0开始,第二个从8字节开始。
这段代码和上面的其实本质上是一样的,一个是给class添加函数实现,使sel和imp对应起来,这个是将c函数的imp和sel进行关联,添加缓存之后,使用objc_msgsend()效果是一样的。
验证消息转发
消息转发可分为3步,第一步根据- (id)forwardingTargetForSelector:(SEL)aSelector返回的类对象或者元类对象,将方法转发给该对象。假如第一步没实现,则第二步根据返回的-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector或+ (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector函数签名,在第三步(void)forwardInvocation:(NSInvocation *)anInvocation调用函数[anInvocation invoke]进行校验成功之后进行调用函数。
1  | @interface Person : NSObject  | 
我们定义了一个Person只声明了test没有实现,然后在消息转发第一步forwardingTargetForSelector将要处理的对象返回,成功调用了Student的test方法。
第一步没拦截,可以在第二步拦截。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19//消息转发第二步 没有对象来处理方法,那将函数签名来实现
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
	if (aSelector == @selector(test)) {
		NSMethodSignature *sign = [NSMethodSignature signatureWithObjCTypes:"v16@0:8"];
		return sign;
	}
	return [super methodSignatureForSelector:aSelector];
}
// 函数签名已返回,到了函数调用的地方
//selector 函数的sel
//target   函数调用者
//methodSignature 函数签名
//NSInvocation  封装数据的对象
- (void)forwardInvocation:(NSInvocation *)anInvocation{
    NSLog(@"%s",__func__);
}
//输出
-[Person forwardInvocation:]
打印出了-[Person forwardInvocation:]而且没有崩溃,在forwardInvocation:(NSInvocation *)anInvocation怎么操作看开发者怎么处理了,探究下都可以做什么事情。
看到NSInvocation的属性和函数,sel和target是读写,函数签名是必须的,所以(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector必须将函数签名返回。
1  | @property (readonly, retain) NSMethodSignature *methodSignature;//只读  | 
当拦截方法是类方法的时候,可以用+ (id)forwardingTargetForSelector:(SEL)aSelecto拦截,
1  | //class 转发  | 
也可以用返回return [[Student alloc]init];将class类方法转化成实例方法,最后调用了Student的对象方法test3。其实本质上都是objc_msgSend(id,SEL,...),我们修改的只是id的值,id类型在这段代码中本质是对象,所以我们可以return instance也可以reurn class。
1  | + (id)forwardingTargetForSelector:(SEL)aSelector{  | 
将刚才写的methodSignatureForSelector和forwardInvocation改成类方法,也是同样可以拦截类方法的。我们看下
1  | //消息转发第二步 没有class来处理方法,那将函数签名来实现  | 
测过其实对象方法和类方法都是用同样的流程拦截的,对象方法是用-方法,类方法是用+方法。
总结
- objc_msgSend发送消息,会首先在cache中查找,查找不到则去方法列表(顺序是
cache->class_rw_t->supclass cache ->superclass class_rw_t ->动态解析) - 第二步是动态解析,能在resolveInstanceMethod或+ (BOOL)resolveClassMethod:(SEL)sel来来拦截,可以给class新增实现函数,达到不崩溃目的
 - 第三步是消息转发,转发第一步可以在
+ (id)forwardingTargetForSelector:(SEL)aSelector或- (id)forwardingTargetForSelector:(SEL)aSelector拦截类或实例方法,能将对象方法转发给其他对象,也能将对象方法转发给类方法,也可以将类方法转发给实例方法 - 第三步消息转发的第二步可以在
+ (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector或- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector实现拦截类和实例方法并返回函数签名 - 第三步消息转发的第三步可以
+ (void)forwardInvocation:(NSInvocation *)anInvocation或- (void)forwardInvocation:(NSInvocation *)anInvocation实现类方法和实例方法的调用和获取返回值 
资料下载
- 学习资料下载
 - demo code
 - runtime可运行的源码
 
本文章之所以图片比较少,我觉得还是跟着代码敲一遍,印象比较深刻。
最怕一生碌碌无为,还安慰自己平凡可贵。
广告时间
