当我们发送的消息,在消息接受者以及它的整个继承链缓存(Cache)和方法列表(methodList)中都找不到时,系统给我们留了崩溃前的最后一次挽救机会。
本节将探索方法找不到时最后的挽救机会 - 消息转发机制。
1. 前期准备
2. 动态方法决议
3. 消息转发
4. 异常消息处理流程图
1.前期准备
打开objc4源文件,在main.m中加入测试代码:
@interface HTPerson : NSObject
- (void)sayHello;
+ (void)say666;
@end
@implementation HTPerson
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
HTPerson * person = [HTPerson alloc];
[person sayHello];
}
return 0;
}
sayHello方法不实现。模拟方法找不到的场景。
- 熟悉上两节快速查找和慢速查找内容,知道
消息在汇编层cache中高速查找,找不到imp后,会进入C/C++混编层,调用lookUpImpOrForward函数。在查询cls接收者和cls的整个继承链对象的cache和methodlist,都找不到对应的imp时。我们会进入动态方法决议 resolveMethod_locked。
if (slowpath(behavior & LOOKUP_RESOLVER)) {
behavior ^= LOOKUP_RESOLVER; // behavior取反后,下一次进入判断时,if条件就不成立了。 确保动态决议只执行一次
return resolveMethod_locked(inst, sel, cls, behavior);
}
补充
lookUpImpOrForward的入参理解IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
inst:真实持有sel方法的类(对象方法存放在类中,类方法存放在元类中)
如对象方法: [person sayHello],inst是HTPerson。
类方法: [HTPerson say666],inst也是HTPerson元类
sel:方法名
cls:方法接受者的所属类
如对象方法: [person sayHello],cls是HTPerson
类方法: [HTPerson say666],cls也是HTPerson
behavior:行为参数, 影响进入动态决议等判断条件。
2. 动态方法决议
- 进入
resolveMethod_locked
static NEVER_INLINE IMP
resolveMethod_locked(id inst, SEL sel, Class cls, int behavior)
{
runtimeLock.assertLocked();
ASSERT(cls->isRealized());
runtimeLock.unlock();
// 对象方法
if (! cls->isMetaClass()) {
resolveInstanceMethod(inst, sel, cls);
}
// 类方法
else {
resolveClassMethod(inst, sel, cls);
if (!lookUpImpOrNil(inst, sel, cls)) {
resolveInstanceMethod(inst, sel, cls);
}
}
// 重新查询一次
return lookUpImpOrForward(inst, sel, cls, behavior | LOOKUP_CACHE);
}
判断是cls是元类还是本类:
如果
不是元类,就是对象方法,直接调用resolveInstanceMethod-
否则,就是
类方法,先调用resolveClassMethod,再调用lookUpImpOrNil检查是否找到imp,没找到的话再调用resolveInstanceMethod。意思是,如果
类方法中沿着元类继承链找到NSObject元类了都没找到,再找就是NSObjet本类了,NSObjet本类存的是对象方法,所以要用resolveInstanceMethod。
最后再调用lookUpImpOrForward重新查询一次。
分析resolveInstanceMethod:
static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{
runtimeLock.assertUnlocked();
ASSERT(cls->isRealized());
SEL resolve_sel = @selector(resolveInstanceMethod:);
// 1. 查找类对象的元类中是否有`resolveInstanceMethod`的imp。
// (根元类中默认实现了`resolveInstanceMethod`方法,所以永远不会return)
if (!lookUpImpOrNil(cls, resolve_sel, cls->ISA())) {
return;
}
// 2. 调用一次`resolveInstanceMethod`函数
BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
bool resolved = msg(cls, resolve_sel, sel);
// 3. 再搜索一次sel的imp
//(如果在上面resolveInstanceMethod函数实现了sel,我们就拿到imp了,成功将sel和imp写入cls的缓存中)
IMP imp = lookUpImpOrNil(inst, sel, cls);
// 做Log记录
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));
}
}
}
了解下
lookUpImpOrNil:static inline IMP lookUpImpOrNil(id obj, SEL sel, Class cls, int behavior = 0) { // behavior = 0, LOOKUP_CACHE = 4, LOOKUP_NIL = 8 return lookUpImpOrForward(obj, sel, cls, behavior | LOOKUP_CACHE | LOOKUP_NIL); }内部继续调用了
lookUpImpOrForward函数,不同的是,behavior变成了 0 | 4 | 8 = 12。 >这决定了进入lookUpImpOrForward后:
fastpath(behavior & LOOKUP_CACHE)= 12 & 4 = 4,条件成立,会优先cache_getImp读取一次缓存slowpath(behavior & LOOKUP_RESOLVER)= 12 & 2 = 0,不成立,不会进入resolveMethod_locked动态方法决议。
lookUpImpOrNil中的lookUpImpOrForward会循环遍历cls继承链的所有类的cache和methodList来寻找imp
所以resolveInstanceMethod函数,就是系统给开发者的一次机会。
- 你可以在合适的地方加上
resolveInstanceMethod函数。在函数内部把sel实现
- 你可以在合适的地方加上
- 系统会发送消息,调用一次
resolveInstanceMethod函数
- 系统会发送消息,调用一次
- 再次循环查找
sel,如果在上面resolveInstanceMethod函数实现了sel,就可以拿到imp,并将sel和imp写入cls的缓存中。
- 再次循环查找
系统大哥用大白话跟你说:老弟,我检查到你的
方法没有实现,我现在给你一次机会,你识趣的话,就在我调用resolveInstanceMethod函数之前,在这函数内部把你的sel实现了。等到我调用完后,你还是没实现的话,我就崩了你。
案例:
- 授人以渔
- 分析
imp实现位置。
- 如:
[person sayHello]是对象方法,对象方法是存放在本类中。[HTPerson say666]是类方法,类方法是存放在元类中。
imp写哪里?
- NSObject是所有类的父类,NSObject中有
resolveInstanceMethod类方法。所以我们可以在继承链的某个合适的类中重写resolveInstanceMethod方法。在这个方法内部,实现imp。
- 授人以鱼
#import <Foundation/Foundation.h>
#import <objc/message.h>
@interface BaseObject: NSObject
@end
@implementation BaseObject
- (void)handleErrorImp {
NSLog(@"我帮你挡住崩溃了,上报数据给后台还是咋操作,你自己想");
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(sayHello)) {
NSLog(@"💣💣💣💣 %@ 没实现!!💣💣💣💣", NSStringFromSelector(sel));
IMP imp = class_getMethodImplementation(self, @selector(handleErrorImp));
Method method = class_getInstanceMethod(self, @selector(handleErrorImp));
const char * type = method_getTypeEncoding(method);
// 将imp加入当前类。
return class_addMethod(self, sel, imp, type);
}
return [super resolveInstanceMethod: sel];
}
@end
@interface HTPerson : BaseObject
- (void)sayHello;
@end
@implementation HTPerson
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
HTPerson * person = [HTPerson alloc];
[person sayHello];
}
return 0;
}
-
HTPerson继承自我们自定义的基类BaseObject,HTPerson中的sayHello没实现,我们在基类中加入拦截。将临时imp和sel写入基类函数列表中。成功防止了崩溃。
具体操作时,我们并不知道sel是哪个,按理说进入resolveInstanceMethod的所有sel都是找不到imp的。我们可以统一拦截进行数据上报,然后做防崩溃处理。
(比如个人中心某个深层页面的函数未实现,我们直接统一返回个人中心,防止崩溃,同时将这次找不到的记录上传给后台,让相应的程序员哥哥下个版本修复它)但
实际开发中,你不确定是否有人在继承链上游就使用了resolveInstanceMethod黑魔法。这样你的resolveInstanceMethod就被重写😂 毕竟牛逼的人很多。
这是打印结果:

