上篇文章我们探索了消息发送机制,包括快速查找和慢速查找,如果上述两种方式都没有找到应该怎么办?下面我们来探索一下
消息的动态决议
找到源码 lookUpImpOrForward 的实现,在父类查找方法完毕后如果还没找到 imp, 并且 method resolver 不起作用,就会把 forward_imp 赋值给 imp:
先追踪一下
forward_imp,发现底层会抛出异常:
forward_imp 就已经找到底了,通过 objc_defaultForwardHandler 就会抛出未找到方法实现的异常,可以发现在底层是没有元类的概念的。再继续往 lookUpImpOrForward 下面看:
resolveMethod_locked 里面就是消息的动态决议和消息转发,并且这个方法只会执行一次,可以进行简单验证,声明一个方法不写实现:
进入
resolveMethod_locked 发现 resolveInstanceMethod 和 resolveClassMethod,这两个分别是实例方法和类方法的动态决议
下面通过代码验证一下,在类里面添加这两个方法,看看如果其他方法没有实现会不会走这边:
+ (BOOL)resolveInstanceMethod:(SEL)sel {
NSLog(@"%s--%@", __func__, NSStringFromSelector(sel));
return [super resolveInstanceMethod:sel];
}
+ (BOOL)resolveClassMethod:(SEL)sel {
NSLog(@"%s--%@", __func__, NSStringFromSelector(sel));
return [super resolveClassMethod:sel];
}
分别调用未实现的实例方法和类方法,会发现崩溃:
可以发现调用未实现的方法时,系统会调用
resolveInstanceMethod 两次,我们可以用 runtime 的 API 动态地给这个 sel 添加 imp, 这样就不会崩溃:
+ (BOOL)resolveInstanceMethod:(SEL)sel {
NSLog(@"%s--%@", __func__, NSStringFromSelector(sel));
if (sel == @selector(getName)) {
IMP imp = class_getMethodImplementation(self.class, @selector(getName1));
class_addMethod(self.class, sel, imp, "v@:");
return YES;
}
return [super resolveInstanceMethod:sel];
}
- (void)getName1 {
NSLog(@"%s", __func__);
}
+ (BOOL)resolveClassMethod:(SEL)sel {
NSLog(@"%s--%@", __func__, NSStringFromSelector(sel));
if (sel == @selector(classMethod)) {
IMP imp = class_getMethodImplementation(self.class, @selector(classMethod1));
class_addMethod(objc_getMetaClass("User"), sel, imp, "v@:");
return YES;
}
return [super resolveClassMethod:sel];
}
- (void)classMethod1 {
NSLog(@"%s", __func__);
}
"v@:" 在类的底层探索(下)里面讲过,是方法的返回值
再来探索一下 resolveInstanceMethod 的源码,发现动态地添加 imp 后,会调用 lookUpImpOrNilTryCache 从 cache 中查找 imp, 那么我们来验证一下 cache 中是否存在 imp:
发现确实在缓存里,就是说使用
runtime 的 API 动态添加方法时,是存在 cache 里的
注意:要打两个断点,因为 lldb 命令会产生 class 和 responseToSelector 方法,从而造成 cache 扩容,导致 getName 方法丢掉,所以应该是先扩容,再执行 getName 方法
resolveClassMethod 实质
现在对代码做如下调整:
打印结果如下:
在
class_getMethodImplementation 里面会调用 lookUpImpOrForward,lookUpImpOrForward 里面调用的 resolveMethod_locked 会判断,如果传入的 cls 不是元类,就会调用 resolveInstanceMethod
再进行如下改动:
会发现进入死循环,综合上述分析,在进入
class_getMethodImplementation 后再调用 resolveMethod_locked 时,传入的 cls 是元类,会一直调用 resolveClassMethod
消息转发
在 lookUpImpOrForward 中找到 imp 后会跳到 done 调用 log_and_fill_cache 进行方法缓存
其中
implementer 肯定是 true, 也就是说在调用 cache.insert() 方法缓存方法之前,如果 objcMsgLogEnabled 为 true,就会产生日志文件,文件位置如下:
下面探索一下如何给
objcMsgLogEnabled 赋值:
可以发现
objcMsgLogEnabled 初始值为 false, 而且只有在 instrumentObjcMessageSends 方法中会对其赋值,可以使用 extern 关键字来调用这个函数来给 objcMsgLogEnabled 赋值,并且产生日志:
我们发现了在消息的动态决议
resolveInstanceMethod 和报错 doesNotRecognizeSelector 之间有 forwardingTargetForSelector、methodSignatureForSelector, 它们分别对应消息的快速转发和慢速转发
消息的快速转发
我们在 User.m 里重写 forwardingTargetForSelector 这个方法:
- (id)forwardingTargetForSelector:(SEL)aSelector {
NSLog(@"%s", __func__);
return nil;
}
发现在系统未找到方法实现之前确实调用了 forwardingTargetForSelector:
同样我们可以在这个方法里面做一些处理,当前类不能相应某些方法时,可以把方法转发给能响应此方法的类,比如再创建一个
CrashDefender 类,里面去实现 User 里未实现的方法:
@implementation User
- (id)forwardingTargetForSelector:(SEL)aSelector {
NSLog(@"%s", __func__);
if (aSelector == @selector(getName)) {
return [CrashDefender new];
}
return nil;
}
@end
@implementation CrashDefender
- (void)getName {
NSLog(@"%s", __func__);
}
@end
此时再调用 User 里面的 getName 方法就不会崩溃了,因为 getName 已经转发给了 CrashDefender 类来处理。同时 getName 方法缓存在 CrashDefender 的 cache 里,因为 forwardingTargetForSelector 返回了 CrashDefender 的实例,是这个实例在调用方法。
我们可以通过这个方法来把未处理的消息转发给一个单独的类,这个单独的类就可以去统一处理那些其他类里面没有处理的消息,也可以通过这个
forwardingTargetForSelector方法去进行那些方法找不到的 crash 的收集等。这是对实例对象的方法的快速转发,对于类方法来说也有对应的消息转发的类方法+ (id)forwardingTargetForSelector:(SEL)aSelector,原理和- (id)forwardingTargetForSelector:(SEL)aSelector方法一样
消息的慢速转发
如果在 forwardingTargetForSelector 没有进行处理,那么系统会调用消息的慢速转发:methodSignatureForSelector, 同样在 User.m 里重写该方法, 并调用 getName:
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
NSLog(@"%s", __func__);
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
注意的是
methodSignatureForSelector 需要和 forwardInvocation 一起使用,methodSignatureForSelector 只是为方法提供了一个有效签名,提供了有效签名之后,系统就会调用 forwardInvocation 来处理这个签名。在 User.m 中使用 forwardInvocation 来避免崩溃:
同样可以在
forwardInvocation 中用其他的类来处理本类相应不了的方法,可以先看一下 NSInvocation 的属性,里面提供了消息接收者 target 和方法名 selector:
通过上面两个参数就可以用
forwardInvocation 这样处理:
- (void)forwardInvocation:(NSInvocation *)anInvocation {
CrashDefender *defender = [CrashDefender new];
if ([self respondsToSelector:anInvocation.selector]) {
// 本身可以响应 自己响应
[anInvocation invokeWithTarget:self];
} else if ([defender respondsToSelector:anInvocation.selector]) {
// 本身不可以响应 但 CrashDefender 可以
[anInvocation invokeWithTarget:defender];
} else {
NSLog(@"该功能正在开发中,敬请期待");
}
}
总结
消息的动态决议 resolveInstanceMethod, 消息的快速查找 forwardingTargetForSelector, 消息的慢速查找 methodSignatureForSelector 是系统提供给我们防止程序因找不到方法而崩溃的“三根救命稻草”,相比于其他两种,消息的慢速查找 methodSignatureForMethod 更为灵活,可以在 forwardInvocation 中什么事也不做。结合上篇的消息派发机制 objc_msgSend, 可以总结消息的发送流程如下:
注意: 一般在开发中不会这样做,可以在项目上预发时,添加一个类别进行无侵入处理
拓展
-
所有的类都继承自
NSObject, 可以在NSObject中实现resolveInstanceMethod, 为什么要设计resolveClassMethod?如果没有
resolveClassMethod这个动态决议方法,系统在查找类方法的时候会走resolveInstanceMethod这个动态决议方法,这样的查找流程就会比较的长,会影响系统查找的效率。基于这个原因系统提供了resolveClassMethod方法来给类方法动态添加方法的实现,用来简化类方法的查找流程而提供给我们去实现的一个方法而已。系统其实最终都需要通过resolveInstanceMethod方法来进行方法的动态决议。 -
没有找到方法时,为什么会走两次
resolveInstanceMethodorresolveClassMethod?第一次进入的方法调用链:
lookUpImpOrForward -> resolveMethod_locked -> resolveInstanceMethod, 这个很好理解, 就是之前分析的那样第二次进入时,通过
bt打印一下堆栈信息:发现第一次执行结束后会进入
___forwarding___函数中,然后再调用methodSignatureForSelector方法,然后再调用class_getInstanceMethod方法,源码查看该方法:发现该方法内部会调用
lookUpImpOrForward, 从而再次调用resolveInstanceMethod方法。resolveClassMethod也是如此 -
AOP 设计模式
AOP(Aspect Oriented Programming): 面向切面编程,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。 基于上述原理可以做以下两件事:
- 收集找不到方法的崩溃信息(无侵入),创建 NSObject 的一个类别,统一处理方法找不到的崩溃:
@interface NSObject (CrashCollector)
@end
#import <objc/runtime.h>
@implementation NSObject (CrashCollector)
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
NSLog(@"%s", __func__);
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
NSLog(@"%s", __func__);
}
@end
- 无侵入统计每个页面停留时间埋点:
#import <objc/runtime.h>
@implementation UIViewController (TimeAnalysis)
+ (void)load {
static dispatch_once_t onceT;
dispatch_once(&onceT, ^{
Method viewWillAppear = class_getInstanceMethod(self.class, @selector(viewWillAppear:));
Method aopViewWillAppear = class_getInstanceMethod(self.class, @selector(aopViewWillAppear));
method_exchangeImplementations(viewWillAppear, aopViewWillAppear);
Method viewWillDisAppear = class_getInstanceMethod(self.class, @selector(viewWillDisappear:));
Method aopViewWillDisAppear = class_getInstanceMethod(self.class, @selector(aopViewWillDisAppear));
method_exchangeImplementations(viewWillDisAppear, aopViewWillDisAppear);
});
}
- (void)aopViewWillAppear {
NSLog(@"enter %@", self.class);
// 方法已经交换,不会造成死循环
[self aopViewWillAppear];
}
- (void)aopViewWillDisAppear {
NSLog(@"leave %@", self.class);
[self aopViewWillDisAppear];
}
@end