1.instrumentObjcMessageSends分析
之前我们在慢速查找中没有找到imp会进行方法决议,但是如果方法决议中还是没有处理怎么办?
我们在慢速查找中会把找到的方法进行缓存,log_and_fill_cache
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
cls->cache.insert(sel, imp, receiver);
}
复制代码
里面有一个objcMsgLogEnabled
的判断,但是bool objcMsgLogEnabled = false
。全局搜索objcMsgLogEnabled
的赋值情况找到了instrumentObjcMessageSends
里面对它进行了改变
void instrumentObjcMessageSends(BOOL flag)
{
bool enable = flag;
// Shortcut NOP
if (objcMsgLogEnabled == enable)//objcMsgLogEnabled默认false
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);//写入log日志
objcMsgLogEnabled = enable;//赋值
}
复制代码
我们要暴露出方法的缓存日志的话把instrumentObjcMessageSends
设置为yes就能得到方法的缓存日志。我们直接使用找不到这个方法,我们这个时候就要暴露出这个方法
extern void instrumentObjcMessageSends(BOOL flag);
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
KBStudent *student = [KBStudent alloc];
instrumentObjcMessageSends(YES);
[student sayHello];
instrumentObjcMessageSends(NO);
}
return 0;
}
复制代码
我们只要关注这个方法即可,调用完在进行关闭。在方法logMessageSend
说了日志的路径
snprintf (buf, sizeof(buf), "/tmp/msgSends-%d", (int) getpid ());
复制代码
得到
我们只要关注KBStudent
就好,resolveInstanceMethod
->forwardingTargetForSelector
->methodSignatureForSelector
->resolveInstanceMethod
->doesNotRecognizeSelector
这里也可以解释为社么之前resolveInstanceMethod
走了次
2.forwardingTargetForSelector分析
源码搜索没有发现介绍,只有NSObject
中实现了。我们Command + shift + 0(是数字0)
查询官方文档关于这个方法的介绍
返回一个对象,对于原对象没有实现这个消息,让这个对象试试,也就是方法重定向。实例对象A没实现这个方法,那么我让实例对象B试试,返回一个实例对象B。
#import "KBPerson.h"
@implementation KBStudent
-(id)forwardingTargetForSelector:(SEL)aSelector
{
return [KBPerson alloc];
}
@end
复制代码
2021-07-03 11:23:03.232618+0800 消息转发[19642:2342779] -[KBPerson sayHello]
复制代码
在KBPerson
中实现了sayHello
方法,没有崩溃。这就行方法快速转发.类方法一样
+ (id)forwardingTargetForSelector:(SEL)aSelector
{
return KBPerson.class;
}
复制代码
但是实际情况,我们没办法判断哪个类实现了,哪个类没有实现。我们可以创建一个类,给这个类添加未实现的方法
,统一处理。可以创建一个类,添加统一处理的类方法
和实例方法
。在NSObject
的Category
中实现forwardingTargetForSelector
的类方法和实例方法。
#import "NSObject+AvoidCrash.h"
#import "KBPerson.h"
#import "KBErrorObject.h"
#import <objc/message.h>
@implementation NSObject (AvoidCrash)
-(id)forwardingTargetForSelector:(SEL)aSelector
{
KBErrorObject * error = [KBErrorObject alloc];
IMP imp = class_getMethodImplementation(KBErrorObject.class, @selector(sayNB));
Method method = class_getInstanceMethod(KBErrorObject.class, @selector(sayNB));
const char *type = method_getTypeEncoding(method);
class_addMethod(KBErrorObject.class, aSelector, imp, type);
return error;
}
+ (id)forwardingTargetForSelector:(SEL)aSelector
{
Class metaClas = object_getClass(KBErrorObject.class);
IMP imp = class_getMethodImplementation(metaClas, @selector(say666));
Method method = class_getClassMethod(metaClas, @selector(say666));
const char *type = method_getTypeEncoding(method);
class_addMethod(metaClas, aSelector, imp, type);
return KBErrorObject.class;
}
@end
复制代码
这样在调用的时候就可以做统一处理了
方法的快速转发是苹果让我们在动态决议没有实现的时候,让我们换个对象来处理这个消息。替代者去实现
。
2.methodSignatureForSelector分析
接着之前崩溃日志,如果快速转发没有实现的话,会调用methodSignatureForSelector
。
此方法也用于必须创建NSInvocation
对象的情况,没有实现对应的方法时候我们重写此方法以返回适当的方法签名。可以不对这个方法进行处理,值得注意的是要解决问题还要依赖forwardInvocation
的实现。
我们在NSObject
的Category
中实现签名
和forwardInvocation
-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
NSMethodSignature * signature = [NSMethodSignature signatureWithObjCTypes:"v@:*"];
return signature;
}
-(void)forwardInvocation:(NSInvocation *)anInvocation
{
NSLog(@"%s",__func__);
}
复制代码
没有实现消息转发
,也没有发送崩溃
。相当于告诉系统这个没实现的方法我知道了,不用处理
了。但是如果想处理也是可以的。
-(void)forwardInvocation:(NSInvocation *)anInvocation
{
anInvocation.target = [KBPerson alloc];
[anInvocation invoke];
}
复制代码
2021-07-03 15:03:40.337782+0800 消息转发[20554:2455376] -[KBPerson sayHello]
复制代码
相当于告诉说这个方法让KBPerson
实例对象去实现,和快速转发
一样进行重定向
,找别人
来实现。类方法
也是一样。
3.Hopper分析
这个是通过源码分析出方法崩溃流程,我们下面通过Hopper分析一下
我们把CoreFoundation
拖入Hopper
搜索forward
。选择__forwarding__
后查看伪代码实现
在动态决议失败后进入快速转发
。会进行系统的loc_64a67
进入慢速转发流程
判断methodSignatureForSelector
方法签名是否实现。实现了,进行forwardStackInvocation:
但是在重写的时候没有响应
,应该是系统的方法。继续查看forwardInvocation:
是否实现(转发求助,进行方法重定向)。没有实现最终调用doesNotRecognizeSelector:
报错。
3.1关于动态决议在methodSignatureForSelector之后再走一次分析
在methodSignatureForSelector
后又走了一次动态决议,我们用Hopper
分析一下
点击methodSignatureForSelector
选择实例方法
得到
查看___methodDescriptionForSelector
具体实现
如果没有实现跳转loc_7c68b
得到
在源码搜索
Method class_getInstanceMethod(Class cls, SEL sel)
{
if (!cls || !sel) return nil;
// This deliberately avoids +initialize because it historically did so.
// This implementation is a bit weird because it's the only place that
// wants a Method instead of an IMP.
#warning fixme build and search caches
// Search method lists, try method resolver, etc.
lookUpImpOrForward(nil, sel, cls, LOOKUP_RESOLVER);
#warning fixme build and search caches
return _class_getMethod(cls, sel);
}
复制代码
会进行慢速查找流程lookUpImpOrForward
,这就是后面再次进行动态决议的原因。
4.总结
- 在慢速查找没有找到会进行方法的
动态决议
,resolveInstanceMethod
或resolveClassMethod
,就是给当前方法重新赋值一个实现的imp。 - 动态决议失败后会进行消息的
快速转发forwardingTargetForSelector
,就是让别的对象或者类来实现这个方法,也就是方法重定向
。 - 快速转发失败进入
慢速转发
。首先methodSignatureForSelector
给方法重新签名,必须要重写forwardInvocation
。如果实现这个方法就在forwardInvocation
中进行重定向。 - 最后还是没有实现,系统
CoreFoundation
会调用class_getInstanceMethod
进行一次方法动态决议。 - 消息的转发
流程图
如下