前言
我们在运行工程时,如果有工程里面的方法没实现,或者找不到,工程会直接报错,导致程序奔溃。这样对于开发者就十分不友好,同时也是十分影响用户体验。所以苹果系统就给与了一次拯救代码的机会,就是通过消息动态决议来找到一个背锅侠来进行承担。这样就使得程序不再奔溃,皆大欢喜。但是,如果这个背锅侠也不能将这个锅给背下的话,那又该如何动态处理这个奔溃的问题了?在苹果系统中,还有没有相关的其他方式的容错处理了?答案当然是有的,就是----消息转发。
资源准备
- objc源码:多个版本的objc源码
- CF源码:多个版本的CF源码
- 冰🍺
进入主题
根据前几篇文章,当调用某方法时,经过对缓存方法查找的分析,以及上篇的消息动态决议分析,可以知道:总体上,查找方法是通过sel去查找其对应的imp。先是快速查找,通过objc_msgSend在cache中查询,如果找不到,然后再经过慢速查找,通过lookUpImpOrForward方法,在methodlist中查找。找到之后,直接返回对应的imp。
如果找不到的话,此时返回的imp就是forward_imp。系统也不是马上就报错,而是给了一次拯救的机会,就是消息动态决议。通过消息动态决议,再重新给赋值一个新的imp。
如果也没有进行消息动态决议,那么imp就直接返回nil了。为了不让系统奔溃,那么就找和这个方法相同或者相类似的方法来执行,就能防止系统奔溃,这就叫消息转发的方式。
下面就探索下消息转发的流程。
通过instrumentObjcMessageSends获取日志
为什么说instrumentObjcMessageSends方法能获取日志了?
根据objc源码,在慢查找过程中,当在methodlist中找到方法时,会把该方法通过log_and_fill_cache插入缓存中:
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
。。。。
done:
if (fastpath((behavior & LOOKUP_NOCACHE) == 0)) {
#if CONFIG_USE_PREOPT_CACHES
while (cls->cache.isConstantOptimizedCache(/* strict */true)) {
cls = cls->cache.preoptFallbackClass();
}
#endif
log_and_fill_cache(cls, imp, sel, inst, curClass);
}
。。。。
在log_and_fill_cache方法的实现里面,有个判断,当达到条件的时候,会方法执行的日志。其判断条件中,implementer是必然存在的,所以objcMsgLogEnabled的值,才是判断的关键。
下图是logMessageSend方法的实现,其中就有打印日志的文件路径:
接下来,就需要搞清楚objcMsgLogEnabled的赋值情况就行。从其定义上,就能知道是一个能够被外部方法的变量。
定义:extern bool objcMsgLogEnabled;
赋值情况:
根据赋值情况,是通过
instrumentObjcMessageSends方法所传进来的BOOL值来进行赋值的,那么是不是就意味着,我们可以把instrumentObjcMessageSends方法写成供外部调用的方法,传进BOOl值,就可以来控制日志的打印了。
打印的日志,通过前往文件夹(commamd + shift + G),输入/tmp/msgSends-%d路径,就能获取到日志文件。
案例分析
创建LGPerson类,然后再声明sayHello实例方法,方法不实现。然后通过instrumentObjcMessageSends方法,来获取sayHello执行报错的日志。根据日志来进行下一步分析。
extern void instrumentObjcMessageSends(BOOL flag);
int main(int argc, const char * argv[]) {
@autoreleasepool {
LGPerson *person = [LGPerson alloc];
instrumentObjcMessageSends(YES);
[person sayHello];
instrumentObjcMessageSends(NO);
NSLog(@"Hello, World!");
}
return 0;
}
运行工程后,直接报错
去查看日志,点开文件夹,然后前往文件夹,输入上述路径即可:
打开文件,查看到日志的详情,
forwardingTargetForSelector的重定向
通过日志,我们就知道,在消息动态决议一文中,resolveInstanceMethod方法是发送消息处理imp的。但是forwardingTargetForSelector方法就比较陌生,虽然不知道这个方法的用法,但是可以快捷键(command + shift + 0),就进入到官方文档中,全局搜索forwardingTargetForSelector方法,就能获取该方法的详情了。
通过文档的描述,知道这方法是用作重定向的。比如在本类中的某个不能实现某个方法,那么就可以重定向另外一个局部的对象。
那么就可以把这个方法在LGPerson类里面实现,此时再创建一个LGStudent类,再在这个类里面声明sayHello方法,再对其进行实现。如下面源码所示,在LGPerson类里面,在forwardingTargetForSelector方法重定向到LGStudent类的方法上:
#import "LGPerson.h"
#import "LGStudent.h"
@implementation LGPerson
//快速转发
- (id)forwardingTargetForSelector:(SEL)aSelector {
NSLog(@"%s - %@",__func__,NSStringFromSelector(aSelector));
return [LGStudent alloc];
}
运行工程,根据打印结果可以看出,是调用的LGStudent类的sayHello方法:
- 也就是说,在
LGPerson类里面调用sayHello方法没找到,那么就重定向到LGStudent类的sayHello方法上,让这个方法来行驶LGPerson类想要的功能。
methodSignatureForSelector使用
到了这里,就有童鞋问了,为啥不是LGPerson类自己来实现这个功能了,反而还要绕这么一圈?就是说LGStudent类罢工不干。那么就得LGPerson类自己来了。从日志上看,接下来就是methodSignatureForSelector方法的实现了。
快速转发forwardingTargetForSelector,没有转发出去,那接下来,就只能是自己的慢速转发了methodSignatureForSelector。同样的快捷键(command + shift + 0),就进入到官方文档中,然后再全局搜索。
从官方文档上知道,这个方法是进行方法签名的,还要搭配forwardInvocation方法使用。在LGPerson类里面实现:
#import "LGPerson.h"
#import "LGStudent.h"
@implementation LGPerson
//慢速转发
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
NSLog(@"%s - %@",__func__,NSStringFromSelector(aSelector));
//签名操作
if (aSelector == @selector(sayHello)) {
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return [super methodSignatureForSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
NSLog(@"%@ -- %@",anInvocation.target,NSStringFromSelector(anInvocation.selector));
}
@end
运行看结果:
-
在系统里面,所有的方法或者函数,都统称为事务。在
methodSignatureForSelector中,只是把LGPerson类的sayHello方法保存在anInvocation里面,相当于处理休眠状态。当要调用时,通过forwardInvocation方法,来实现sayHello的功能。 -
相比于
forwardingTargetForSelector,methodSignatureForSelector没有实现sayHello方法,而是只保留的sayHello方法的职能。
与此同时,forwardInvocation方法还能指定事务的子处理者,可以是本身,也能是其他类,如下代码:
- (void)forwardInvocation:(NSInvocation *)anInvocation{
LGStudent *s = [LGStudent alloc];
if ([self respondsToSelector:anInvocation.selector]) {//自己处理
[anInvocation invoke];
}else if ([s respondsToSelector:anInvocation.selector]){//指定LGStudent类处理
[anInvocation invokeWithTarget:s];
}else{
NSLog(@"%s - %@",__func__,NSStringFromSelector(anInvocation.selector));
}
}
看到这里的童鞋,可能就有疑问了,我要是知道这些个方法,以及用途,也能进行处理,如果不知道了?那该怎么办?
没办法,只能使用终极大招-----汇编
hoper反汇编CoreFoundation
那么现在,把刚刚使用的工具方法都给注释掉
#import <Foundation/Foundation.h>
#import "LGPerson.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
LGPerson *person = [LGPerson alloc];
[person sayHello];
}
return 0;
}
根据打印结果,使用bt汇编指令,查看堆栈的内容。
如上图所示,从
main.m开始,想要发生doesNotRecognizeSelector(①号位置),必然要经过③和②号位置,那么这两处位置,就是我们寻找答案的关键所在。而且都存在于CoreFoundation库里面。
因为我们的程序执行之后,就回变成可执行文件。然后我们把这个可执行文件还原回来,就是反汇编。
接下来,就用工具Hopper Disassebler v4,打开CoreFoundation的machO文件,获取里面的汇编代码,再索引forwarding,查看伪代码,得到如下图:
- 根据伪代码就知道,当查找方法时,如果在该类执行了
forwardingTargetForSelector方法,就会通过_objc_msgSend再次发送对应消息;如果没有执行就进入goto loc_64a67,走methodSignatureForSelector方法; - 执行
methodSignatureForSelector方法后,再经过不断的地址平移,到达forwardInvocation方法
小结
-
慢速查找过程中,在本类中,先进行
消息动态决议,如果找到imp,就处理消息;如果还是没有找到imp,就进行消息快速转发; -
消息快速转发
forwardingTargetForSelector,如果找到imp,就处理消息;如果没有找到imp,就进行消息慢速转发; -
消息慢速转发
methodSignatureForSelector,如果做了签名操作,就返回签名,再执行forwardInvocation方法,再处理消息;如果没有签名操作,那么就直接报错了。