-
厉害的人很多,大家都知道用
resolveInstanceMethod。那有没有更厉害一点的呢?
------------------------------ 来了,老弟 👇 ------------------------------
3. 消息转发
当我们在动态方法决议中不做任何处理,在崩溃前,系统还有2个隐藏的挽救点。快速转发forwardingTargetForSelector 和慢速转发methodSignatureForSelector
在正式介绍这2个方法前,我们应该知道如何发现他们的。
方法1: 日志查看
在lookUpImpOrForward中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
cache_fill(cls, sel, imp, receiver);
}
我们看到在cache_fill写入操作前,会检查SUPPORT_MESSAGE_LOGGING是否支持消息记录。
进入logMessageSend函数内部,发现日志存放路径在/tmp文件夹:

我们打开任意一个文件夹,按Command + Shift + G,输入/tmp文件夹查看。发现并没有msgSends名字的文件。
- 因为
系统默认是关闭日志功能,objcMsgLogEnabled默认为false。

文件内搜索objcMsgLogEnabled,发现赋值操作是在instrumentObjcMessageSends中。

所以我们在崩溃前,手动调用instrumentObjcMessageSends进行日志的开启和关闭:
- 测试代码:
注意,这次不是在源码环境。而是在任意一个正常项目中进行测试。
// extern: 声明当前变量或函数在别的文件中定义了。不要报错。
extern void instrumentObjcMessageSends(BOOL flag);
@interface HTPerson : NSObject
- (void)sayHello;
@end
@implementation HTPerson
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
HTPerson * person = [HTPerson alloc];
instrumentObjcMessageSends(YES); // 打开记录
[person sayHello];
instrumentObjcMessageSends(NO); // 关闭记录
}
return 0;
}
运行程序,crash后,在/tmp文件夹中看不到msgSends-92567文件

