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.s
304行,是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可运行的源码
本文章之所以图片比较少,我觉得还是跟着代码敲一遍,印象比较深刻。
最怕一生碌碌无为,还安慰自己平凡可贵。
广告时间