前言
我们在运行工程时,如果有工程里面的方法没实现,或者找不到,工程会直接报错,导致程序奔溃。这样对于开发者就十分不友好,同时也是十分影响用户体验。所以苹果系统就给与了一次拯救代码的机会,就是通过消息动态决议来找到一个背锅侠来进行承担。这样就使得程序不再奔溃,皆大欢喜。但是,如果这个背锅侠也不能将这个锅给背下的话,那又该如何动态处理这个奔溃的问题了?在苹果系统中,还有没有相关的其他方式的容错处理了?答案当然是有的,就是—-消息转发
。
资源准备
- 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
类想要的功能。
forwardingTargetForSelector
使用
到了这里,就有童鞋问了,为啥不是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
方法,再处理消息;如果没有签名操作,那么就直接报错了。