OC底层原理十四:objc_msgSend(消息转发)
当我们发送的消息,在消息接受者
以及它的整个继承链缓存(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
就被重写
😂 毕竟牛逼的人很多。
这是打印结果:
![](https://img.haomeiwen.com/i12857030/77ec782fddcf6470.png)
-
厉害的人很多,大家都知道用
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
文件夹:
![](https://img.haomeiwen.com/i12857030/23aaded0c2d0d697.png)
我们打开任意一个文件夹,按Command + Shift + G
,输入/tmp
文件夹查看。发现并没有msgSends
名字的文件。
- 因为
系统默认
是关闭
日志功能,objcMsgLogEnabled
默认为false
。
![](https://img.haomeiwen.com/i12857030/ea254831b88ff047.png)
文件内搜索objcMsgLogEnabled
,发现赋值操作是在instrumentObjcMessageSends
中。
![](https://img.haomeiwen.com/i12857030/b0d6857d2606c99f.png)
所以我们在崩溃前
,手动调用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
文件
![](https://img.haomeiwen.com/i12857030/fd00452071b86cec.png)
打开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
![](https://img.haomeiwen.com/i12857030/8636c99973015b8e.png)
- 打印的
顺序
和日志一样,那这四个函数
有什么用呢?
我们知道,
resolveInstanceMethod动态方法决议
是系统
给开发者
留的最后一次机会
。
系统
希望开发者
在resolveInstanceMethod
中将未实现的sel
完成实现。并且他会在给完机会后
重新执行一次lookUpImpOrForward
。打印日志中有2次
resolveInstanceMethod
,如果我们没有在resolveInstanceMethod
中修改。 那么我们可以在第二次resolveInstanceMethod
前的2个方法进行操作。所以我们可以在
forwardingTargetForSelector
和methodSignatureForSelector
方法里做补救
。
大家都知道改resolveInstanceMethod
,那就让你们随意发挥。大佬在你们resolveInstanceMethod
后面阻击一切漏网之鱼
。😼
现在,我们来了解大佬的法器
法器一:快速转发forwardingTargetForSelector
-
官方文档:
forwardingTargetForSelector
意思是:aSelector
是没实现的选择器,我找不到
实现的对象
- 既然你
找不到
实现的对象
,那我就给你个实现
这个方法的对象
案例:
![](https://img.haomeiwen.com/i12857030/136365673bd7cd78.png)
在第一次resolveInstanceMethod
之后,成功替换
了实现
方法,并拦截
了崩溃
。
法器二:慢速转发methodSignatureForSelector
![](https://img.haomeiwen.com/i12857030/c1ae03b0ddef21ac.png)
![](https://img.haomeiwen.com/i12857030/775d9696ee889574.png)
意思是:返回一个NSMethodSignature
函数签名对象,Discussion
中说了需要实现协议
。搭配forwardinvocation
函数使用。
![](https://img.haomeiwen.com/i12857030/2940ca7e38f9b528.png)
点击进入NSInvocation
查看格式:
![](https://img.haomeiwen.com/i12857030/80535d9ff9fdbbe2.png)
我们打印invocation
看看
![](https://img.haomeiwen.com/i12857030/6722425206f27910.png)
-
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. 异常消息处理流程图
![](https://img.haomeiwen.com/i12857030/22b75a22569a1bc8.png)