iOS 进阶之路

OC底层原理十四:objc_msgSend(消息转发)

2020-09-24  本文已影响0人  markhetao

OC底层原理 学习大纲

当我们发送的消息,在消息接受者以及它的整个继承链缓存(Cache)方法列表(methodList)中都找不到时,系统给我们留了崩溃前最后一次挽救机会

本节将探索方法找不到最后的挽救机会 - 消息转发机制

1. 前期准备
2. 动态方法决议
3. 消息转发
4. 异常消息处理流程图

1.前期准备

打开objc4源文件,在main.m中加入测试代码:

@interface HTPerson : NSObject
- (void)sayHello;
+ (void)say666;
@end

@implementation HTPerson
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        HTPerson * person  = [HTPerson alloc];
        [person sayHello];
        
    }
    return 0;
}

sayHello方法不实现。模拟方法找不到的场景。

if (slowpath(behavior & LOOKUP_RESOLVER)) {
    behavior ^= LOOKUP_RESOLVER; // behavior取反后,下一次进入判断时,if条件就不成立了。 确保动态决议只执行一次
    return resolveMethod_locked(inst, sel, cls, behavior);
}

补充lookUpImpOrForward的入参理解

IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
  • inst:真实持有sel方法的对象方法存放在类中,类方法存放在元类中
    对象方法: [person sayHello], instHTPerson
    类方法: [HTPerson say666], inst也是HTPerson元类

  • sel:方法名

  • cls:方法接受者的所属类
    对象方法: [person sayHello], cls是HTPerson
    类方法: [HTPerson say666], cls也是HTPerson

  • behavior:行为参数, 影响进入动态决议等判断条件。

2. 动态方法决议

static NEVER_INLINE IMP
resolveMethod_locked(id inst, SEL sel, Class cls, int behavior)
{
    runtimeLock.assertLocked();
    ASSERT(cls->isRealized());

    runtimeLock.unlock();
  
    // 对象方法
    if (! cls->isMetaClass()) {
        resolveInstanceMethod(inst, sel, cls);
    } 
    // 类方法
    else {
        resolveClassMethod(inst, sel, cls);
        if (!lookUpImpOrNil(inst, sel, cls)) {
            resolveInstanceMethod(inst, sel, cls);
        }
    }

    // 重新查询一次
    return lookUpImpOrForward(inst, sel, cls, behavior | LOOKUP_CACHE);
}

判断是cls是元类还是本类:

最后再调用lookUpImpOrForward重新查询一次。

分析resolveInstanceMethod

