前言
前面我们分析了消息的查找流程
,快速查找流程
,慢速查找流程
。如果这两种方式都没找到方法的实现,苹果给了两个建议
-
动态方法决议
:慢速查找流程未找到后,会执行一次动态方法决议 -
消息转发
:如果动态方法决议仍然没有找到实现,则进行消息转发
如果这两个建议都没有找到方法的实现,就会崩溃报错unrecognized selector sent to instance 0x600000c880d0
定义LGPerson
类,其中sayHello实例方法
没有实现,main.m文件
调用,崩溃如下
<!-- LGPerson.h文件 -->
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface LGPerson : NSObject
- (void)sayHello;
- (void)sayMaster;
@end
NS_ASSUME_NONNULL_END
<!-- LGPerson.m文件 -->
#import "LGPerson.h"
@implementation LGPerson
- (void)sayMaster {
NSLog(@"%@ : %s",self,__func__);
}
@end
image.png
方法找不到的报错底层
根据慢速查找
源码,我们发现其报错后都是走到__objc_msgForward_impcache
方法,以下是报错流程的源码
STATIC_ENTRY __objc_msgForward_impcache
// No stret specialization.
b __objc_msgForward
END_ENTRY __objc_msgForward_impcache
ENTRY __objc_msgForward
adrp x17, __objc_forward_handler@PAGE
ldr p17, [x17, __objc_forward_handler@PAGEOFF]
TailCallFunctionPointer x17
END_ENTRY __objc_msgForward
汇编实现中查找__objc_forward_handler
没有找到,在源码中去掉一个下划线进行全局搜索_objc_forward_handler
有如下实现,本质是调用objc_defaultForwardHandler
方法
// Default forward handler halts the process.
__attribute__((noreturn, cold)) void
objc_defaultForwardHandler(id self, SEL sel)
{
_objc_fatal("%c[%s %s]: unrecognized selector sent to instance %p "
"(no message forward handler is installed)",
class_isMetaClass(object_getClass(self)) ? '+' : '-',
object_getClassName(self), sel_getName(sel), self);
}
void *_objc_forward_handler = (void*)objc_defaultForwardHandler;
这就是我们日常开发中最常见的错误:没有实现函数,运行程序,崩溃报错提示
。探索流程总结
- 由控制台打印信息
unrecognized selector sent to instance
为出发点,向下深挖方法的响应流程
- 通过对
LookUpImpOrForward
的分析得出在慢速查找未找到的时候,给imp默认赋值forward_imp
即objc_msgForward_impcache
- 通过对
objc_msgForward_impcache
的查找,发现在汇编中它只是一个中间函数,真正指向的是objc_msgForward
- 对
objc_msgForward
的分析得出,方法内调用了objc_forward_handle
获取返回值存到x17寄存器
,再由TailCallFunctionPointer
方法跳转到x17寄存器内真正指向的imp地址 -
_objc_forward_handler
会给定默认的实现objc_defaultForwardHandle
,当imp最终的查找流程全部走完的时候还未找到imp,那么此时就会进入objc_defaultForwardHandle函数,将错误信息打印出来。
为了防止方法未实现的崩溃,下面我们从对象方法动态决议
开始探索
对象方法动态决议
在慢速查找流程
未找到方法实现时,循环查找结束,接下来进入动态方法决议
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
const IMP forward_imp = (IMP)_objc_msgForward_impcache;
IMP imp = nil;
for (unsigned attempts = unreasonableClassCount();;) {
if (slowpath((curClass = curClass->getSuperclass()) == nil)) {
//当消息的快速查找、慢速查找流程都走完还找不到的情况下
//imp会给一个默认值forward_imp,进入消息转发流程
imp = forward_imp;
break;
}
//找到了sel对应的imp
if (fastpath(imp)) {
goto done;
}
}
/**
* 如果遍历查找的过程找到了,会跳过此步骤,取到done分支,进行后续操作
* 如果找不到,会进行下面这个算法,最终进入resolveMethod_locked函数
* 此算法真正达到的目的为单例,保证一个lookUpImpOrForward
* 只执行一次resolveMethod_locked
*/
if (slowpath(behavior & LOOKUP_RESOLVER)) {
behavior ^= LOOKUP_RESOLVER;
return resolveMethod_locked(inst, sel, cls, behavior);
}
return imp;
}
位运算
-
&
:按位与,是双目运算符。其功能是参与运算的两数各对应的二进位相与。相同位置数字都为1时,结果位才为1。 -
^
:按位异或,比较的是二进制位,相同位置数字不同为1,相同为0 -
^=
:按位异或,比较的是二进制位,相同位置数字不同为1,相同为0,将结果赋值为运算符左边。
单例解读
.macro MethodTableLookup
SAVE_REGS MSGSEND
// lookUpImpOrForward(obj, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER)
// receiver and selector already in x0 and x1
mov x2, x16
mov x3, #3
bl _lookUpImpOrForward
// IMP in x0
mov x17, x0
RESTORE_REGS MSGSEND
.endmacro
- 前提条件(初始值)
behavior = 3 LOOKUP_RESOLVER = 2
- 初次判断操作
behavior & LOOKUP_RESOLVER
= 3 & 2 = 0x11 & 0x10 = 0x10 = 2 - 重置behaivor
behavior
=behavior ^ LOOKUP_RESOLVER
= 3 ^ 2 = 0x11 ^ 0x10 = 0x01 = 1 - 再次进入判断操作(第二次至无限次)
behavior & LOOKUP_RESOLVER
= 1 & 2 = 0x01 & 0x10 = 0x00 = 0
得出结论
保证了每一个lookUpImpOrForward
函数最多只能执行一次resolveMethod_locked
(动态方法决议),直到behavior被重新赋值
动态方法决议源码
调用一个方法流程,先进入消息快速查询流程
-> 然后消息慢速查找流程
,当底层源码已经查找了两遍依然找不到方法的实现,此时imp=nil
,理论上来讲程序应该崩溃。但在开发者的角度,崩溃表示这个框架不稳定,系统很不友善。所以此框架决定再给你一次机会,为你提供一个返回自定义imp的机会,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()) { //类不是元类,调用对象的解析方法
// try [cls resolveInstanceMethod:sel]
resolveInstanceMethod(inst, sel, cls);
}
else {//如果是元类,调用类的解析方法, 类 -- 元类
// 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);
}
该函数内的三个关键函数
-
resolveInstanceMethod
:实例方法动态添加imp -
resolveClassMethod
:类方法动态添加imp -
lookUpImpOrForwardTryCache
,当完成添加之后,回到之前的慢速查找流程再来一遍
类动态方法决议会进入resolveClassMethod
,然后根据判断有可能会再次进入resolveInstanceMethod
为什么?
正常的类对象动态方法决议会进入resolveClassMethod
,这点是毋庸置疑的,但是类方法查找过程是在元类中查找,那么通过isa的指向图中可以得知,类方法的查找过程也是有继承关系的,会一直向上找,找到superMetaClass
,找到rootMetaClass
,最终找到NSObject
,到这一层所有的方法对于NSObject来讲都是实例方法所以会调用resolveInstanceMethod
。
实例方法源码
针对实例方法调用
,在快速-慢速查找
均没有找到实例方法的实现时,我们有一次挽救的机会即尝试一次动态方法决议
,由于是实例方法,所以会走到resolveInstanceMethod
方法
static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{
runtimeLock.assertUnlocked();
ASSERT(cls->isRealized());
SEL resolve_sel = @selector(resolveInstanceMethod:);
// look的是 resolveInstanceMethod --相当于是发送消息前的容错处理
if (!lookUpImpOrNil(cls, resolve_sel, cls->ISA())) {
// Resolver not implemented.
return;
}
BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
bool resolved = msg(cls, resolve_sel, sel); //发送resolve_sel消息
// Cache the result (good or bad) so the resolver doesn't fire next time.
// +resolveInstanceMethod adds to self a.k.a. cls
//查找say666
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
消息前,需要查找cls类中是否有该方法的实现,即通过lookUpImpOrNil
方法又会进入lookUpImpOrForward
慢速查找流程查找resolveInstanceMethod
方法
如果没有直接返回
如果有则发送resolveInstanceMethod
消息 - 再次慢速查找实例方法的实现,即通过
lookUpImpOrNil
方法又会进入lookUpImpOrForward
慢速查找流程查找实例方法
崩溃修改
针对实例方法sayHello
未实现的报错,可以通过在类中重写resolveInstanceMethod
类方法,并将其指向其他方法的实现。即在LGPerson中重写resolveInstanceMethod
类方法,将实例方法sayHello
的实现指向sayMaster
方法实现,如下所示
// LGPerson.m 文件添加如下resolveInstanceMethod方法
+ (BOOL)resolveInstanceMethod:(SEL)sel{
if (sel == @selector(sayHello)) {
NSLog(@"%@ 来了", NSStringFromSelector(sel));
//获取sayMaster方法的imp
IMP imp = class_getMethodImplementation(self, @selector(sayMaster));
//获取sayMaster的实例方法
Method sayMethod = class_getInstanceMethod(self, @selector(sayMaster));
//获取sayMaster的丰富签名
const char *type = method_getTypeEncoding(sayMethod);
//将sel的实现指向sayMaster
return class_addMethod(self, sel, imp, type);
}
return [super resolveInstanceMethod:sel];
}
// 运行工程其打印如下
2021-08-01 17:51:30.226277+0800 DDDDD[5106:762939] sayHello 来了
2021-08-01 17:51:30.226734+0800 DDDDD[5106:762939] <LGPerson: 0x281ecc040> : -[LGPerson sayMaster]
类方法的动态决议
类方法动态决议源码
/*********************************************************
* 解析类方法
* 调用+resolveClass 方法,寻找要添加到类cls 的方法。
* cls 应该是一个元类。
* 不检查该方法是否已经存在。
*********************************************************/
static void resolveClassMethod(id inst, SEL sel, Class cls)
{
runtimeLock.assertUnlocked();
ASSERT(cls->isRealized());
ASSERT(cls->isMetaClass());
//当你为实现resolveClassMethod的时候,此处也不会进入return
//因为系统给resolveClassMethod函数默认返回NO,即默认实现了
if (!lookUpImpOrNilTryCache(inst, @selector(resolveClassMethod:), cls)) {
// Resolver not implemented.
return;
}
//nonmeta容错处理
Class nonmeta;
{
mutex_locker_t lock(runtimeLock);
nonmeta = getMaybeUnrealizedNonMetaClass(cls, inst);
if (!nonmeta->isRealized()) {
_objc_fatal("nonmeta class %s (%p) unexpectedly not realized",
nonmeta->nameForLogging(), nonmeta);
}
}
//系统会在此处为你发送一个消息resolveClassMethod
//当你的这个类检测了这个消息,并且做了处理
BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
bool resolved = msg(nonmeta, @selector(resolveClassMethod:), sel);
//那么此时系统会重新查找,此函数最终会触发LookUpImpOrForward
IMP imp = lookUpImpOrNilTryCache(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));
}
}
}
LGPerson
类中添加类方法如下,其中sayHappy
方法没有实现
<!-- LGPerson.h文件 -->
+ (void)sayNB;
+ (void)sayHappy;
<!-- LGPerson.m文件 -->
+ (void)sayNB {
NSLog(@"%@ : %s",self,__func__);
}
// 添加类方法动态决议
+ (BOOL)resolveClassMethod:(SEL)sel{
if (sel == @selector(sayHappy)) {
NSLog(@"%@ 来了", NSStringFromSelector(sel));
IMP imp = class_getMethodImplementation(objc_getMetaClass("LGPerson"), @selector(sayNB));
Method lgClassMethod = class_getInstanceMethod(objc_getMetaClass("LGPerson"), @selector(sayNB));
const char *type = method_getTypeEncoding(lgClassMethod);
return class_addMethod(objc_getMetaClass("LGPerson"), sel, imp, type);
}
return [super resolveClassMethod:sel];
}
// 运行工程其打印如下
2021-08-01 18:46:27.239762+0800 DDDDD[5295:774648] sayHappy 来了
2021-08-01 18:46:27.239814+0800 DDDDD[5295:774648] LGPerson : +[LGPerson sayNB]
注意: resolveClassMethod
类方法的重写传入的cls
不再是类,而是元类。可以通过objc_getMetaClass
方法获取类的元类,原因是类方法在元类中是实例方法
得出结论
- 通过手动添加
resolveInstanceMethod/resolveClassMethod
可以防止方法未实现崩溃 - 通过判断当前查找的sel动态添加一个新的imp
- 此时
sel(sayHello/sayHappy)
对应实现的imp不再是sayHello/sayHappy
,而是变成了我动态指定的sayMaster/sayNB
优化
日常开发中不可能每个类中都实现一份resolveInstanceMethod/resolveClassMethod
,有没有更好的解决方案呢?通过方法慢速查找流程可以发现其查找路径有两条
- 实例方法:
类 -- 父类 -- 根类 -- nil
- 类方法:
元类 -- 根元类 -- 根类 -- nil
它们的共同点是如果前面没找到,都会来到根类即NSObject
中查找,所以我们可以将上述的两个方法整合在一起。通过NSObject添加分类的方式来统一处理,而且由于类方法的查找在其继承链中查找的也是实例方法,所以可以将实例方法和类方法统一放在resolveInstanceMethod
方法中处理,如下所示
@implementation NSObject (LG)
//为实例对象创建一个IMP
- (void) sayMaster {
NSLog(@"%@ : %s",self,__func__);
}
//为元类对象创建创建一个IMP
+ (void) sayNB {
NSLog(@"%@ - %s",self,__func__);
}
#pragma clang diagnostic push
// 让编译器忽略错误
#pragma clang diagnostic ignored "-Wundeclared-selector"
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(sayHello)) {
NSLog(@"%@ 来了", NSStringFromSelector(sel));
IMP imp = class_getMethodImplementation(self, @selector(sayMaster));
Method sayMethod = class_getInstanceMethod(self, @selector(sayMaster));
const char *type = method_getTypeEncoding(sayMethod);
return class_addMethod(self, sel, imp, type);
} else if (sel == @selector(sayHappy)) {
NSLog(@"%@ 来了", NSStringFromSelector(sel));
IMP imp = class_getMethodImplementation(objc_getMetaClass("LGPerson"), @selector(sayNB));
Method lgClassMethod = class_getInstanceMethod(objc_getMetaClass("LGPerson"), @selector(sayNB));
const char *type = method_getTypeEncoding(lgClassMethod);
return class_addMethod(objc_getMetaClass("LGPerson"), sel, imp, type);
}
return NO;
}
#pragma clang diagnostic pop
@end
// 运行工程其打印如下
2021-08-01 19:20:31.446141+0800 DDDDD[5405:783621] sayHello 来了
2021-08-01 19:20:31.446775+0800 DDDDD[5405:783621] <LGPerson: 0x2838f8080> : -[NSObject(LG) sayMaster]
2021-08-01 19:20:31.446847+0800 DDDDD[5405:783621] sayHappy 来了
2021-08-01 19:20:31.446881+0800 DDDDD[5405:783621] LGPerson - +[NSObject(LG) sayNB]
这种方式的实现,正好与源码中针对类方法的处理逻辑是一致的,即完美阐述为什么调用了类方法动态方法决议
,还要调用对象方法动态方法决议
,其根本原因还是类方法在元类中的实例方法
。
当然,上面这种写法还是会有其他的问题,比如系统方法也会被更改
,针对这一点是可以优化的,即我们可以针对自定义类中方法统一方法名的前缀,根据前缀来判断是否是自定义方法,然后统一处理自定义方法,例如可以在崩溃前pop到首页,主要是用于app线上防崩溃
的处理,提升用户的体验。
aop和oop总结
oop
:面向对象编程
,不同类做不同事情,分工明确。
-
优点
:耦合度很低 -
缺点
:代码冗余,常规解决办法是提取,这样会有一个公共类。所有人对公共类进行集成,形成强依赖,也就代表着出现了强耦合
aop
:面向切面编程,是oop的延伸
-
优点
:对业务无侵入,通过动态方式将某些方法进行注入 -
缺点
:做了一些判断,执行了很多无关代码,包括系统方法,造成性能消耗,会打断apple的方法动态转发流程。
消息转发流程引入
在慢速查找的流程中,我们了解到如果快速+慢速
没有找到方法实现,动态方法决议
也不行,就使用消息转发
。但是我们找遍了源码也没有发现消息转发的相关源码,可以通过以下方式来了解,方法调用崩溃前都走了哪些方法
- 通过
instrumentObjcMessageSends
方式打印发送消息的日志
objc4-818.2源码
全局搜索instrumentObjcMessageSends
实现如下
void instrumentObjcMessageSends(BOOL flag)
{
bool enable = flag;
// Shortcut NOP
if (objcMsgLogEnabled == enable)
return;
// If enabling, flush all method caches so we get some traces
if (enable)
_objc_flush_caches(Nil);
// Sync our log file
if (objcMsgLogFD != -1)
fsync (objcMsgLogFD);
objcMsgLogEnabled = enable;
}
探索instrumentObjcMessageSends
流程
- 通过
lookUpImpOrForward --> log_and_fill_cache --> logMessageSend
,在logMessageSend
源码下方找到instrumentObjcMessageSends
的源码实现 - 在
main
中调用instrumentObjcMessageSends
打印方法调用的日志信息,有以下两点准备
- 打开
objcMsgLogEnabled
开关,即调用instrumentObjcMessageSends
方法时传入YES - 在
main
中通过extern
声明instrumentObjcMessageSends
方法
extern void instrumentObjcMessageSends(BOOL flag);
int main(int argc, const char * argv[]) {
@autoreleasepool {
LGPerson *person = [LGPerson alloc];
//使用区域,将想要查找的方法用instrumentObjcMessageSends圈起来
//开始监听传递参数为YES,结束监听传递参数为NO
instrumentObjcMessageSends(YES);
[person sayHello];
// 这里再次赋值为NO,表示只查看sayHello方法
instrumentObjcMessageSends(NO);
}
return 0;
}
- 通过
logMessageSend
源码了解到消息发送打印信息存储在/tmp/msgSends
目录
bool logMessageSend(bool isClassMethod,
const char *objectsClass,
const char *implementingClass,
SEL selector)
{
char buf[ 1024 ];
// Create/open the log file
if (objcMsgLogFD == (-1))
{
snprintf (buf, sizeof(buf), "/tmp/msgSends-%d", (int) getpid ());
objcMsgLogFD = secure_open (buf, O_WRONLY | O_CREAT, geteuid());
if (objcMsgLogFD < 0) {
// no log file - disable logging
objcMsgLogEnabled = false;
objcMsgLogFD = -1;
return true;
}
}
// Make the log entry
snprintf(buf, sizeof(buf), "%c %s %s %s\n",
isClassMethod ? '+' : '-',
objectsClass,
implementingClass,
sel_getName(selector));
objcMsgLogLock.lock();
write (objcMsgLogFD, buf, strlen(buf));
objcMsgLogLock.unlock();
// Tell caller to not cache the method
return false;
}
- 运行代码并前往
/tmp/msgSends
目录,发现有msgSends
开头的日志文件,打开发现在崩溃前执行了以下方法
两次动态方法决议
:resolveInstanceMethod方法
两次消息快速转发
:forwardingTargetForSelector方法
两次消息慢速转发
:methodSignatureForSelector + resolveInvocation
网友评论