前言
日常开发中我们得知,当我们通过对象调用一个方法时,本质是通过objc_msgSend
给对象发送消息。这点我们可以通过clang
编译后的代码得知。
1 | MyPerson *p = [MyPerson new]; |
通过xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc OC源文件 -o 输出的CPP文件
编译得:
1 | MyPerson *p = ((MyPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("MyPerson"), sel_registerName("new")); |
可知接收消息的对象是:(id)objc_getClass("MyPerson")
。
接收的消息编号:sel_registerName("new")
== @selector(new)
。
通过分析objc4-750源码,以objc_msgSend
为入口,接下来我们开始分析整个消息发送及处理流程。
整个流程分为快速和慢速两种方式。
快速:通过汇编,在缓存(cache)的imp哈希表中寻找。这样的好处是C、C++等语言不能通过写一个函数,来直接保留未知的参数,跳转到任意的指针。而汇编通过调用寄存器,可很好的实现这一点。
慢速: 通过C、C++在方法列表中寻找。找到了会往chche中存。以上方法找不到,就会通过特殊的动态处理。
0x01 汇编缓存查找
在objc4-750源码中搜索_objc_msgSend
,点击查看在arm64
架构中的ENTRY _objc_msgSend
。代码和注释如下:
1 | ENTRY _objc_msgSend |
通过查看CacheLookup
的宏定义代码,得知缓存中寻找的三种形式:
CacheHit | CheckMiss | add
//1:找到直接返回
//2:找不到的话直接checkmiss
//3:在其它地方找到的话通过汇编直接add进缓存中。
1 | .macro CacheLookup |
查看CacheHit
的定义文件即可得知找到imp
后可直接返回.
1 | .macro CacheHit |
查看CheckMiss
的定义文件即可得知找不到imp
,便调用__objc_msgSend_uncached
。
1 | .macro CheckMiss |
查看__objc_msgSend_uncached
的代码中发现MethodTableLookup
的调用,继续跟进,便发现了__class_lookupMethodAndLoadCache3
的调用。
1 | .macro MethodTableLookup |
0x02 分析C++代码
继上:
1 | IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls) |
lookUpImpOrForward
:是寻找imp的关键函数。runtime
中涉及imp的获取底层都会走这个方法。比如class_getMethodImplementation
、class_getInstanceMethod
、class_getInstanceMethod
也是通过lookUpImpOrNil
,最后底层走这个方法的。
在下面的方法中大致操作为:
1、首先检测缓存,如果cache有的话直接就在缓存中查找返回imp.
2、如果类没被创建,便进行实例化操作。
3、第一次调用类的时候,执行初始化。
4、为了防止并发,再次从缓存中查找。
5、遍历当前类的父类,在父类中缓存的imp中查找
6、在父类的方法列表中,获取method_t对象。如果找到则缓存查找到的IMP
7、如果都没有找到,就尝试动态方法解析和消息转发。
1 | IMP lookUpImpOrForward(Class cls, SEL sel, id inst, |
0x03 动态方法解析
1 | // 如果没有找到,则尝试动态方法解析 |
_class_resolveMethod:
1 | void _class_resolveMethod(Class cls, SEL sel, id inst) |
在_class_resolveClassMethod
的实现中有如下代码,表示了消息的发送。可知消息的接受者_class_getNonMetaClass(cls, inst)
。
1 | BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend; |
进入class_getNonMetaClass
的实现中,得知返回的依旧是类对象,这样是方便能够在同一个类中处理,方便管理,而避免了去虚拟的元类中进行改动。
1 | static Class getNonMetaClass(Class metacls, id inst) |
在动态方法解析的过程中,都会调用lookUpImpOrNil
来递归查找动态解析方法的imp,而不会发生死递归的原因是在NSObject
中实现了动态方法解析,所以最终会找到它。
同时我们通过重写NSObject
中的+ (BOOL)resolveInstanceMethod:(SEL)sel
,在这个方法中通过给没有实现的sel添加imp方法避免崩溃,同时也可以将crash传给后台做崩溃统计等工作。
0x04 消息转发
以下的_objc_msgForward_impcache
因为苹果闭源是无法看到实现的,我们可以通过定义一个instrumentObjcMessageSends
,或者通过反编译函数实现的可执行文件来查看其流程。这里简单介绍一下第二种。
1 | imp = (IMP)_objc_msgForward_impcache; |
通过实现动态方法解析,未实现转发而崩溃的堆栈信息可以看出_objc_msgForward_impcache
具体是在CoreFoundation.framework
中实现。如图:CoreFoundation.framework
的本地地址:
1 | /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation |
通过hopper
或者ida
打开,搜索_CFInitialize
,再依次进入_forwarding_prep_0_
,__forwarding__
。通过查看伪代码,会有以下发现:
1 | var_50 = rbx; |
如下,代码实现消息转发。
1 | // 只有汇编调用 没有源码实现 |
如果没有实现消息转发,我们再根据源码追踪一下走位。
进入消息转发的汇编部分。如下:
1 | STATIC_ENTRY __objc_msgForward_impcache |
查看__objc_forward_handler
回调
1 | void *_objc_forward_handler = (void*)objc_defaultForwardHandler; |
如下可以见到我们常见的崩溃信息打印的源头了。
1 |
|