打开msgSends-92567文件,发现最上面的日志记录为:
+ HTPerson NSObject resolveInstanceMethod:
+ HTPerson NSObject resolveInstanceMethod:
- HTPerson NSObject forwardingTargetForSelector:
- HTPerson NSObject forwardingTargetForSelector:
- HTPerson NSObject methodSignatureForSelector:
- HTPerson NSObject methodSignatureForSelector:
- HTPerson NSObject class
+ HTPerson NSObject resolveInstanceMethod:
+ HTPerson NSObject resolveInstanceMethod:
- HTPerson NSObject doesNotRecognizeSelector:
- HTPerson NSObject doesNotRecognizeSelector:
- HTPerson NSObject class
这是崩溃前调用的函数记录。
我们发现调用者是HTPerson,我们直接在HTPerson中加入类似的方法进行实现。
- 在
HTPerosn实现中打印这四个函数
@implementation HTPerosn
+ (BOOL)resolveInstanceMethod:(SEL)sel {
NSLog(@"%s %@", __func__ ,NSStringFromSelector(sel));
return [super resolveInstanceMethod:sel];
}
- (id)forwardingTargetForSelector:(SEL)aSelector {
NSLog(@"%s %@", __func__ ,NSStringFromSelector(aSelector));
return [super forwardingTargetForSelector:aSelector];
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
NSLog(@"%s %@", __func__ ,NSStringFromSelector(aSelector));
return [super methodSignatureForSelector:aSelector];
}
-(void)doesNotRecognizeSelector:(SEL)aSelector {
NSLog(@"%s %@", __func__ ,NSStringFromSelector(aSelector));
return [super doesNotRecognizeSelector:aSelector];
}
@end

- 打印的
顺序和日志一样,那这四个函数有什么用呢?
我们知道,
resolveInstanceMethod动态方法决议是系统给开发者留的最后一次机会。
系统希望开发者在resolveInstanceMethod中将未实现的sel完成实现。并且他会在给完机会后重新执行一次lookUpImpOrForward。打印日志中有2次
resolveInstanceMethod,如果我们没有在resolveInstanceMethod中修改。 那么我们可以在第二次resolveInstanceMethod前的2个方法进行操作。所以我们可以在
forwardingTargetForSelector和methodSignatureForSelector方法里做补救。
大家都知道改resolveInstanceMethod,那就让你们随意发挥。大佬在你们resolveInstanceMethod后面阻击一切漏网之鱼。😼
现在,我们来了解大佬的法器
法器一:快速转发forwardingTargetForSelector
-
官方文档:
forwardingTargetForSelector
意思是:aSelector是没实现的选择器,我找不到实现的对象
- 既然你
找不到实现的对象,那我就给你个实现这个方法的对象
案例:

在第一次resolveInstanceMethod之后,成功替换了实现方法,并拦截了崩溃。
法器二:慢速转发methodSignatureForSelector


意思是:返回一个NSMethodSignature函数签名对象,Discussion中说了需要实现协议。搭配forwardinvocation函数使用。

点击进入NSInvocation查看格式:

我们打印invocation看看

invocation就是一个漂流瓶,只要methodSignatureForSelector返回不为nil,且实现了forwardinvocation函数,就一定不会崩溃。系统会把这个invocatio当做一个漂流瓶,抛弃它了。谁想处理谁处理,没人处理我也不管了invocation可以修改target接收对象和替换selector。可操作性空间更大。invocation需要调用invoke对象方法执行([invocation invoke])必要时可以保存
invocation,修改完参数再调用invoke执行。
例如:
- 在
HTStudent实现一个sayNB,然后修改anInvocation的target和selector。- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector { NSLog(@"%s %@", __func__ ,NSStringFromSelector(aSelector)); return [NSMethodSignature signatureWithObjCTypes:"v@:"]; } -(void)forwardInvocation:(NSInvocation *)anInvocation { anInvocation.target = [HTStudent alloc]; anInvocation.selector = @selector(sayNB); [anInvocation invoke]; }
4. 异常消息处理流程图