static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{
    runtimeLock.assertUnlocked();
    ASSERT(cls->isRealized());
    SEL resolve_sel = @selector(resolveInstanceMethod:);

    // 1. 查找类对象的元类中是否有`resolveInstanceMethod`的imp。
    // (根元类中默认实现了`resolveInstanceMethod`方法,所以永远不会return)
    if (!lookUpImpOrNil(cls, resolve_sel, cls->ISA())) {
        return;
    }
    
    // 2. 调用一次`resolveInstanceMethod`函数
    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(cls, resolve_sel, sel);

    // 3. 再搜索一次sel的imp
    //(如果在上面resolveInstanceMethod函数实现了sel,我们就拿到imp了,成功将sel和imp写入cls的缓存中)
    IMP imp = lookUpImpOrNil(inst, sel, cls);
    
    // 做Log记录
    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:

static inline IMP
lookUpImpOrNil(id obj, SEL sel, Class cls, int behavior = 0)
{
   // behavior = 0, LOOKUP_CACHE = 4, LOOKUP_NIL = 8
   return lookUpImpOrForward(obj, sel, cls, behavior | LOOKUP_CACHE | LOOKUP_NIL);
}

内部继续调用了lookUpImpOrForward函数,不同的是,behavior变成了 0 | 4 | 8 = 12。 >这决定了进入lookUpImpOrForward后:

  • fastpath(behavior & LOOKUP_CACHE) = 12 & 4 = 4,条件成立,会优先cache_getImp读取一次缓存
  • slowpath(behavior & LOOKUP_RESOLVER) = 12 & 2 = 0,不成立,不会进入resolveMethod_locked动态方法决议。

lookUpImpOrNil中的lookUpImpOrForward会循环遍历cls继承链所有类cachemethodList来寻找imp

所以resolveInstanceMethod函数,就是系统给开发者的一次机会。

系统大哥用大白话跟你说:老弟,我检查到你的方法没有实现,我现在给你一次机会,你识趣的话,就在我调用 resolveInstanceMethod函数之前,在这函数内部把你的sel实现了。等到我调用完后,你还是没实现的话,我就了你。

案例:

  1. 分析imp实现位置。
  • 如:[person sayHello]对象方法对象方法存放本类中。[HTPerson say666]类方法类方法是存放在元类中。
  1. imp写哪里?
  • NSObject是所有类的父类,NSObject中有resolveInstanceMethod类方法。所以我们可以在继承链的某个合适的类中重写resolveInstanceMethod方法。在这个方法内部,实现imp
#import <Foundation/Foundation.h>
#import <objc/message.h>

@interface BaseObject: NSObject
@end

@implementation BaseObject

- (void)handleErrorImp {
    NSLog(@"我帮你挡住崩溃了,上报数据给后台还是咋操作,你自己想");
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    
    if (sel ==  @selector(sayHello)) {
        NSLog(@"💣💣💣💣 %@ 没实现!!💣💣💣💣", NSStringFromSelector(sel));
        
        IMP imp = class_getMethodImplementation(self, @selector(handleErrorImp));
        Method method = class_getInstanceMethod(self, @selector(handleErrorImp));
        const char * type = method_getTypeEncoding(method);
        // 将imp加入当前类。
        return class_addMethod(self, sel, imp, type);
        
    }
    
    return [super resolveInstanceMethod: sel];
}
@end

@interface HTPerson : BaseObject
- (void)sayHello;
@end

@implementation HTPerson
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        HTPerson * person  = [HTPerson alloc];
        [person sayHello];
        
    }
    return 0;
}

具体操作时,我们并不知道sel是哪个,按理说进入resolveInstanceMethod的所有sel都是找不到imp的。我们可以统一拦截进行数据上报,然后做防崩溃处理。
(比如个人中心某个深层页面函数未实现,我们直接统一返回个人中心防止崩溃,同时将这次找不到的记录上传给后台,让相应的程序员哥哥下个版本修复它)

实际开发中,你不确定是否有人在继承链上游就使用了resolveInstanceMethod黑魔法。这样你的resolveInstanceMethod被重写 😂 毕竟牛逼的人很多。

这是打印结果:


image.png

3. 消息转发

当我们在动态方法决议中不做任何处理,在崩溃前,系统还有2个隐藏挽救点快速转发forwardingTargetForSelector慢速转发methodSignatureForSelector

在正式介绍这2个方法前,我们应该知道如何发现他们的。

方法1: 日志查看

lookUpImpOrForwardlog_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);
}

我们看到在cache_fill写入操作前,会检查SUPPORT_MESSAGE_LOGGING是否支持消息记录

进入logMessageSend函数内部,发现日志存放路径/tmp文件夹:

image.png

我们打开任意一个文件夹,按Command + Shift + G,输入/tmp文件夹查看。发现并没有msgSends名字的文件。

image.png

文件内搜索objcMsgLogEnabled,发现赋值操作是在instrumentObjcMessageSends中。

image.png

所以我们在崩溃前,手动调用instrumentObjcMessageSends进行日志的开启关闭

注意,这次不是在源码环境。而是在任意一个正常项目中进行测试

// extern: 声明当前变量或函数在别的文件中定义了。不要报错。
extern void instrumentObjcMessageSends(BOOL flag);

