经历的九、objc_msgSend流程分析快速查找和十、objc_msgSend流程分析慢速查找都未找到方法的IMP, 则会进行消息转发, 苹果为我们提供了几个方法供我们在崩溃之前再次对消息进行处理
1.方法动态决议(会执行两次)
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
...
if (slowpath(behavior & LOOKUP_RESOLVER)) {
behavior ^= LOOKUP_RESOLVER;
return resolveMethod_locked(inst, sel, cls, behavior);
}
....
1.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()) {
// 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(inst, sel, cls);
当前类是元类则 调用
resolveClassMethod(inst, sel, cls);
if (!lookUpImpOrNil(inst, sel, cls)) {
resolveInstanceMethod(inst, sel, cls);
}
static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{
runtimeLock.assertUnlocked();
ASSERT(cls->isRealized());
SEL resolve_sel = @selector(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);
// 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);
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));
}
}
}
2. 消息转发-消息接受者重定向 forwardingTargetForSelector
在看这个方法之前, 我们会疑问为什么是这个方法而不是别的?
我们借助一个打印函数来查看方法的调用
在IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
的 done:
中我们可以看到这个方法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);
}
结合上面克制是否打印的控制条件在于objcMsgLogEnabled
, 而objcMsgLogEnabled
又由instrumentObjcMessageSends()
传入的参数控制, 所以我们可以直接在外部使用这个函数来控制是否打印
extern void instrumentObjcMessageSends(BOOL flag);
int main(int argc, const char * argv[]) {
@autoreleasepool {
LGPerson *person = [LGPerson alloc];
instrumentObjcMessageSends(YES);
[person sayHello];
instrumentObjcMessageSends(NO);
NSLog(@"Hello, World!");
}
return 0;
}
打印日志在方法中可以看到
我们在Finder 中直接搜索 (cmd + shift +g )
/tmp
可以看到目录中多了一个文件
感觉明显的可以,删掉这个日志重新跑一边程序,打开看下日志内容
我们可以借助官方文档来查看下这个方法 在xcode中 cmd + shift +0(零)
大致的意思就是
如果一个对象实现(或继承)这个方法,并返回一个非nil(和非self)结果,那么返回的对象将用作新的接收者对象,消息分派将继续到这个新对象。(显然,如果从这个方法返回self,代码将陷入无限循环。)
如果你在一个非根类中实现这个方法,如果你的类对于给定的选择器没有返回任何东西,那么你应该返回调用super的实现的结果。
此方法让对象有机会重定向发送给它的未知消息,然后再由开销大得多的forwardInvocation: machinery接管。当您只是想将消息重定向到另一个对象时,这是非常有用的,并且可能比常规转发快一个数量级。如果转发的目标是捕获NSInvocation,或者在转发过程中操纵参数或返回值,那么它就没有用了。
LGPerson中没有实现sayHello, LGStudent中实现了, 我们在LGPerson中重写-forwardingTargetForSelector
方法, 并返回一个LGStudent对象, 程序没有崩溃, 日志中可以看出是调用的[LGStudent sayHello]
我们可以利用这一点在APP中做crash
的防护, 下面的代码是我在APP中添加防护分类,需要的小伙伴可以借鉴改进
// 自定义实现 `-kb_forwardingTargetForSelector:` 方法, 和`+ (id)kb_forwardingTargetForSelector:(SEL)aSelector`
- (id)kb_forwardingTargetForSelector:(SEL)aSelector {
SEL forwarding_sel = @selector(forwardingTargetForSelector:);
// 获取 NSObject 的消息转发方法
Method root_forwarding_method = class_getInstanceMethod([NSObject class], forwarding_sel);
// 获取 当前类 的消息转发方法
Method current_forwarding_method = class_getInstanceMethod([self class], forwarding_sel);
// 判断当前类本身是否实现第二步:消息接受者重定向
BOOL realize = method_getImplementation(current_forwarding_method) != method_getImplementation(root_forwarding_method);
// 如果没有实现第二步:消息接受者重定向
if (!realize) {
// 判断有没有实现第三步:消息重定向
SEL methodSignature_sel = @selector(methodSignatureForSelector:);
Method root_methodSignature_method = class_getInstanceMethod([NSObject class], methodSignature_sel);
Method current_methodSignature_method = class_getInstanceMethod([self class], methodSignature_sel);
realize = method_getImplementation(current_methodSignature_method) != method_getImplementation(root_methodSignature_method);
// 如果没有实现第三步:消息重定向
if (!realize) {
// 创建一个新类
NSString *errClassName = NSStringFromClass([self class]);
NSString *errSel = NSStringFromSelector(aSelector);
NSLog(@"*** Crash Message: -[%@ %@]: unrecognized selector sent to instance %p ***",errClassName, errSel, self);
NSString *className = @"CrachClass";
Class cls = NSClassFromString(className);
// 如果类不存在 动态创建一个类
if (!cls) {
Class superClsss = [NSObject class];
cls = objc_allocateClassPair(superClsss, className.UTF8String, 0);
// 注册类
objc_registerClassPair(cls);
}
// 如果类没有对应的方法,则动态添加一个
if (!class_getInstanceMethod(NSClassFromString(className), aSelector)) {
class_addMethod(cls, aSelector, (IMP)Crash, "@@:@");
}
// 把消息转发到当前动态生成类的实例对象上
return [[cls alloc] init];
}
}
return [self kb_forwardingTargetForSelector:aSelector];
}
如果我们也没有重写这个方法, 或者没有返回一个对象呢? 上面的tmp/msgsend日志和官方文档中都指出了会调用forwardInvocation
方法, 接下来我们看下 forwardInvocation
3. forwardInvocation 消息重定向
首先看下官方文档
大致意思就是
当向对象发送没有对应方法的消息时,运行时系统给接收方一个机会将消息委托给另一个接收方。它通过创建一个表示消息的
NSInvocation对象
并向接收者
发送一个包含这个NSInvocation对象
作为参数
的forwardInvocation:
消息来委托消息
。然后,接收方的forwardInvocation:
方法可以选择将消息转发
到另一个对象
。(如果该对象也不能
响应消息,那么它也
将获得一个转发消息的机会
。)
因此,forwardInvocation: message
允许对象与其他对象建立关系,对于某些消息,这些对象将代表它行事。在某种意义上,转发对象能够“继承”
它所转发消息
的对象的某些特征
。
还有重要的一点
为了响应对象本身不能识别的方法,您必须重写
methodSignatureForSelector:
和forwardInvocation:
。转发消息的机制使用methodSignatureForSelector:
获得的信息来创建要转发的NSInvocation
对象。重写方法必须
为给定的选择器
提供适当的方法签名
,可以通过预先构造
一个选择器
,也可以通过向另一个对象
请求一个选择器
。
看下代码
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
NSLog(@"%s - %@",__func__,NSStringFromSelector(aSelector));
return [NSMethodSignature signatureWithObjCTypes:"v@:*"];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation{
NSLog(@"%s - %@",__func__,anInvocation);
anInvocation.target = [LGStudent alloc];
// anInvocation 保存 - 方法
[anInvocation invoke];
}
打印日志
结果: 运行正常无崩溃