上一篇我们提到了动态方法决议
形式,那么现在让我们继续补充。
动态方法决议调用次数
运行项目可以发现这个resolveInstanceMethod
方法会调用两次,如下图
那么为什么会调用两次呢?继续探索
*【第一步】
LOOKUP_RESOLVER
宏定义固定值时2
。这个判断是为了控制条件
,只有两者存在
的时候才能进入(进行亦或操作,behavior
^LOOKUP_RESOLVER
相同是0
,不同是1
),然后进入动态方法决议
//判断两者都存在进入,控制条件,然后直接返回imp
if (slowpath(behavior & LOOKUP_RESOLVER)) {
behavior ^= LOOKUP_RESOLVER;//输出值不同为1,相同0
return resolveMethod_locked(inst, sel, cls, behavior);
}
*【第二步】 resolveMethod_locked
动态方法决议源码如下,第一次进入就发送了消息
,调用resolveInstanceMethod
方法
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
源码如下- 其中
lookUp的是resolveInstanceMethod方法
,因为say666没有意义
, - 这时需要容错处理,不然找不到方法会崩溃
-
IMP imp = lookUpImpOrNil(inst, sel, cls);
进行查找say666
方法
- 其中
static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{
runtimeLock.assertUnlocked();
ASSERT(cls->isRealized());
SEL resolve_sel = @selector(resolveInstanceMethod:);
//lookup resolveInstanceMethod,say666没有意义
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);
// 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);//查询say666
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));
}
}
}
- 【第四步】再次通过【第一步】进入动态方法决议
resolveMethod_locked
源码位置,定位到lookUpImpOrForward
方法,再走一次动态方法决议流程
。
这时resolveInstanceMethod
方法就走了两次
,同时也因为苹果再给一个机会
// chances are that calling the resolver have populated the cache
// so attempt using it
return lookUpImpOrForward(inst, sel, cls, behavior | LOOKUP_CACHE);
类方法的动态方法决议
-
main
函数调用LGPerson 中的 sayNB类方法
,并且sayNB
在LGPerson的m文件中并没有实现
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
LGPerson *person = [LGPerson alloc];
[LGPerson sayNB];
}
return 0;
}
- 流程和上面一样依旧会进入
lookUpImpOrForward
源码中的动态方法决议中resolveMethod_locked
,因为sayNB
是类方法,即元类中的实例方法
。动态方法决议
中的inst
是类
,cls
就是元类
,因此会走else
方法内容。
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);
}
}
- 再次进入
resolveClassMethod
源码中,作出相应的处理
static void resolveClassMethod(id inst, SEL sel, Class cls)
{
runtimeLock.assertUnlocked();
ASSERT(cls->isRealized());
ASSERT(cls->isMetaClass());
//判断resolveClassMethod
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));
}
}
}
运行之后,我们会发现系统崩溃
了,因为sayNB
类方法没有被实现,那么如何避免崩溃
呢?
-
使用
resolveClassMethod
的类方法和resolveInstanceMethod
一样的判断- 对象方法,实现方法需要使用
sayNB
类方法 - 根据
isa
走位图,不应该找当前的self
,需要查找当前的元类
即类方法存在元类中
这就解决了崩溃的现象,当时这是我们需要思考一个问题
为什么动态的类方法决议
之后还需要动态方法进行对象方法
?
- 对象方法,实现方法需要使用
-
NSObject对象方法
是对类
的考究,类方法
是元类
的考究,元类是对象方法的存在
-
开发人员
认为它是一个类方法
,但是我们所有的操作
都在类方法
里面,其实本身
是不对
的,根本的查询路线
还是在元类
里面,因此需要查询整个
关于对象的路径
,因为就有了resolveInstanceMethod
方法
由此就有另外一个问题
是不是可以不写resolveClassMethod
直接在resolveInstanceMethod
方法中以else
的方式写呢?
运行之后发现是不行的,因为在类
是无法找到类方法
的,因为它是存在元类
中,所以是不能这样的写的
同时因为元类的继承链
关系,因此我们所有方法的补救方法
都会来到NSObject
,那么我们就可以创建一个NSObject
的分类
来防崩溃。
实际上,在NSObject
中resolveInstanceMethod
默认是有实现
,我们在这里将重写
而不是覆盖
这个方法
。
- 头部导入
#import <objc/message.h>
-
调用方法
的时候优先
查找分类
问题是解决了,但是我们这种方式不好
,我们在打印了很多我们不需要处理的消息
分类非常的便利
,同时这种写法就相当于封装sdk
判断对应的方法
,切面
,让其不会产生崩溃
但是这样写还是不太好,例如你封装了这样的防崩溃处理
,但是其他人不知道也写了,那么就导致sdk
就浪费
了,其实真正的sdk
是健全稳定而长期的
,那么这里做防崩溃有点暴力
,因此这里一般不处理,给他一个容错
的机会。
但是我们还是有崩溃的现象,怎么办呢?
那么就会进入另一个层面
,就是动态方法决议
没有处理的话,他就会进行消息转发流程
消息转发探索
快速消息转发
- 这时在
lookUpImpOrForward
源码中我们注意到log_and_fill_cache
的源码
if (slowpath(objcMsgLogEnabled && implementer)) {
bool cacheIt = logMessageSend(implementer->isMetaClass(),
cls->nameForLogging(),
implementer->nameForLogging(),
sel);
if (!cacheIt) return;
}
-
objcMsgLogEnabled
源码进行消息的打印,这是我们将介绍instrumentObjcMessageSends
方法,用于监控objc
底层发送的一些发送的消息
,调用这个方法需要几个条件:- 1、
objcMsgLogEnabled
控制开关,进行处理。 - 2、
extern
在变量函数中表示定义在其他文件中,编译器去他地方找,这里暂时没有不要报错
- 1、
-
在main函数的源码中对
instrumentObjcMessageSends函数
的调用,先开启再关闭
,进行包裹作用,嵌住研究重点,并且打印objc
的调用情况。
-
objc
消息处理的源码
bool logMessageSend(bool isClassMethod,
const char *objectsClass,
const char *implementingClass,
SEL selector)
{
char buf[ 1024 ];
// Create/open the log file
if (objcMsgLogFD == (-1))
{
//tmp/msgSends写入的文件目录
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;
}
查找objc调用方法文件
-
command+shift+g
打开搜索框输入/tmp/msgSends/
进入tmp文件,这时什么都没有,我们需要再运行一次即使崩溃
也没关系,这时就会有对应的文件
-
双击打开文件
从图中可以发现动态方法决议resolveInstanceMethod
之后调用forwardingTargetForSelector
,LGPerson.m
文件中实现这个方法
- (id)forwardingTargetForSelector:(SEL)aSelector{
NSLog(@"%s - %@",__func__,NSStringFromSelector(aSelector));
// runtime + aSelector + addMethod + imp
return [super forwardingTargetForSelector:aSelector];
}
打印结果
即使项目崩溃了,但是打印了sayHello
说明工程崩溃之前调用了forwardingTargetForSelector
方法,苹果官方文档
对forwardingTargetForSelector
的描述
大概的意思是:当消息没有人接受的时候,就返回第一接受者
既然LGPerson
类没有实现,那么我们就创建一个继承于NSObject
的新文件LGStudent
,在LGStudent
中实现sayHello
方法
- (id)forwardingTargetForSelector:(SEL)aSelector{
NSLog(@"%s - %@",__func__,NSStringFromSelector(aSelector));
// runtime + aSelector + addMethod + imp
// return [super forwardingTargetForSelector:aSelector];
return [LGStudent alloc];
}
打印结果
由此可以看出执行的是LGStudent
的sayHello
方法,这就是快速转发流程
慢速消息转发
-
methodSignatureForSelector
方法需要搭配forwardInvocation
方法使用,这就是所谓的慢速转发流程``methodSignatureForSelector
的苹果官方文档说明
LGPerson
实现的代码
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
NSLog(@"%s - %@",__func__,NSStringFromSelector(aSelector));
return nil;
}
- (void)forwardInvocation:(NSInvocation *)anInvocation{
NSLog(@"%s - %@",__func__,anInvocation);
// GM sayHello - anInvocation - 漂流瓶 - anInvocation
anInvocation.target = [LGStudent alloc];
// anInvocation 保存 - 方法
[anInvocation invoke];
}
打印结果
anInvocation
只需调用invoke
进行启动和保存
如果不调用的话就会浪费一个事物,耗费了性能
,是一种业务层面的浪费
慢速转发流程
拥有的权限更大,更加灵活