前言
上篇 动态方法决议中,如果在动态方法决议
也没有找到imp
,动态方法决议会返回imp = _objc_msgForward_impcache
,进行消息转发,那么消息是如何转发的?下面具体探索消息转发的原理。
instrumentObjcMessageSends 分析
创建一个Command Line Tool
的新Project
,并创建一个对象ZLObject
:
@interface ZLObject : NSObject
- (void)instanceMethod;
@end
@implementation ZLObject
@end
复制代码
声明一个函数instrumentObjcMessageSends
打印系统调用的方法的列表,调用和声明方式如下:
extern void instrumentObjcMessageSends(BOOL flag);
int main(int argc, const char * argv[]) {
@autoreleasepool {
ZLObject *obj = [ZLObject alloc];
instrumentObjcMessageSends(YES);
[obj instanceMethod];
instrumentObjcMessageSends(NO);
}
return 0;
}
复制代码
执行结果:
-[ZLObject instanceMethod]: unrecognized selector sent to instance 0x10792cc40
复制代码
instrumentObjcMessageSends 源码
void instrumentObjcMessageSends(BOOL flag)
{
bool enable = flag;
// 如果设置的 enable 和之前的 objcMsgLogEnabled 相同,无需再次设置。
// objcMsgLogEnabled 默认为 false
if (objcMsgLogEnabled == enable)
return;
// 如果允许打印,则冲洗所有方法缓存,以便获得一些跟踪
if (enable)
_objc_flush_caches(Nil);
// 如果不等于-1,说明本地有日志文件,则同步日志文件
if (objcMsgLogFD != -1)
fsync (objcMsgLogFD);
// 是否允许
objcMsgLogEnabled = enable;
}
复制代码
objcMsgLogEnabled
是打印日志的标记,而该值就是从instrumentObjcMessageSends
方法传入的参数值。
objcMsgLogEnabled
全局搜索objcMsgLogEnabled
,在慢速查找lookUpImpOrForward
流程中,done
的代码块里,执行log_and_fill_cache
,缓存并打印日志。
static void
log_and_fill_cache(Class cls, IMP imp, SEL sel, id receiver, Class implementer)
{
#if SUPPORT_MESSAGE_LOGGING
if (slowpath(objcMsgLogEnabled && implementer)) {
// 打印发送消息日志
bool cacheIt = logMessageSend(implementer->isMetaClass(),
cls->nameForLogging(),
implementer->nameForLogging(),
sel);
if (!cacheIt) return;
}
#endif
// 插入缓存
cls->cache.insert(sel, imp, receiver);
}
复制代码
当objcMsgLogEnabled
为ture
时,会执行logMessageSend
方法。
logMessageSend
bool logMessageSend(bool isClassMethod, const char *objectsClass, const char *implementingClass, SEL selector)
{
char buf[ 1024 ];
// 创建/打开日志文件
if (objcMsgLogFD == (-1))
{
snprintf (buf, sizeof(buf), "/tmp/msgSends-%d", (int) getpid ());
objcMsgLogFD = secure_open (buf, O_WRONLY | O_CREAT, geteuid());
if (objcMsgLogFD < 0) {
// no log file - disable logging
objcMsgLogEnabled = false;
objcMsgLogFD = -1;
return true;
}
}
// 创建要插入的日志条目
snprintf(buf, sizeof(buf), "%c %s %s %s\n",
isClassMethod ? '+' : '-',
objectsClass,
implementingClass,
sel_getName(selector));
objcMsgLogLock.lock();
// 写入日志
write (objcMsgLogFD, buf, strlen(buf));
objcMsgLogLock.unlock();
return false;
}
复制代码
如果开启缓存,会在/tmp/msgSends-%d
中写下日志:
将msgSends
前缀的文件都删除,再次执行上面的案例代码,再次查看缓存文件:
结论
resolveInstanceMethod
动态方法决议,如果找不到imp
,则进行消息转发。- 消息转发流程如下:
- forwardingTargetForSelector
- methodSignatureForSelector
- resolveInstanceMethod
- doesNotRecognizeSelector
forwardingTargetForSelector 快速消息转发
从objc_msgSend
和动态方法决议
来看,当调用没有实现的方法imp
时,在进行快速查找,慢速查找,动态方法决议后,会返回imp = _objc_msgForward_impcache
,进行消息转发。首先进入消息快速转发forwardingTargetForSelector
方法。
源码如下:
+ (id)forwardingTargetForSelector:(SEL)sel {
return nil;
}
- (id)forwardingTargetForSelector:(SEL)sel {
return nil;
}
复制代码
特点:
- 返回值是
对象
,将用作新的接收方对象
,消息调度将转移到该新对象中。换句话说,当前类调用没有实现
的方法时,调度到新的对象中去。- 返回值默认是
nil
,意思为不执行重定向。- 返回值
不能
为self
,会造成死循环。- 如果在非
NSObject
中实现,使用super
实现返回的结果。- 比常规转发快一个数量级
- 如果转发的目标是NSInvocation,或在转发过程中操纵参数或返回值,则此方法将不起作用。
forwardingTargetForSelector 案例
接着上面instrumentObjcMessageSends
的案例,改进如下:
@interface ZLObject : NSObject
+ (void)classMethod;
- (void)instanceMethod;
@end
@implementation ZLObject
+ (id)forwardingTargetForSelector:(SEL)aSelector {
NSLog(@"%s - %@",__func__,NSStringFromSelector(aSelector));
return [super forwardingTargetForSelector:aSelector];
}
- (id)forwardingTargetForSelector:(SEL)aSelector {
NSLog(@"%s - %@",__func__,NSStringFromSelector(aSelector));
return [super forwardingTargetForSelector:aSelector];
}
@end
复制代码
分别执行对象方法和类方法:
ZLObject *obj = [ZLObject alloc];
[obj instanceMethod];
[ZLObject classMethod];
复制代码
分别打印执行结果:
-[ZLObject forwardingTargetForSelector:] - instanceMethod
-[ZLObject instanceMethod]: unrecognized selector sent to instance 0x10050cae0
+[ZLObject forwardingTargetForSelector:] - classMethod
+[ZLObject classMethod]: unrecognized selector sent to class 0x100008270
复制代码
说明:在forwardingTargetForSelector
对象方法和类方法中,可以捕获到异常方法。
forwardingTargetForSelector 重定向
添加一个转发类ZLForwardObject
类:
@interface ZLForwardObject : NSObject
+ (void)classMethod;
- (void)instanceMethod;
@end
@implementation ZLForwardObject
+ (void)classMethod {
NSLog(@"%s",__func__);
}
- (void)instanceMethod {
NSLog(@"%s",__func__);
}
@end
复制代码
改进ZLObject
类:
+ (id)forwardingTargetForSelector:(SEL)aSelector {
NSLog(@"%s - %@",__func__,NSStringFromSelector(aSelector));
if (aSelector == @selector(classMethod)) {
return [ZLForwardObject class];
}
return [super forwardingTargetForSelector:aSelector];
}
- (id)forwardingTargetForSelector:(SEL)aSelector {
NSLog(@"%s - %@",__func__,NSStringFromSelector(aSelector));
if (aSelector == @selector(instanceMethod)) {
return [ZLForwardObject alloc];
}
return [super forwardingTargetForSelector:aSelector];
}
复制代码
重新执行对象方法和类方法:
-[ZLObject forwardingTargetForSelector:] - instanceMethod
-[ZLForwardObject instanceMethod]
+[ZLObject forwardingTargetForSelector:] - classMethod
+[ZLForwardObject classMethod]
复制代码
这样就重定向到ZLForwardObject
中,执行ZLForwardObject
中的对象方法和类方法了。
总结
重定向的方法名必须和捕获到的方法名相同,否则无法重定向成功。例如案例中的
ZLObject
执行的instanceMethod
和classMethod
方法名,在重定向ZLForwardObject
的类中,方法名要相同。
methodSignatureForSelector 慢速消息转发
源码如下:
+ (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
_objc_fatal("+[NSObject methodSignatureForSelector:] "
"not available without CoreFoundation");
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
_objc_fatal("-[NSObject methodSignatureForSelector:] "
"not available without CoreFoundation");
}
复制代码
特点:
- 返回值是
NSMethodSignature对象
,该对象包含:由给定SEL的方法标识的描述。- 用于协议的实现。
- 在消息转发期间,必须创建
NSInvocation
对象。- 如果对象维持一个代理,或者处理它未实现的消息时,则应重写此方法以返回适当的方法签名。
其中消息转发时,必须实现forwardInvocation
方法:
+ (void)forwardInvocation:(NSInvocation *)invocation {
[self doesNotRecognizeSelector:(invocation ? [invocation selector] : 0)];
}
- (void)forwardInvocation:(NSInvocation *)invocation {
[self doesNotRecognizeSelector:(invocation ? [invocation selector] : 0)];
}
复制代码
methodSignatureForSelector 案例
注意: 如果使用快速转发的案例,需将快速转发的方法注释掉,或者将其返回nil
+ (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
NSLog(@"%s - %@",__func__,NSStringFromSelector(sel));
return [super methodSignatureForSelector:sel];
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
NSLog(@"%s - %@",__func__,NSStringFromSelector(sel));
return [super methodSignatureForSelector:sel];
}
复制代码
分别执行对象方法和类方法:
-[ZLObject methodSignatureForSelector:] - instanceMethod
-[ZLObject instanceMethod]: unrecognized selector sent to instance 0x10072db80
+[ZLObject methodSignatureForSelector:] - classMethod
+[ZLObject classMethod]: unrecognized selector sent to class 0x100008290
复制代码
methodSignatureForSelector 实现方法签名
+ (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
NSLog(@"%s - %@",__func__,NSStringFromSelector(sel));
if (sel == @selector(classMethod)) {
Method m = class_getClassMethod([ZLForwardObject class], @selector(classMethod));
const char * type = method_getTypeEncoding(m);
return [NSMethodSignature signatureWithObjCTypes:type];
}
return [super methodSignatureForSelector:sel];
}
+ (void)forwardInvocation:(NSInvocation *)anInvocation {
NSLog(@"%s - %@ - %@", __func__, anInvocation.target, NSStringFromSelector(anInvocation.selector));
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
NSLog(@"%s - %@",__func__,NSStringFromSelector(sel));
if (sel == @selector(instanceMethod)) {
Method m = class_getInstanceMethod([ZLForwardObject class], @selector(instanceMethod));
const char * type = method_getTypeEncoding(m);
return [NSMethodSignature signatureWithObjCTypes:type];
}
return [super methodSignatureForSelector:sel];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
NSLog(@"%s - %@ - %@", __func__, anInvocation.target, NSStringFromSelector(anInvocation.selector));
}
复制代码
再次调用对象方法和类方法:
-[ZLObject methodSignatureForSelector:] - instanceMethod
-[ZLObject forwardInvocation:] - <ZLObject: 0x107925df0> - instanceMethod
+[ZLObject methodSignatureForSelector:] - classMethod
+[ZLObject forwardInvocation:] - ZLObject - classMethod
复制代码
这样虽然实现了消息代理,并且forwardInvocation
也有了回调,但是新方法没有处理,换句话说就是事务
没有处理。(方法的调用都是消息发送,这些消息就是事务,可实现可不实现。)
实现forwardInvocation
的事务处理:
+ (void)forwardInvocation:(NSInvocation *)anInvocation {
Class fClass = [ZLForwardObject class];
if ([self respondsToSelector:anInvocation.selector]) {
// 自己能够响应
[anInvocation invoke];
} else if ([fClass respondsToSelector:anInvocation.selector]) {
// 消息重定向
[anInvocation invokeWithTarget:fClass];
} else {
// 可以在这里上报错误
[super forwardInvocation:anInvocation];
}
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
ZLForwardObject *fObj = [ZLForwardObject alloc];
if ([self respondsToSelector:anInvocation.selector]) {
// 自己能够响应
[anInvocation invoke];
} else if ([fObj respondsToSelector:anInvocation.selector]) {
// 消息重定向
[anInvocation invokeWithTarget:fObj];
} else {
// 可以在这里上报错误
[super forwardInvocation:anInvocation];
}
}
复制代码
重新调用对象方法和类方法:
-[ZLObject methodSignatureForSelector:] - instanceMethod
-[ZLForwardObject instanceMethod]
+[ZLObject methodSignatureForSelector:] - classMethod
+[ZLForwardObject classMethod]
复制代码
总结
methodSignatureForSelector
必须与forwardInvocation
成对出现一起使用。- 慢速消息转发可以根据自身业务和需求灵活处理。
- 相对于快速转发给了很大的灵活性。
- 如果提炼出来在
NSObject
分类中实现,对于OC
方法找不到的崩溃都能避免掉。但是这只是假象的消失,造成了很多资源的浪费。因为能执行到这一步必然经历了很多流程(快速查找→慢速查找→动态方法决议→快速转发→慢速转发
)
doesNotRecognizeSelector
当慢速转发也找不到重定向机制的话,最后进入doesNotRecognizeSelector
:
+ (void)doesNotRecognizeSelector:(SEL)sel {
_objc_fatal("+[%s %s]: unrecognized selector sent to instance %p",
class_getName(self), sel_getName(sel), self);
}
- (void)doesNotRecognizeSelector:(SEL)sel {
_objc_fatal("-[%s %s]: unrecognized selector sent to instance %p",
object_getClassName(self), sel_getName(sel), self);
}
复制代码
- 该方法并不能处理事务,只是让异常可控,拿到错误信息。
- 这也是在日常开发中遇到方法崩溃,打印的崩溃信息的出处。
消息转发的汇编分析
下面将通过汇编的方式探索消息转发流程。将上面案例中的快速转发和慢速转发的方法注释掉,执行。崩溃后bt
打印:
通过分析消息流程:start
→ main.m
→ _CF_forwarding_prep_0
→ ___forwarding___
→ doesNotRecognizeSelector
。其中_CF_forwarding_prep_0
和___forwarding___
才是探究的研究方向。并且都属于CoreFoundation
的库。
CoreFoundation
通过 opensource 下载
CoreFoundation
源码
但是去搜索_CF_forwarding_prep_0
和___forwarding___
相关字段时,并没有在CoreFoundation
源码中搜索到。这说明苹果对于这部分的内容没有开源
。
CoreFoundation库的获取
- 新建一个可以真机或者模拟器的
Project
,执行image list
获取镜像列表,并获取CoreFoundation
库。
- 根据查找到的库链接,找到对应的
CoreFoundation
可执行文件
- 使用
Hopper
打开找到的CoreFoundation
可执行文件(以arm64
为例),搜索forwarding
字段:
至此,_forwarding_prep_0
和___forwarding___
相关字段就找到了。下面具体分析各自的作用。
__forwarding_prep_0
分析
int ___forwarding_prep_0___(int arg0, int arg1, int arg2, int arg3, int arg4, int arg5) {
// 转发流程
rax = ____forwarding___(rsp, 0x0);
if (rax != 0x0) {
rax = *rax;
} else {
// 如果消息转发没实现
rax = objc_msgSend(arg0, arg1);
}
return rax;
}
复制代码
____forwarding___
方法之前,是拉伸栈空间、操作寄存器存储一些数据____forwarding___
的返回值r0
,如果不存在,执行objc_msgSend
,作为返回值。
__forwarding__
伪代码分析
int ____forwarding___(int arg0, int arg1) {
SEL sel = arg1;
Class obj = arg0;
Class cls = object_getClass(obj);
const char * className = class_getName(cls);
forwardingTargetForSelector(cls, sel, className);
return 0;
}
void forwardingTargetForSelector(Class cls, SEL sel, const char * className) {
// 是否能响应 forwardingTargetForSelector
if (class_respondsToSelector(cls, @selector(forwardingTargetForSelector:))) {
id obj = [cls forwardingTargetForSelector:sel];
if (obj) {
// 返回 forwardingTargetForSelector 消息接收者
return obj;
} else {
SEL currentSel = @selector(methodSignatureForSelector:);
methodSignatureForSelector(cls, currentSel);
}
} else {
SEL currentSel = @selector(methodSignatureForSelector:);
methodSignatureForSelector(cls, currentSel);
}
}
void methodSignatureForSelector(Class cls, SEL sel) {
// 是否野指针
if (strncmp(className, "_NSZombie_", 0xa) == 0x0) {
Class cls = class_getSuperclass(cls);
SEL currentSel = @selector(doesNotRecognizeSelector:);
doesNotRecognizeSelector(cls, currentSel);
} else {
if (class_respondsToSelector(cls, @selector(methodSignatureForSelector:))) {
NSMethodSignature *signature = [cls methodSignatureForSelector:sel];
if (signature) {
_forwardStackInvocation(cls, signature);
} else {
doesNotRecognizeSelector(cls, sel);
}
} else {
doesNotRecognizeSelector(cls, sel);
}
}
}
void _forwardStackInvocation(Class cls, NSMethodSignature *signature) {
if (class_respondsToSelector(cls, @selector(_forwardStackInvocation:))) {
// 执行dispatch_once相关逻辑
NSInvocation *invocation = [NSInvocation requiredStackSizeForSignature:signature];
void *bytes;
NSInvocation *inv = [invocation _initWithMethodSignature:signature frame:NULL buffer:NULL size:bytes];
[cls _forwardStackInvocation:invocation];
} else {
forwardInvocation(cls, signature);
}
}
void forwardInvocation(Class cls, NSMethodSignature *signature) {
if (class_respondsToSelector(cls, @selector(forwardInvocation:))){
NSInvocation *invocation = [NSInvocation _invocationWithMethodSignature:signature frame:NULL];
[cls forwardInvocation:invocation];
} else {
doesNotRecognizeSelector(cls, sel);
}
}
void doesNotRecognizeSelector(Class cls, SEL sel) {
if (class_respondsToSelector(cls, @selector(doesNotRecognizeSelector:))) {
[cls doesNotRecognizeSelector:sel];
} else {
return nil;
}
}
复制代码
- 判断
forwardingTargetForSelector
快速转发,如果快速转发没有实现(返回nil
)。- 判断是不是
野指针
,如果是野指针
,则执行doesNotRecognizeSelector
流程;否则执行methodSignatureForSelector
慢速转发流程。methodSignatureForSelector
如果没有返回签名信息,不处理,也不会继续进行下面的流程。- 如果进入
methodSignatureForSelector
,先判断有没有实现_forwardStackInvocation
,如果没有实现,进入_forwardStackInvocation
流程;否则进入forwardInvocation
流程。相当于_forwardStackInvocation
是一个私有的前置条件。- 最后,如果
forwardInvocation
也没有实现,则执行doesNotRecognizeSelector
流程。
消息转发流程图
第二次调用lookUpImpOrForward
流程
上篇 动态方法决议 中,已经探索第二次执行lookUpImpOrForward
的原因:是因为在消息转发流程中,调用了class_getInstanceMethod
,该方法再次调用了lookUpImpOrForward
,且 behavior = LOOKUP_RESOLVER
,意思是在lookUpImpOrForward
方法中再次执行动态方法决议。那么消息转发流程是如果调用class_getInstanceMethod
的?
在执行第二次的resolveInstanceMethod
中,打印sel
:
(lldb) po sel
"instanceMethod1"
(lldb) po sel
"instanceMethod1"
(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
* frame #0: 0x000000010032acec libobjc.A.dylib`resolveInstanceMethod(inst=0x0000000000000000, sel="instanceMethod1", cls=ZLSubObject) at objc-runtime-new.mm:6274:21
frame #1: 0x0000000100314423 libobjc.A.dylib`resolveMethod_locked(inst=0x0000000000000000, sel="instanceMethod1", cls=ZLSubObject, behavior=0) at objc-runtime-new.mm:6318:9
frame #2: 0x00000001003130a6 libobjc.A.dylib`lookUpImpOrForward(inst=0x0000000000000000, sel="instanceMethod1", cls=ZLSubObject, behavior=0) at objc-runtime-new.mm:6582:16
frame #3: 0x00000001002ea479 libobjc.A.dylib`class_getInstanceMethod(cls=ZLSubObject, sel="instanceMethod1") at objc-runtime-new.mm:6189:5
frame #4: 0x00007fff20438653 CoreFoundation`__methodDescriptionForSelector + 276
frame #5: 0x00007fff20451fa0 CoreFoundation`-[NSObject(NSObject) methodSignatureForSelector:] + 30
frame #6: 0x00007fff204224ef CoreFoundation`___forwarding___ + 396
frame #7: 0x00007fff204222d8 CoreFoundation`_CF_forwarding_prep_0 + 120
frame #8: 0x0000000100003e10 ZLObjc`main(argc=<unavailable>, argv=<unavailable>) + 64 [opt]
frame #9: 0x00007fff20362f5d libdyld.dylib`start + 1
frame #10: 0x00007fff20362f5d libdyld.dylib`start + 1
(lldb)
复制代码
从堆栈信息中,可以看到:
___forwarding___
→methodSignatureForSelector
→__methodDescriptionForSelector
→class_getInstanceMethod
。
反汇编方式探究
- 在
CoreFoundation
中,搜索methodSignatureForSelector
:
- 查看
__methodDescriptionForSelector
int ___methodDescriptionForSelector(int arg0, int arg1) {
rbx = arg1;
var_30 = *___stack_chk_guard;
var_40 = arg0;
// 如果obj为空,就是没有处理方法,跳转loc_120e62
if (arg0 == 0x0) goto loc_120e62;
loc_120e62:
// class_getInstanceMethod(cls, sel);
rax = class_getInstanceMethod(var_40, rbx);
if (rax != 0x0) {
rax = method_getDescription(rax);
r15 = *rax;
rbx = *(rax + 0x8);
} else {
rbx = 0x0;
r15 = 0x0;
}
goto loc_120e89;
loc_120e89:
if (*___stack_chk_guard == var_30) {
rax = r15;
} else {
rax = __stack_chk_fail();
}
// 返回 class_getInstanceMethod() 结果。
return rax;
}
复制代码
- 查看
class_getInstanceMethod
Method class_getInstanceMethod {
rax = _class_getInstanceMethod(rdi, rsi);
return rax;
}
复制代码
- 根据汇编特性,
_class_getInstanceMethod
其实是调用c++的class_getInstanceMethod
函数:
Method class_getInstanceMethod(Class cls, SEL sel)
{
if (!cls || !sel) return nil;
// 慢速查找方法,且 behavior = LOOKUP_RESOLVER
lookUpImpOrForward(nil, sel, cls, LOOKUP_RESOLVER);
// 查找 cls 的 method
return _class_getMethod(cls, sel);
}
复制代码
符号断点探究
- 根据堆栈信息,通过符号断点方式,添加
methodSignatureForSelector:
符号断点:
- 进入
__methodDescriptionForSelector
:
- 进入
class_getInstanceMethod
:
总结
- 实例方法:
- methodSignatureForSelector
→___methodDescriptionForSelector
→class_getInstanceMethod
→lookUpImpOrForward
→resolveMethod_locked
→resolveInstanceMethod
。 - 类方法(元类对象方法):
+ methodSignatureForSelector
→___methodDescriptionForSelector(元类)
→class_getInstanceMethod
→lookUpImpOrForward
→resolveMethod_locked
→resolveClassMethod
。
消息发送所有流程总结
- 消息发送的整体流程:
消息缓存查找(快速查找)
、慢速查找
、动态方法决议
、消息转发(消息的快速转发、消息慢速转发)
。 - 通过
CacheLookup
进行 消息快速查找。如果快速查找找不到imp
,则进入消息慢速查找。 - 通过
lookUpImpOrForward
进行 消息慢速查找。如果慢速也找不到imp
,则进入动态方法决议。 - 通过
resolveMethod_locked
进行 方法动态决议。如果动态方法决议没有返回imp
,则进入消息转发流程。 消息转发流程
:快速消息转发
提供一个备用消息接收者,返回值不能为self
,如果为nil
则进入慢速消息转发。慢速消息转发
需要提供消息签名,只要提供有效签名就可以解决消息发送错误问题。同时要实现forwardInvocation
函数配合处理消息。- 在慢速消息转发后系统会再进行一次
慢速消息查找
流程。这次不会再进行消息转发。 消息转发
仍然没有解决问题,会进入doesNotRecognizeSelector
,这个方法并不能处理错误,只能拿到错误信息。