-
准备工作
- objc4-781可编译源码 github.com/Bore-TuDou/…
- 创建一个TDPerson 里面有一个没有实现的实例方法sayNB和一个没有实现的类方法sayHappy,然后在创建一个TDStudent继承TDPerson,然后TDStudent中有一个没有实现的实例方法say666和一个没有实现的类方法say888
-
动态方法决议 & 消息转发执行时机
上面一篇文章有提到过慢速查找之后还没有找到辉县判断是否有执行过
动态方法决议
如果如果没有执行则进入到动态方法决议
,如果已经执行了一次动态方法决议
,则进入到消息转发
,如果动态方法决议
未被实现,同时消息转发
也未被处理最后就会报错,所以在程序崩溃之前苹果还是给了两次补救的机会:- 动态方法决议
- 消息转发
-
动态方法决议
- 源码分析
static NEVER_INLINE IMP resolveMethod_locked(id inst, SEL sel, Class cls, int behavior) { runtimeLock.assertLocked(); ASSERT(cls->isRealized()); runtimeLock.unlock(); if (! cls->isMetaClass()) { //如果当前传入的cls是一个类的话则调用对象的解析方法 // try [cls resolveInstanceMethod:sel] resolveInstanceMethod(inst, sel, cls); } else { //如果当前传入的cls是一个元类的话则调用类的解析方法 // try [nonMetaClass resolveClassMethod:sel] // and [cls resolveInstanceMethod:sel] resolveClassMethod(inst, sel, cls); if (!lookUpImpOrNil(inst, sel, cls)) { //如果没有找到或者为空,在元类的对象方法解析方法中查找 //应为类其实就是元类的实例对象所以类方法其实就是元类的实例方法,所以 //还需要查询元类的对象方法的动态方法决议 resolveInstanceMethod(inst, sel, cls); } } // chances are that calling the resolver have populated the cache // so attempt using it //再次慢速查找一次方法 return lookUpImpOrForward(inst, sel, cls, behavior | LOOKUP_CACHE); }
-
流程分析
-
关于实例方法的处理
- 源码解析
作为一个开发者,有一个学习的氛围跟一个交流圈子特别重要,这里有一个iOS交流群:[891488181]不管你是大牛还是小白都欢迎入驻 ,分享BAT,阿里面试题、面试经验,讨论技术, 大家一起交流学习成长!
```
static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{
runtimeLock.assertUnlocked();
ASSERT(cls->isRealized());
SEL resolve_sel = @selector(resolveInstanceMethod:);
//一个简单的容错处理如果没有查找到resolveInstanceMethod方法的实现直接return
//正常情况下resolveInstanceMethod都有实现的
if (!lookUpImpOrNil(cls, resolve_sel, cls->ISA())) {
// Resolver not implemented.
return;
}
BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
//发送resolve_sel消息
bool resolved = msg(cls, resolve_sel, sel);
// Cache the result (good or bad) so the resolver doesn't fire next time.
// +resolveInstanceMethod adds to self a.k.a. cls
//再次查找
IMP imp = lookUpImpOrNil(inst, sel, cls);
if (resolved && PrintResolving) {
if (imp) {
_objc_inform("RESOLVE: method %c[%s %s] "
"dynamically resolved to %p",
cls->isMetaClass() ? '+' : '-',
cls->nameForLogging(), sel_getName(sel), imp);
}
else {
// Method resolver didn't add anything?
_objc_inform("RESOLVE: +[%s resolveInstanceMethod:%s] returned YES"
", but no new implementation of %c[%s %s] was found",
cls->nameForLogging(), sel_getName(sel),
cls->isMetaClass() ? '+' : '-',
cls->nameForLogging(), sel_getName(sel));
}
}
}
```
* 源码解读从上面的源码我们可以得到的消息是我们还有一个补救的机会就是重写`resolveInstanceMethod`方法,然后程序会再次查找方法,当然重写了`resolveInstanceMethod`方法但是里面没有相关的实现最后还是会崩溃,如果想要防止崩溃可以再`resolveInstanceMethod`方法中将`say666`方法的实现指向一个实现了的方法如下代码,就是讲`say666`的实现指向了`sayGooyBye`,然后在加入到方法列表,然后在查找根据`say666`就能查到`sayGoodBye`的实现
```
+(BOOL)resolveInstanceMethod:(SEL)sel{
if (sel == @selector(say666)) {
NSLog(@"%@ 来了", NSStringFromSelector(sel));
//获取sayGoodBye方法的imp
IMP imp = class_getMethodImplementation(self, @selector(sayGoodBye));
//获取sayGoodBye的实例方法
Method sayMethod = class_getInstanceMethod(self, @selector(sayGoodBye));
//获取sayGoodBye的方法签名
const char *type = method_getTypeEncoding(sayMethod);
//将sel的实现指向sayGoodBye 并加入到方法列表
return class_addMethod(self, sel, imp, type);
}
return [super resolveInstanceMethod:sel];
}
```
此时我们在看控制台的打印信息![](https://upload-images.jianshu.io/upload_images/25061170-e50d5da5957de404.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
* **关于类方法的处理**
* 源码解析![](https://upload-images.jianshu.io/upload_images/25061170-b393287d404e9cc5.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
同样的还是这一块代码,但是会走else里面的代码
* 源码解读
从上述代码可以看出,如果类方法找不到,这里会有两个地方可以补救(实例方法只有一个地方`resolveInstanceMethod`)一个是重写`resolveClassMethod`方法还一个是在元类中重写`resolveInstanceMethod`方法,代码如下:![](https://upload-images.jianshu.io/upload_images/25061170-961aba3f7e7e041b.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
* **关于崩溃处理的优化方法**
* 明确`resolveInstanceMethod`方法和`resolveClassMethod`方法的查找路径
实例方法:类->父类->根类->nil
类方法: 元类->根元类->根类->nil
* 代码实现及思路
从上面可以得知实例方法和类方法查找路径上都会经过根类,且根类是最后一环,所以我们只要创建一个根类的分类然后重写`resolveInstanceMethod`方法和`resolveClassMethod`方法就能做到拦截所有崩溃方法,如下代码实现:
```
//
// NSObject+cash.m
// KCObjc
//
// Created by huahua on 2020/11/15.
//
#import "NSObject+cash.h"
#import <objc/runtime.h>
@implementation NSObject (cash)
+(BOOL)resolveClassMethod:(SEL)sel{
NSLog(@"%@ 来了", NSStringFromSelector(sel));
if (sel == @selector(say888)) {
// NSLog(@"%@ 来了", NSStringFromSelector(sel));
//获取sayGoodBye方法的imp
IMP imp = class_getMethodImplementation(self, @selector(sayGoodBye));
//获取sayGoodBye的实例方法
Method sayMethod = class_getInstanceMethod(self, @selector(sayGoodBye));
//获取sayGoodBye的方法签名
const char *type = method_getTypeEncoding(sayMethod);
//将sel的实现指向sayGoodBye 并加入到方法列表
return class_addMethod(objc_getMetaClass("TDStudent"), sel, imp, type);
}
return NO;
}
+(BOOL)resolveInstanceMethod:(SEL)sel{
NSLog(@"%@ 来了", NSStringFromSelector(sel));
if (sel == @selector(say888)) {
// NSLog(@"%@ 来了", NSStringFromSelector(sel));
//获取sayGoodBye方法的imp
//因为类方法是元类的实例方法所以这里要传入元类
IMP imp = class_getMethodImplementation(objc_getMetaClass("TDStudent"), @selector(sayhai));
//获取sayGoodBye的实例方法
Method sayMethod = class_getInstanceMethod(objc_getMetaClass("TDStudent"), @selector(sayhai));
//获取sayGoodBye的方法签名
const char *type = method_getTypeEncoding(sayMethod);
//将sel的实现指向sayGoodBye 并加入到方法列表
return class_addMethod(objc_getMetaClass("TDStudent"), sel, imp, type);
}else if(sel == @selector(say666)){
IMP imp = class_getMethodImplementation(self, @selector(sayGoodBye));
//获取sayGoodBye的实例方法
Method sayMethod = class_getInstanceMethod(self, @selector(sayGoodBye));
//获取sayGoodBye的方法签名
const char *type = method_getTypeEncoding(sayMethod);
//将sel的实现指向sayGoodBye 并加入到方法列表
return class_addMethod(self, sel, imp, type);
}
return NO;
}
@end
```
-
消息转发
单纯的阅读源码你会发现源码上显示的是如果动态决议已经执行过一次了,就会直接报错,但是其实苹果还给了一个消息换发的机会
-
查找消息转发的流程方法
-
通过instrumentObjcMessageSends方式打印发送消息的日志
首先看源码我们知道如果执行过动态方法决议之后回来到log_and_fill_cache
方法点进
log_and_fill_cache
查看发现
log_and_fill_cache
方法的作用就是方法缓存和是否调用logMessageSend
方法再看logMessageSend
方法源码实现从这一步可以确认
log_and_fill_cache
方法就是一个方法缓存和判断是否需要打印日志的一个方法,在打印日志的条件objcMsgLogEnabled && implementer
implementer是传入的类(类或者元类)所以只要objcMsgLogEnabled为true就能打印日志,再看objcMsgLogEnabled
发现是在
instrumentObjcMessageSends
方法中赋值的,所以在main函数中调用一下instrumentObjcMessageSends
方法,将objcMsgLogEnabled
赋值成true
(这里注意需要先声明instrumentObjcMessageSends
方法)如下代码:,再去
/tmp
文件夹中找到对应的日志文件
-
通过hopper/IDA反编译
首先查看出错的堆栈信息:然后通过
image list
指令查找整个镜像文件的执行路径然后搜索CoreFountion
,查找到其路径并找到对应的可执行文件
拖进hopper然后左边栏搜索___forwarding___如下[![](https://img-blog.csdnimg.cn/img_convert/9fa7c3d9a920fb5b9da4a03d2c99005e.png) 看伪代码发现首先判断`forwardingTargetForSelector`方法有没有实现,如果没有实现就跳到`loc_64a67`(慢速转发)有实现但是返回值是nil的时候直接报错![](https://img-blog.csdnimg.cn/img_convert/5cc8ca153fff6bd8dcdbdaf55f92c3d5.png) 然后再判断`methodSignatureForSelector`方法 有没有实现,没有实现直接报错了![](https://img-blog.csdnimg.cn/img_convert/c2337bd25c9035267537b90fa378c39a.png)
同样的如果返回值是nil也是会直接报错,如果不为空的话则在`forwardInvocation`方法中对`invocation`进行处理![](https://img-blog.csdnimg.cn/img_convert/48e19efc34f2362c632001d803788b47.png)
-
-
快速转发
forwardingTargetForSelector
- 崩溃处理 实现
forwardingTargetForSelector
方法并返回对应的消息接受者(消息接受者中要有同名方法的实现)如下代码:
-
慢速转发
-
methodSignatureForSelector
-
forwardInvocation
-
崩溃处理 首先实现
methodSignatureForSelector
返回方法签名,然后再实现forwardInvocation
方法也可以处理invocation,修改target然后触发事务
-
-
消息转发流程图
-
作者:Potato_土豆
链接:https://juejin.im/post/6895755250272534541