在上一篇文章iOS-底层原理11:消息流程分析之慢速查找 中,分析了消息慢速查找流程
,如果查找不到将进行动态方法决议
,如果动态方法决议仍然没有找到实现,则进行消息转发
。
案例
step1:
新建一个LBHPerson
类,定义一个实例方法instanceMethod1
和一个类方法classMethod1
,只声明不实现
//.h
@interface LBHPerson : NSObject
- (void)instanceMethod1;
+ (void)classMethod1;
@end
//.m
@implementation LBHPerson
@end
step2:
在main
函数中调用LBHPerson
类的实例方法instanceMethod1
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
LBHPerson *person = [LBHPerson alloc];
[person instanceMethod1];
}
return 0;
}
step3:
运行结果
调用类方法
[LBHPerson classMethod1];
运行结果
unrecognized selector sent to instance 0xxxxx
找不到方法实现
这是一个开发中很常见的奔溃问题,先学习这篇文章,然后用动态方法决议和消息转发解决这个问题。
1. 动态方法决议
动态方法决议
:慢速查找流程未找到方法,会给一次机会
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);
}
分为以下几步:
part1:
判断cls
是否是元类
- 如果是
类
,调用实例方法
的动态方法决议resolveInstanceMethod
- 如果是
元类
,调用类方法
的动态方法决议resolveClassMethod
,如果在元类
中没有找到或者为空
,则在元类
的实例方法
的动态方法决议resolveInstanceMethod
中查找, 是因为类方法存储在元类中,是元类的实例方法,所以还需要查找元类中实例方法的动态方法决议
part2:
如果动态方法决议中,将其实现指向了其他方法
,则继续查找
指定的imp
,即继续慢速查找lookUpImpOrForward
流程
此时
behavior = 1
,LOOKUP_CACHE = 4
,lookUpImpOrForward
函数中形参behavior
变成了1 | 4 = 5
,这决定了进入lookUpImpOrForward后:
fastpath(behavior & LOOKUP_CACHE) = 5 & 4 = 4
,条件成立,会优先cache_getImp
读取一次缓存slowpath(behavior & LOOKUP_RESOLVER) = 5 & 2 = 0
,条件成立,不会进入resolveMethod_locked
动态方法决议。lookUpImpOrForward
会循环遍历cls继承链的所有类的cache和methodList来寻找imp
流程图:
1.1 实例方法决议
step1:
实例方法在快速查找
-> 慢速查找
都没有找到的情况下,会走到 resolveInstanceMethod
方法,源码如下:
static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{
runtimeLock.assertUnlocked();
ASSERT(cls->isRealized());
SEL resolve_sel = @selector(resolveInstanceMethod:);
// 1. 查找元类对象(cls->ISA())的类中是否有`resolveInstanceMethod`的imp。
// (根元类中默认实现了`resolveInstanceMethod`方法,所以永远不会return)
if (!lookUpImpOrNil(cls, resolve_sel, cls->ISA())) {
return;
}
// 2. 发送resolve_sel消息
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));
}
}
}
分步解析:
part1:
查找resolve_sel
if (!lookUpImpOrNil(cls, resolve_sel, cls->ISA())) {
// Resolver not implemented.
return;
}
问题
:lookUpImpOrNil
到底做了什么?
解答
:
查看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); }
可以看到在
lookUpImpOrNil
中又调用了lookUpImpOrForward慢速查找流程
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
方法实现。实际上根本不会进入if
条件,因为在NSObject元类
存在resolveInstanceMethod
类方法。
问题
:为什么不会进if
条件?NSObject元类
中存在resolveInstanceMethod
类方法能证明吗?
解答
:/// 遍历方法 -(void) printMethodes: (Class)cls { // 记录函数个数 unsigned int count = 0; // 读取函数列表 Method *methodList = class_copyMethodList(cls, &count); for (int i = 0; i < count; i++) { Method method = methodList[i]; SEL sel = method_getName(method); IMP imp = class_getMethodImplementation(cls, sel); NSLog(@"method: %@-%p", NSStringFromSelector(sel), imp); } free(methodList); } //调用 [self printMethodes:objc_getMetaClass("NSObject")];
运行结果
在
NSObject元类
方法列表中可以找到resolveInstanceMethod
类方法
part2:
发送resolve_sel
消息
// 2. 消息发送
BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
bool resolved = msg(cls, resolve_sel, sel);
part3:
通过慢速查找流程
获取用户调用的方法sel
(demo中为instanceMethod1)的方法实现imp
,此处的获取是为了日志使用
IMP imp = lookUpImpOrNil(inst, sel, cls);
奔溃修改
step1:
在LBHPerson
中新增一个lbhInstanceMethod
的实例方法,声明并实现
//.h
@interface LBHPerson : NSObject
- (void)lbhInstanceMethod;
@end
//.m
@implementation LBHPerson
- (void)lbhInstanceMethod
{
NSLog(@"%s",__func__);
}
@end
step2:
在LBHPerson
类中重写resolveInstanceMethod
类方法
@implementation LBHPerson
+ (BOOL)resolveInstanceMethod:(SEL)sel{
NSLog(@"%@ 来了", NSStringFromSelector(sel));
if (sel == @selector(instanceMethod1)) {
//获取lbhInstanceMethod方法的imp
IMP imp = class_getMethodImplementation(self, @selector(lbhInstanceMethod));
//获取lbhInstanceMethod的实例方法
Method lbhInstanceMethod = class_getInstanceMethod(self, @selector(lbhInstanceMethod));
//获取lbhInstanceMethod的丰富签名
const char *type = method_getTypeEncoding(lbhInstanceMethod);
//将sel的实现指向lbhInstanceMethod
return class_addMethod(self, sel, imp, type);
}
return [super resolveInstanceMethod:sel];
// return NO;
}
@end
运行
崩溃解决了,实际上这么写是比较鸡肋的,都已经知道某个方法没有实现,那直接实现就好了,当然可以将if
条件去掉,所有未实现的方法都走这个实现,那么有没有更好的方法呢?继续往下学习。
1.2 类方法决议
类方法在快速查找
-> 慢速查找
都没有找到的情况下,会走到resolveClassMethod
方法,源码如下:
static void resolveClassMethod(id inst, SEL sel, Class cls)
{
runtimeLock.assertUnlocked();
ASSERT(cls->isRealized());
ASSERT(cls->isMetaClass());
if (!lookUpImpOrNil(inst, @selector(resolveClassMethod:), cls)) {
// Resolver not implemented.
return;
}
Class nonmeta;
{
mutex_locker_t lock(runtimeLock);
nonmeta = getMaybeUnrealizedNonMetaClass(cls, inst);
// +initialize path should have realized nonmeta already
if (!nonmeta->isRealized()) {
_objc_fatal("nonmeta class %s (%p) unexpectedly not realized",
nonmeta->nameForLogging(), nonmeta);
}
}
BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
bool resolved = msg(nonmeta, @selector(resolveClassMethod:), sel);
// Cache the result (good or bad) so the resolver doesn't fire next time.
// +resolveClassMethod adds to self->ISA() 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 resolveClassMethod:%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));
}
}
}
resolveClassMethod
方法流程与resolveInstanceMethod
方法流程类似。
崩溃解决
在LBHPerson
中添加一个lbhClassMethod
类方法的,重写resolveClassMethod
类方法
+ (void)lbhClassMethod
{
NSLog(@"%s",__func__);
}
+ (BOOL)resolveClassMethod:(SEL)sel{
NSLog(@"%@ 来了", NSStringFromSelector(sel));
if (sel == @selector(classMethod1)) {
IMP imp = class_getMethodImplementation(objc_getMetaClass("LBHPerson"), @selector(lbhClassMethod));
Method lgClassMethod1 = class_getInstanceMethod(objc_getMetaClass("LBHPerson"), @selector(lbhClassMethod));
const char *type = method_getTypeEncoding(lgClassMethod1);
return class_addMethod(objc_getMetaClass("LBHPerson"), sel, imp, type);
}
return [super resolveClassMethod:sel];
}
1.3 优化
上面解决方法都是在单独的某个类中重写动态决议方法,这意味着每个类中都需要重写这两个方法,这样太麻烦了,怎么做呢? 相信大家都会。
- 实例方法:
类
-->父类
-->根类
-->nil
- 类方法 :
元类
-->根元类
-->根类
-->nil
如果在当前类
或元类
中没有找到方法实现,会沿着它们的继承链向上查找,它们都会经过根类即NSObject
。
问题
: 是否可以将上述的两个方法统一整合在一起呢?
解答
:是可以的,可以通过NSObject分类
的方式来实现统一处理
,而且由于类方法的查找,在其继承链,查找的也是实例方法,所以可以将实例方法
和类方法
的统一放在resolveInstanceMethod
方法中处理。
+ (BOOL)resolveInstanceMethod:(SEL)sel{
NSLog(@"%@ 来了", NSStringFromSelector(sel));
if (sel == @selector(classMethod1)) {
IMP imp = class_getMethodImplementation(objc_getMetaClass("LBHPerson"), @selector(lbhClassMethod));
Method lgClassMethod1 = class_getInstanceMethod(objc_getMetaClass("LBHPerson"), @selector(lbhClassMethod));
const char *type = method_getTypeEncoding(lgClassMethod1);
return class_addMethod(objc_getMetaClass("LBHPerson"), sel, imp, type);
}else if (sel == @selector(instanceMethod1)) {
//获取lbhInstanceMethod方法的imp
IMP imp = class_getMethodImplementation(self, @selector(lbhInstanceMethod));
//获取lbhInstanceMethod的实例方法
Method lbhInstanceMethod1 = class_getInstanceMethod(self, @selector(lbhInstanceMethod));
//获取lbhInstanceMethod的丰富签名
const char *type = method_getTypeEncoding(lbhInstanceMethod1);
//将sel的实现指向lbhInstanceMethod
return class_addMethod(self, sel, imp, type);
}
// return [super resolveInstanceMethod:sel];
return NO;
}
这种方式的实现,正好与源码中针对类方法的处理逻辑是一致的,即完美阐述为什么调用了类方法动态方法决议,还要调用对象方法动态方法决议,其根本原因是类方法是元类中的实例方法
。
2. 消息转发
我们了解到,如果快速+慢速没有找到方法实现,动态方法决议也不行,就使用消息转发
,但是,我们找遍了源码也没有发现消息转发的相关源码,可以通过以下方式来了解:
- 通过
instrumentObjcMessageSends
方式打印发送消息的日志 - 通过
hopper/IDA反编译
2.1 instrumentObjcMessageSends
通过lookUpImpOrForward
--> log_and_fill_cache
--> logMessageSend
,在logMessageSend
源码下方找到instrumentObjcMessageSends
的源码实现。
在main中调用
instrumentObjcMessageSends
打印方法调用的日志信息,有以下两点准备工作
1、打开 objcMsgLogEnabled
开关,即调用instrumentObjcMessageSends
方法时,传入YES
2、在main中通过extern
声明instrumentObjcMessageSends
方法
//最好使用命令行
extern void instrumentObjcMessageSends(BOOL flag);
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
LBHPerson *person = [LBHPerson alloc];
instrumentObjcMessageSends(YES);
[person instanceMethod1];
// [LBHPerson classMethod];
instrumentObjcMessageSends(NO);
}
return 0;
}
- 通过
logMessageSend
源码,了解到消息发送打印信息存储在/tmp
文件夹下,
- 运行代码,并前往
/tmp
文件夹,发现有msgSends
开头的日志文件,打开发现在崩溃前,执行了以下方法
2.2 通过hopper/IDA反编译
Hopper
和IDA
是一个可以帮助我们静态分析可视性文件的工具,可以将可执行文件反汇编成伪代码、控制流程图
等,下面以Hopper为例 (针对比较简单的反汇编,demo版本即可)
step1:
在上面的例子中,查看下崩溃的堆栈信息
发现___forwarding___
来自CoreFoundation
step2:
通过image list
,读取整个镜像文件,然后搜索CoreFoundation
,查看其可执行文件的路径
step3:
通过文件路径,找到CoreFoundation
的可执行文件
step4:
打开hopper
,选择Try the Demo
,然后将上一步的可执行文件拖入hopper
进行反汇编,选择x86(64 bits)
step5:
以下是反汇编后的界面,主要使用上面的三个功能,分别是 汇编、流程图、伪代码
step6:
通过左侧的搜索框搜索__forwarding_prep_0___
,然后选择伪代码
step7:
进入___forwarding___
的伪代码实现,首先是查看是否实现forwardingTargetForSelector
方法,如果没有响应,跳转至loc_6459b
即快速转发没有响应,进入慢速转发流程
step8:
跳转至loc_64a67
,在其下方判断是否响应methodSignatureForSelector
方法
- 如果
没有响应
,跳转至loc_64dd7
,则直接报错 - 如果获取
methodSignatureForSelector
的方法签名为nil
,也是直接报错
step9:
如果methodSignatureForSelector
返回值不为空,则在forwardInvocation
方法中对invocation
进行处理
所以,通过上面两种查找方式可以验证,消息转发的方法有3个
步骤 | 方法 |
---|---|
快速转发 | forwardingTargetForSelector |
慢速转发 |
methodSignatureForSelector + forwardInvocation
|
综上所述,消息转发整体的流程如下
3. 消息转发之快速转发
针对前面的崩溃问题,如果动态方法决议也没有找到实现
,则需要在LBHPerson中重写forwardingTargetForSelector
方法,将LBHPerson的实例方法的接收者指定为LBHStudent 的对象
(LBHStudent类中有instanceMethod1的具体实现),如下所示
//LBHPerson
@interface LBHPerson : NSObject
- (void)instanceMethod1;
@end
@implementation LBHPerson
- (id)forwardingTargetForSelector:(SEL)aSelector{
NSLog(@"%s - %@",__func__,NSStringFromSelector(aSelector));
// runtime + aSelector + addMethod + imp
//将消息的接收者指定为LBHStudent,在LBHStudent中查找instanceMethod1的实现
return [LBHStudent alloc];
}
@end
//LBHStudent
@interface LBHStudent : LBHPerson
@end
@implementation LBHStudent
- (void)instanceMethod1
{
NSLog(@"%s",__func__);
}
@end
运行结果
问题:
如果将LBHStudent
的instanceMethod1
方法注释掉,程序运行并不会崩溃注释掉
LBHStudent
的instanceMethod1
方法,在forwardingTargetForSelector
打上断点,发现程序在不停的执行这个方法,具体原因后续再去查找,不停的执行这个方法是很不好的。
实际上这么写是很鸡肋的,除非把所有方法都写在一个类中,显然这是不现实的,而且指定的类中如果没有这个方法的实现,forwardingTargetForSelector
方法会一直被调用
将forwardingTargetForSelector
方法改一下
- (id)forwardingTargetForSelector:(SEL)aSelector
{
NSLog(@"LBHPerson %s %@",__func__,NSStringFromSelector(aSelector));
// return [NSObject alloc];
return [super forwardingTargetForSelector:aSelector];
}
这么写可以解决方法找不到而一直执行forwardingTargetForSelector
,但是并不能解决崩溃,需要配合慢速转发使用。
4. 消息转发之慢速转发
如果快速转发中还是没有找到
,则进入最后的一次挽救机会,即在LBHPerson中重写methodSignatureForSelector
,如下所示
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
NSLog(@"%s - %@",__func__,NSStringFromSelector(aSelector));
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation{
NSLog(@"%s - %@",__func__,anInvocation);
}
运行结果
当然此时可以将快速转发的代码注释掉
,只保留慢速转发
也可以处理invocation
事务,如下所示,修改invocation的target
为[LBHStudent alloc]
,调用 [anInvocation invoke]
触发 LBHStudent
类的instanceMethod1
实例方法
不过实际开发中这么写比较鸡肋,有种画蛇添足的感觉。