@interface HTPerson : NSObject
- (void)sayHello;
@end

@implementation HTPerson
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        HTPerson * person  = [HTPerson alloc];
        instrumentObjcMessageSends(YES); // 打开记录
        [person sayHello];
        instrumentObjcMessageSends(NO); // 关闭记录
        
    }
    return 0;
}

运行程序,crash后,在/tmp文件夹中看不到msgSends-92567文件

image.png

打开msgSends-92567文件,发现最上面的日志记录为:

+ HTPerson NSObject resolveInstanceMethod:
+ HTPerson NSObject resolveInstanceMethod:

- HTPerson NSObject forwardingTargetForSelector:
- HTPerson NSObject forwardingTargetForSelector:

- HTPerson NSObject methodSignatureForSelector:
- HTPerson NSObject methodSignatureForSelector:

- HTPerson NSObject class

+ HTPerson NSObject resolveInstanceMethod:
+ HTPerson NSObject resolveInstanceMethod:

- HTPerson NSObject doesNotRecognizeSelector:
- HTPerson NSObject doesNotRecognizeSelector:

- HTPerson NSObject class

这是崩溃前调用的函数记录。
我们发现调用者是HTPerson,我们直接在HTPerson中加入类似的方法进行实现。

@implementation HTPerosn

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    NSLog(@"%s %@", __func__ ,NSStringFromSelector(sel));
    return [super resolveInstanceMethod:sel];
}

- (id)forwardingTargetForSelector:(SEL)aSelector {
    NSLog(@"%s %@", __func__ ,NSStringFromSelector(aSelector));
    return [super forwardingTargetForSelector:aSelector];
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    NSLog(@"%s %@", __func__ ,NSStringFromSelector(aSelector));
    return [super methodSignatureForSelector:aSelector];
}

-(void)doesNotRecognizeSelector:(SEL)aSelector {
    NSLog(@"%s %@", __func__ ,NSStringFromSelector(aSelector));
    return [super doesNotRecognizeSelector:aSelector];
}

@end
image.png

我们知道,resolveInstanceMethod动态方法决议系统开发者留的最后一次机会

  • 系统希望开发者resolveInstanceMethod中将未实现的sel完成实现。并且他会在给完机会后重新执行一次lookUpImpOrForward

  • 打印日志中有2次resolveInstanceMethod,如果我们没有在resolveInstanceMethod中修改。 那么我们可以在第二次resolveInstanceMethod前的2个方法进行操作。

所以我们可以在forwardingTargetForSelectormethodSignatureForSelector方法里做补救

大家都知道改resolveInstanceMethod,那就让你们随意发挥。大佬在你们resolveInstanceMethod后面阻击一切漏网之鱼。😼

现在,我们来了解大佬的法器

法器一:快速转发forwardingTargetForSelector

意思是:aSelector是没实现的选择器,我找不到实现的对象

  • 既然你找不到实现的对象,那我就给你个实现这个方法的对象

案例:

image.png

在第一次resolveInstanceMethod之后,成功替换实现方法,并拦截崩溃

法器二:慢速转发methodSignatureForSelector

forwardingTargetForSelector image.png

意思是:返回一个NSMethodSignature函数签名对象,Discussion中说了需要实现协议。搭配forwardinvocation函数使用。

image.png

点击进入NSInvocation查看格式:

image.png

我们打印invocation看看

image.png

例如:

  • HTStudent实现一个sayNB,然后修改anInvocationtargetselector
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
   NSLog(@"%s %@", __func__ ,NSStringFromSelector(aSelector));
   return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}

-(void)forwardInvocation:(NSInvocation *)anInvocation {
   anInvocation.target = [HTStudent alloc];
   anInvocation.selector = @selector(sayNB);
   [anInvocation invoke];
}

4. 异常消息处理流程图

异常消息处理流程图

最后,奉上OC底层原理:objc_msgSend全流程图

上一篇 下一篇

猜你喜欢

热点阅读