前言
上面文章我们说了在cache_t找不到方法就会进行动态方法决议,会再给一次机会调用:resolveInstanceMethod:方法,如果实现这个方法就能避免闪退。具体看上篇文章OC底层原理之-objc_msgSend方法查找(中)。如果resolveInstanceMethod:还没有被实现,就要进入快速查找流程和慢速查找流程。这篇文章我们需要看下后续的过程
查找调用方法
还是上篇文章我们说了,一直在lookUpImpOrForward方法里一直打印进来的sel名字,就会发现打印的有forwardingTargetForSelector以及methodSignatureForSelector,其中forwardingTargetForSelector是快速查找,methodSignatureForSelector是慢速查找。我们还有别的办法没,下面我们介绍一种办法
通过日志获取
写下如下代码:
@interface Person : NSObject
- (void)likeFood;
@end
@implementation Person
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *person = [[Person alloc]init];
[person likeFood];
}
return 0;
}
我们看到Person的likeFood方法没有实践,我们调用了likeFood方法,我们知道肯定崩溃,那么在崩溃过程中会调用哪些方法。在OC底层原理之-objc_msgSend方法查找(中)文章我们说过在lookUpImpOrForward方法里,如果执行了done方法,就会调用log_and_fill_cache,在这个方法如下
上篇文章我们说过cache_fill是将调用的方法写入cache_t中,那上面的方法是什么呢?方法下图:
这个方法就是是否开启写入Log日志,如果开启了,就调用logMessageSend方法下图所示
这个方法就是日志写入方法,日志写入保存地方就是/tmp/msgSends文件中。这个方法进入就要保证objcMsgLogEnabled这个是true。但这个值系统默认的是false
所以改变objcMsgLogEnabled的值就需要调用instrumentObjcMessageSends方法,方法下图:
我们看到给flag传的值如果跟objcMsgLogEnabled当前值不一致,就会将flag赋值给objcMsgLogEnabled。
总结:
- 1.objcMsgLogEnabled相当于方法写入日志的开关,instrumentObjcMessageSends方法是控制开关。
- 2.instrumentObjcMessageSends在内部,外界是不知道存在这个方法的,所以我们要用extern向外界展示,extern就是告诉编译期这个方法实现我们没有,其它地方有,你到其它地方找。
到此我们通过日志获取调用方法说完了,下面我们来验证
extern void instrumentObjcMessageSends(BOOL flag);
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *person = [[Person alloc]init];
instrumentObjcMessageSends(YES);
[person likeFood];
instrumentObjcMessageSends(NO);
}
return 0;
}
我们来到tmp文件目录
可以看到现在没有我们要的文件,运行代码
此时发现生成的有文件了,我们打开这个文件:
我们发现红框内就是我们找不到方法时调用的方法。
我们发现resolveInstanceMethod一共调了4次,forwardingTargetForSelector也调了2次,methodSignatureForSelector调了2次,doesNotRecognizeSelector也掉了2次,这是一种方法,但是这种方法你需要知道崩溃地点,只有这样你才能知道什么地方打开跟关闭日志开关,那有没有别的办法呢?
反编译
我们将刚写的开关删除,在执行代码,让崩溃!打印bt,bt就是打印当前堆栈信息。补充:register read(打印寄存器地址信息)
我们发现 _ _ forwarding_prep_0 _ _ , _ forwarding _ _我们全局搜也搜不到,点击堆栈信息,我们发现这两个方法都在CoreFoundation,我们取下载这个源码下载地址,我们用VSCode打开下载的源码,搜索这两个东西,发现也没有搜索到,那该怎么办?我们打印
image list,这个指令打印所有的系统类库,我们找到/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation,获取coreFoundation,下面我们用Hopper进行反汇编
这就是将coreFoundation放入Hopper进行反汇编的结果,堆栈调用是从下往上,所以我们先搜索___forwarding_prep_0___方法
我们发现这个方法在里面调用了forwarding方法,我们点进去看到很多代码,很多看不懂,我截图截取一下关键代码,来帮助我们分析整个流程
如果我们找不到方法就会调用loc_64a2b
如果发送forwardingTargetForSelector消息,未响应,就进入loc_64ad7
判断是否是僵尸对象,如果又返回就会执行loc_64e31
这个方法forwardingTargetForSelector依然没有实现,就会执行loc_64e47
报警告,同时会调用loc_64eac
报错返回错误信息
如果forwardingTargetForSelector被执行了,就会执行下面方法
判断接收者调用forwardingTargetForSelector返回值被rax接收,如果rax==0x0,就会执行loc_64ad7就是我们上面说的方法,这说明这个快速查找,接收者不能不存在,否则有报错。当执行到loc_64ad7是说明forwardingTargetForSelector方法(快速查找)没有实现,那么继续向下走,然后下面的方法就是我们熟悉的methodSignatureForSelector:(慢速查找)。
看到rax==0x0说明这个methodSignatureForSelector:方法签名没有实现,就会进入loc_64e47就是上面介绍的方法。如果方法签名有了,就会走下一步
这个方法有了,就要去调用去实现。继续往下走
这个方法是对签名对底层拿出来,对target等的包装,然后拿到类信息进行_forwardStackInvocation:调用,如果上面方法没有实现,就会调用loc_64c89
然后就会直接响应forwardInvocation:(这就是慢速查找流程)如果为空就会调用loc_64f32,也是会保存,如果实现了,就会继续下一步
会对Invocation进行处理,然后直接调用forwardInvocation:,这就是会什么外界会直接调用了forwardInvocation这个方法的原因。
到此我们通过反汇编明白了forwardingTargetForSelector以及methodSignatureForSelector大致调用过程。我们知道什么情况下会崩溃,那么下面我们进行代码调试。
代码实践,防止崩溃
验证快速查找流程
先写如下代码
@implementation Person
- (id)forwardingTargetForSelector:(SEL)aSelector {
NSLog(@"-->%@", NSStringFromSelector(aSelector));
return [super forwardingTargetForSelector:aSelector];
}
@end
likeFood方法未实现,我们运行发现崩了,但是打印了likeFood说明走了forwardingTargetForSelector。
forwardingTargetForSelector(这个方法的官方解释意思:这个方法给未实现方法找一个接收者),下面我们创建Man类,如下代码:
@interface Man : NSObject
- (void)likeFood;
@end
@implementation Man
- (void)likeFood {
NSLog(@"%s",__func__);
}
@end
@implementation Person
- (id)forwardingTargetForSelector:(SEL)aSelector {
NSLog(@"-->%@", NSStringFromSelector(aSelector));
return [Man alloc];
}
@end
运行代码:
发现这样就不崩溃了
此时我们Man的方法需要和Person方法一样,因为forwardingTargetForSelector是告诉编译器去Man找找这个方法实现.
验证慢速查找
上面是验了快速查找forwardingTargetForSelector,下面我们验证methodSignatureForSelector。准备如下代码
@implementation Person
- (id)forwardingTargetForSelector:(SEL)aSelector {
NSLog(@"-->%@", NSStringFromSelector(aSelector));
return [super forwardingTargetForSelector:aSelector];
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
NSLog(@"%s-%@", __func__, NSStringFromSelector(aSelector));
return [super methodSignatureForSelector:aSelector];
}
@end
运行代码:
发现methodSignatureForSelector方法在forwardingTargetForSelector没有被实现的时候执行了(验证了我们在反汇编中分析的情况)。
下面对代码进行如下改动
@implementation Person
- (id)forwardingTargetForSelector:(SEL)aSelector {
NSLog(@"-->%@", NSStringFromSelector(aSelector));
return [super forwardingTargetForSelector:aSelector];
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
NSLog(@"--%s-%@", __func__, NSStringFromSelector(aSelector));
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
NSLog(@"--=%s--%@", __func__, anInvocation);
}
@end
我们再运行
发现不会崩溃了,我们看下NSInvocation内部都有什么吧
发现含有sel以及target
我们打印下NSInvocation发现里面
包含了接收者信息,包含调用的方法,这个就是慢速查找,它将这个方法的所有信息进行打包,然后扔出去,谁能处理谁去处理,它就不管了。
我们在改造下代码
@implementation Person
- (id)forwardingTargetForSelector:(SEL)aSelector {
NSLog(@"-->%@", NSStringFromSelector(aSelector));
return [super forwardingTargetForSelector:aSelector];
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
NSLog(@"--%s-%@", __func__, NSStringFromSelector(aSelector));
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
NSLog(@"--=%s--%@", __func__, anInvocation);
anInvocation.target = [Man alloc];
anInvocation.selector = NSSelectorFromString(@"likeRun");
[anInvocation invoke];
}
@end
再运行,打印如下:
说明慢速查找就相当与把方法所有信息打包,扔出去,谁拿到了给它指定接收者或者方法就行了。
总结
上面我们讲了快速查找和慢速查找流程,其中如何知道调用什么方法,我们用了打印日志,反汇编去获得,获取过程的方法很重要。我们实验了如何通过快速和慢速查找方法去防止崩溃。最后我们再补一张流程图
补充 - 类方法动态方法决议
之前文章我们说了对象方法的动态方法决议,下面我们分下类方法动态方法决议。 我们写如下代码
@interface Person : NSObject
+ (void)likeColor;
@end
@implementation Person
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
[Person likeColor];
}
return 0;
}
我们Person的类方法没有实现,并调用了。打断点运行,前面和对象方法一直
我们找到这个方法
如果当前是类方法进来,那么inst就是当前接收者的类,再此时Person,此时的cls就是元类,就是Person的元类
cls->isMetaClass()意思是cls是否是元类,如果不是就进去,是就到下面,此时我们的cls是原来所以会走下面
会调用resolveClassMethod这个方法,我们查看了resolveClassMethod方法发现跟对象方法类似。
根据对象方法指导:
如果我们实现了resolveClassMethod也能防止崩溃。试一下,在person.m准备如下代码
@implementation Person
+ (BOOL)resolveClassMethod:(SEL)sel {
NSLog(@"%@ 来了",NSStringFromSelector(sel));
if (sel == @selector(likeColor)) {
IMP imp = class_getMethodImplementation(objc_getMetaClass("Person"), @selector(workTime));
Method likeMMethod = class_getInstanceMethod(objc_getMetaClass("Person"), @selector(workTime));
const char *type = method_getTypeEncoding(likeMMethod);
return class_addMethod(objc_getMetaClass("Person"), sel, imp, type);
}
return [super resolveClassMethod:sel];
}
+ (NSString *)workTime {
NSLog(@"%s", __func__);
return @"09:00";
}
@end
运行一下
发现并没有崩溃报错,说明解释是正确的的。这个方法是要在元类方法中查找
接上面方法调用了resolveClassMethod,为什么还要调用resolveInstanceMethod。
解析:这个方法如果是类方法调用,应该是给类方法一次机会,重新查询,去拯救,拯救地方应该是元类。类中的方法是以对象方法的形式存在,元类的方法是以类对象方法形式存在,我们调用resolveInstanceMethod,此时我们再调用lookUpImpOrForward是在元类以及元类父类中找,但是resolveInstanceMethod是对象方法,只有在NSObject元类的父类中才可能找到resolveInstanceMethod方法。
上面的分析告诉我们在NSObject中实现resolveInstanceMethod也可以防止类方法的崩溃,试验下。我们创建NSObject分类,之所以创建分类是因为在NSObject中已经实现了resolveInstanceMethod方法,创建分类为了重写resolveInstanceMethod。
@implementation NSObject (Test)
+ (BOOL)resolveInstanceMethod:(SEL)sel{
NSLog(@"%@ 来了",NSStringFromSelector(sel));
if (sel == @selector(likeFood)) {
NSLog(@"%@ 来了",NSStringFromSelector(sel));
IMP imp = class_getMethodImplementation(self, @selector(sleepTime));
Method sayMMethod = class_getInstanceMethod(self, @selector(sleepTime));
const char *type = method_getTypeEncoding(sayMMethod);
return class_addMethod(self, sel, imp, type);
}else if (sel == @selector(likeColor)) {
IMP imp = class_getMethodImplementation(objc_getMetaClass("Person"), @selector(workTime));
Method sayMMethod = class_getInstanceMethod(objc_getMetaClass("Person"), @selector(workTime));
const char *type = method_getTypeEncoding(sayMMethod);
return class_addMethod(objc_getMetaClass("Person"), sel, imp, type);
}
return YES;
}
@end
运行,完美解决但是这种写法可以无切入的拦截没有实现方法,可以进行优化
方法起名比如自己创建的方法为pr开头,首页为pr_Home_method,我的为pr_Me_methods,消息为pr_IM_methods,我们在此处拦截未实现的方法,如果发现是pr开头就拦截,如果不是就放过,如果是Home就说明是首页方法,以此类推,不同地方的方法进行不同的操作,但是这种不好,最好是在最下层慢速查找中做处理。
写到最后
后面会补充lookUpImpOrForward具体调用过程,lookUpImpOrForward这个方法是消息发送的重要方法。实际断点发现这个方法会在找不到imp时多次调用,传过来的sel不尽相同,后续研究清楚了再补充,resolveInstanceMethod调用多次,也跟这个有一定关系
补充内容
resolveInstanceMethod调用2次,lookUpImpOrForward调用多次问题,我们看下图
红框1就是去寻找resolveInstanceMethod这个方法,肯定能找到,因为NSObject里实现了,此时会进入红框2,因为找到这个方法了,那就去调用resolveInstanceMethod,看看这个方法是不是处理了sel。这个地方就是第一次调用resolveInstanceMethod,下面调用红框3会直接返回imp地址为nil,第二次调用是在methodSignatureForSelector之后,我们在下面打断点
运行,每次到断点打印下堆栈。下如下内容,证明我们说的内容