前言
相关文章
iOS底层探索九(方法的本质下objc_msgSend慢速及方法转发初探)
相关代码:
objc4_752源码 消息转发
上篇文章讲述了方法的查找流程进行了详细的探索,并对方法的转发进行了初步探索,这篇文章,我们对消息的转发流程进行详细的探索。
消息转发
上篇文章我们介绍了,如果在OC中如果调用一个没有实现的方法,在方法查找过程中因为找不到会造成崩溃,但是苹果大大给我们提供了另一个解决思路,就是在动态解析(resolveMethod)的过程中有进行消息转发,并且我们在上篇文章中也实现了初步的防止崩溃方法:
static void resolveMethod(Class cls, SEL sel, id inst)
{
runtimeLock.assertUnlocked();
assert(cls->isRealized());
if (! cls->isMetaClass()) {//不是元类
//判断类不是元类,那sel就是实例方法,那就先转发resolveInstanceMethod方法,判断有没有实现resolveInstanceMethod,没实现就不做处理
// try [cls resolveInstanceMethod:sel]
resolveInstanceMethod(cls, sel, inst);
}
else {
// try [nonMetaClass resolveClassMethod:sel]
// and [cls resolveInstanceMethod:sel]
//先转发resolveClassMethod,会先查找下resolveClassMethod,如果没实现就不做处理
resolveClassMethod(cls, sel, inst);
//再次查找下方法,如果没有的话,就再转发一下resolveInstanceMethod方法
if (!lookUpImpOrNil(cls, sel, inst,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/))
{
resolveInstanceMethod(cls, sel, inst);
}
}
}
实例方法动态消息决议
我们在main函数中调用teacher 中一个没有实现的方法;
int main(int argc, const char * argv[]) {
@autoreleasepool {
#pragma clang diagnostic push
// // 让编译器忽略错误,不然调用没有的方法,编译器编译完语法分析会报错,导致不能运行
#pragma clang diagnostic ignored "-Wundeclared-selector"
XZTeacher *teacher = [[XZTeacher alloc] init];
// 消息转发流程这个方法.h中之声明,不实现
[teacher saySomthing];
#pragma clang diagnostic pop
}
return 0;
}
因为调用的是实力方法,我们来查看resolveInstanceMethod 方法,这里传入3个参数 cls 类,sel 方法编号, inst 对象
static void resolveInstanceMethod(Class cls, SEL sel, id inst)
{
runtimeLock.assertUnlocked();
assert(cls->isRealized());
//查找系统方法中是否有实现resolveInstanceMethod这个方法,这里会根据继承链进行查找
if (! lookUpImpOrNil(cls->ISA(), SEL_resolveInstanceMethod, cls,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/))
{
// Resolver not implemented.
return;
}
//发送SEL_resolveInstanceMethod消息,系统给你一次机会-->你是不是要针对这个没有实现的sel进行操作一下
BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
//发送消息SEL_resolveInstanceMethod 传参为我们的方法编号
//崩溃的方法不是这个方法,说明再NSObject方法中有实现这个方法
bool resolved = msg(cls, SEL_resolveInstanceMethod, sel);
// Cache the result (good or bad) so the resolver doesn't fire next time.
// +resolveInstanceMethod adds to self a.k.a. cls
//再次查找类中是否sel方法,因为resolveInstanceMethod方法执行后可能动态进行添加了,resolver是不要进行消息转发了
IMP imp = lookUpImpOrNil(cls, sel, inst,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/);
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));
}
}
}
我们可以看到首先lookUpImpOrNil查找类中方法中是否有实现resolveInstanceMethod这个方法,这里会根据继承链进行查找,因为是根据继承链查找,也就是说我们类中如果没有实现的话,就需要查找系统类,如果都找不到,这里就直接return,
我们可以在类中查找到这个方法(resolveInstanceMethod),有这个方法后,代码继续执行,然后使用
msg(cls, SEL_resolveInstanceMethod, sel),方法给resolveInstanceMethod发送消息从而继续lookUpImpOrNil查询方法IMP有没有实现,所以我们可以在我们自己的类中实现resolveInstanceMethod方法并对IMP进行绑定,所以在teacher中实现+resolveInstanceMethod 方法,并对未实现的方法进行IMP实现及绑定,使得,本身调用saySomthing 方法,经过IMP绑定后,调用了SayHello方法。从而防止了崩溃,
#import <objc/message.h>
- (void)sayHello{
NSLog(@"%s",__func__);
}
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
if (sel == @selector(saySomthing)) {
NSLog(@"进入方法了");
IMP saySomeIMP = class_getMethodImplementation(self, @selector(sayHello));
Method sayMethod = class_getInstanceMethod(self, @selector(sayHello));
const char *sayType = method_getTypeEncoding(sayMethod);
return class_addMethod(self, sel, saySomeIMP, sayType);
}
return [super resolveInstanceMethod:sel];
}
打印结果:
根据打印结果,我们可以看出,系统调用我们在XZTeacher 类中的resolveInstanceMethod方法,并根据我们的绑定,执行了sayHello方法。
实例方法我们看到了可以这么处理,那么类方法呢,我们也来看一下,我们在Main 函数中调用[XZTeacher sayLove],方法,sayLove 方法在XZTeacher类中只声明不实现,运行崩溃,这是肯定的就不进行演示了,这里根据源码,我们可以看到会进入resolveMethod方法,并进入resolveClassMethod方法;
static void resolveClassMethod(Class cls, SEL sel, id inst)
{
runtimeLock.assertUnlocked();
assert(cls->isRealized());
assert(cls->isMetaClass());
//查找下类是否实现了resolveClassMethod方法,NSObject类已经实现了
if (! lookUpImpOrNil(cls, SEL_resolveClassMethod, inst,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/))
{
// 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;
//切记,此处是向元类发送resolveClassMethod消息,也就是调用resolveClassMethod方法
bool resolved = msg(nonmeta, SEL_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(cls, sel, inst,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/);
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));
}
}
}
这里我们需要注意的是传入的参数cls 为元类,sel方法编号,inst为类;我们可以看到这里和resolveInstanceMethod方法中的逻辑基本一样,不过这里检查的系统方法为resolveClassMethod方法,这个方法我们可以看到上面的截图里面在NSObject类中也是有实现这个方法的;
类方法动态消息决议
我们在main函数中调用一个类方法 [XZTeacher sayLove] 在XZTeacher类中实现resolveClassMethod方法并运行程序:
可以看到确实来了resolveClassMethod方法,所以我们可以在这个方法进行类似处理来进行防止崩溃;在方法内加入代码:
+(BOOL)resolveClassMethod:(SEL)sel
{
NSLog(@"来了:resolveClassMethod");
if (sel == @selector(sayLove)) {
NSLog(@"进入方法了sayLove");
IMP sayObjIMP = class_getMethodImplementation(objc_getMetaClass("XZTeacher"), @selector(sayObjc));
Method sayObjMethod = class_getClassMethod(objc_getMetaClass("XZTeacher"), @selector(sayObjc));
const char *sayObjType = method_getTypeEncoding(sayObjMethod);
// 类方法在元类 objc_getMetaClass("XZTeacher")
BOOL isAdd = class_addMethod(objc_getMetaClass("XZTeacher"), sel, sayObjIMP, sayObjType);
return isAdd;
}
return [super resolveClassMethod:sel];
}
运行程序,查看运行结果,可以看出方法正常运行
在resolveMethod方法中类方法在调用resolveClassMethod完成后还进行了一次调用
if (!lookUpImpOrNil(cls, sel, inst,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/))
{
resolveInstanceMethod(cls, sel, inst);
}
IMP lookUpImpOrNil(Class cls, SEL sel, id inst,
bool initialize, bool cache, bool resolver)
{
IMP imp = lookUpImpOrForward(cls, sel, inst, initialize, cache, resolver);
if (imp == _objc_msgForward_impcache) return nil;
else return imp;
}
在这里我们可以看出,因为这里是的cls是元类,所以在元类中lookUpImpOrNil查找sayLove方法肯定也是找不到的,这里就会调用resolveInstanceMethod,那我们是不是可以考虑将所有的处理都放到这个方法中呢resolveInstanceMethod,尝试将resolveInstanceMethod重写,并调用sayLove方法
可以看出还是只来了resolveClassMethod方法,这是我们考虑,因为是在元类职工查找resolveInstanceMethod方法,所以找不到,这个时候会想到,查找方法就是根据继承链来进行查找方法的,也就是如下图:
根据继承链关系,可以看到根元类也是继承与NSObject类,那就可以在NSObject类中只处理resolveInstanceMethod方法,先写上这个方法添加处理方法:
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
NSLog(@"来了,%s",__func__);
if (sel == @selector(saySomthing)) {
NSLog(@"进入方法了saySomthing");
IMP saySomeIMP = class_getMethodImplementation(self, @selector(sayMaster));
Method sayMethod = class_getInstanceMethod(self, @selector(sayMaster));
const char *sayType = method_getTypeEncoding(sayMethod);
return class_addMethod(self, sel, saySomeIMP, sayType);
}
if (sel == @selector(sayLove)) {
NSLog(@"进入方法了sayLove");
IMP sayObjIMP = class_getMethodImplementation(objc_getMetaClass("NSObject"), @selector(sayEasy));
Method sayObjMethod = class_getClassMethod(objc_getMetaClass("NSObject"), @selector(sayEasy));
const char *sayObjType = method_getTypeEncoding(sayObjMethod);
// 类方法在元类 objc_getMetaClass("XZTeacher")
return class_addMethod(objc_getMetaClass("NSObject"), sel, sayObjIMP, sayObjType);;
}
// [super resolveInstanceMethod:sel],因为这里是根类不能调用super了,直接返回NO就行
return NO;
}
运行查看下运行结果
调用类方法sayLove方法可以调用到SayEasy方法,调用实例方法saySomething根据IMP转换调用了SayMaster方法;说民这里处理确实可以一劳永逸
崩溃优化策略
-
可以在自己的项目工程中每个模块进行命名规范,例如首页的所有方法都以home_开头,我的都有me_开头
-
实现一个NSObject分类,进行bug替换,当检测到是home_开始,没有方法时,直接跳转到首页,并上报服务器,这样的简单操作就可以实现一个优化策略了。
这个策略可以发小是有漏洞的,如果在上层类中实现了resolveInstanceMethod方法那么实例方法在防止的时候就不会走到NSObject类中了,可能就会导致上报不全,等问题。所以我们再进行消息处理的时候,一般会放到消息转发的最后一步,那么我们继续来看消息转发中的流程,在源码中我们可以看到resolveMethod调用之后就是cache_fill 就会导致崩溃了,那么中件流程我们根本没法探索,这时我们需要回头看一下方法缓存;
消息转发流程探索
根据查找到imp后会进行缓存调用方法为log_and_fill_cache,看到log说明应该是有日志产生的,进入方法进行查看
log_and_fill_cache(Class cls, IMP imp, SEL sel, id receiver, Class implementer)
{
#if SUPPORT_MESSAGE_LOGGING
//如果有这个objcMsgLogEnabled变量就会写入日志
if (objcMsgLogEnabled) {
bool cacheIt = logMessageSend(implementer->isMetaClass(),
cls->nameForLogging(),
implementer->nameForLogging(),
sel);
if (!cacheIt) return;
}
#endif
// 写入缓存
cache_fill (cls, sel, imp, receiver);
}
需要一个参数为objcMsgLogEnabled为true时调用logMessageSend 方法进行日志书写,继续进入
bool objcMsgLogEnabled = false;
static int objcMsgLogFD = -1;
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-编号"的目录下总结下来就是需要2个参数objcMsgLogEnabled为true 可以看到函数上面默认为false ,还有objcMsgLogFD<0 函数上这个值默认为-1,所以就只需要看objcMsgLogEnabled为true即可;搜索一下这个函数的赋值方法
OBJC_EXPORT void
instrumentObjcMessageSends(BOOL flag)
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
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;
}
找到这个方法我们需要在外部声明一下这个函数来看一下在main文件中这枚写下
#import "XZPerson.h"
#import "XZTeacher.h"
#import "NSObject+XZ.h"
#import <objc/runtime.h>
extern void instrumentObjcMessageSends(BOOL flag);
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
XZTeacher *teacher = [XZTeacher alloc];
// 方法调用日志打开
instrumentObjcMessageSends(true);
[teacher saySomthing];
// [XZTeacher sayLove];
instrumentObjcMessageSends(false);
}
return 0;
}
将之前处理崩溃的都先注释掉,然后调用日志打开,运行并查看/tmp/路径下以msgSends开头的文件进行查看
+ XZTeacher NSObject resolveInstanceMethod:
+ XZTeacher NSObject resolveInstanceMethod:
- XZTeacher NSObject forwardingTargetForSelector:
- XZTeacher NSObject forwardingTargetForSelector:
- XZTeacher NSObject methodSignatureForSelector:
- XZTeacher NSObject methodSignatureForSelector:
- XZTeacher NSObject class
+ XZTeacher NSObject resolveInstanceMethod:
+ XZTeacher NSObject resolveInstanceMethod:
- XZTeacher NSObject doesNotRecognizeSelector:
- XZTeacher NSObject doesNotRecognizeSelector:
- XZTeacher NSObject class
- OS_xpc_serializer OS_xpc_object dealloc
- OS_object NSObject dealloc
+ OS_xpc_payload NSObject class
。。。根据名称可以判断下面都是系统方法
可以看到都是调用了2次分别为resolveInstanceMethod,forwardingTargetForSelector,methodSignatureForSelector方法,这个方法resolveInstanceMethod已经研究过了,下面我们对后面2个方法研究一下
forwardingTargetForSelector 方法研究
点击xcode 工具中的help 搜索
自己没有处理的方法,会使用快速转发到forwardingTargetForSelector方法中,使用这个方法,返回一个对象,让别人进行处理这个方法,那么我们新定义一个类XZStudent类,在这个类实现saySomething 和sayLove 方法并实现
#import "XZStudent.h"
@implementation XZStudent
- (void)saySomthing
{
NSLog(@"%s",__func__);
}
+ (void)sayLove{
NSLog(@"%s",__func__);
}
@end
在XZTeacher类中实现forwardingTargetForSelector方法
//实例方法调用这个转发
- (id)forwardingTargetForSelector:(SEL)aSelector
{
if (aSelector == @selector(saySomthing)) {
return [XZStudent alloc];
}
return [super forwardingTargetForSelector:aSelector];
}
//类方法调用这个转发
+ (id)forwardingTargetForSelector:(SEL)aSelector
{
if (aSelector == @selector(sayLove)) {
return [XZStudent class];
}
return [super forwardingTargetForSelector:aSelector];
}
并在main中调用saySomething方法查看打印效果
正常打印,可以看出这样也是能完美解决崩溃
methodSignatureForSelector方法研究
根据xcode中的help 搜索,methodSignatureForSelector搜索
这个方法methodSignatureForSelector 方法关联使用的就是forwardInvocation方法,将之前的处理先去掉,在XZTeacher类中加入这个最新处理:
//方法签名的下层慢速处理,返回一个方法签名 但是只有这个方法肯定是不够的
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
NSLog(@"%s -- %@",__func__,NSStringFromSelector(aSelector));
if (aSelector == @selector(saySomthing)) {
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return [super methodSignatureForSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation{
NSLog(@"%s",__func__);
}
这里的方法签名是有固定格式的,标示不同的类型具体可查看官方文档
查看一下运行结果:
可以正常执行到方法,而且不崩溃了 ,但是也没有对这个方法进行处理,我们继续查看文档中的这个方法
看到可以这么进行处理,
- (void)forwardInvocation:(NSInvocation *)invocation
{
SEL aSelector = [invocation selector];
if ([friend respondsToSelector:aSelector])
[invocation invokeWithTarget:friend];
else
[super forwardInvocation:invocation];
}
按照官方文档我们进行处理
if ([[XZStudent alloc] respondsToSelector:aSelector])
[anInvocation invokeWithTarget:[XZStudent alloc]];
else
[super forwardInvocation:anInvocation];
运行查看结果:
也可以正常运行,同样的这个如果是类方法处理方式和实例方法处理方式不同,如下代码:
//方法签名的下层慢速处理,返回一个方法签名 但是只有这个方法肯定是不够的
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
NSLog(@"%s -- %@",__func__,NSStringFromSelector(aSelector));
if (aSelector == @selector(saySomthing)) {
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return [super methodSignatureForSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation{
NSLog(@"%s",__func__);
// 在这里进行处理方法,如果处理的话就可以处理,不处理就会失效
SEL aSelector = [anInvocation selector];
// 查看XZStudent 对象能否进行处理,如果能,就交给他处理不能就不处理了
if ([[XZStudent alloc] respondsToSelector:aSelector])
[anInvocation invokeWithTarget:[XZStudent alloc]];
else
[super forwardInvocation:anInvocation];
}
//类方法处理
+ (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
NSLog(@"%s -- %@",__func__,NSStringFromSelector(aSelector));
if (aSelector == @selector(sayLove)) {
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return [super methodSignatureForSelector:aSelector];
}
+ (void)forwardInvocation:(NSInvocation *)anInvocation{
NSLog(@"%s",__func__);
// 在这里进行处理方法,如果处理的话就可以处理,不处理就会失效
SEL aSelector = [anInvocation selector];
// 查看XZStudent 类能否进行处理,如果能,就交给他处理不能就不处理了
if ([[XZStudent class] respondsToSelector:aSelector])
[anInvocation invokeWithTarget:[XZStudent class]];
else
[super forwardInvocation:anInvocation];
}
如果这几个方法都没有实现的话,系统就会调用 doesNotRecognizeSelector崩溃信息出来
+ (void)doesNotRecognizeSelector:(SEL)sel {
_objc_fatal("+[%s %s]: unrecognized selector sent to instance %p",
class_getName(self), sel_getName(sel), self);
}
总结
这篇文章我们就对方法的本质进行了完整的探索:
-
方法的底层就是调用objc_msgSend 方法
-
进行汇编在缓存中快速查找
-
调用_class_lookupMethodAndCache3进行慢速查找
-
找不到方法后进行动态解析
4.1 :resolveInstanceMethod:为发送消息的对象的添加一个IMP,然后再让该对象去处理
4.2:forwardingTargetForSelector:将该消息转发给能处理该消息的对象
4.3:methodSignatureForSelector和forwardInvocation:第一个方法生成方法签名,然后创建NSInvocation对象作为参数给第二个方法,
4.3.2 然后在第二个方法(forwardInvocation)里面做消息处理,只要在第二个方法里面不执行父类的方法,即使不处理也不会崩溃
5.如果不进行动态解析就会导致崩溃
最后附带一份苹果的官方消息转发图:
写给自己:
喋喋不休不如观心自省,埋怨他人不如即听即忘。能干扰你的,往往是自己的太在意,能伤害你的,往往是自己的想不开,未完待续。。。。