iOS-底层原理12:消息流程分析之 动态方法决议 & 消息转发

在上一篇文章iOS-底层原理11:消息流程分析之慢速查找 中,分析了消息慢速查找流程,如果查找不到将进行动态方法决议,如果动态方法决议仍然没有找到实现,则进行消息转发

案例

step1: 新建一个LBHPerson类,定义一个实例方法instanceMethod1和一个类方法classMethod1,只声明不实现

//.h 
@interface LBHPerson : NSObject

- (void)instanceMethod1;
+ (void)classMethod1;

@end

//.m

@implementation LBHPerson

@end

step2:main函数中调用LBHPerson类的实例方法instanceMethod1

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        
        LBHPerson *person = [LBHPerson alloc];
        
        [person instanceMethod1];
        
    }
    return 0;
}

step3: 运行结果

调用类方法

[LBHPerson classMethod1];

运行结果

unrecognized selector sent to instance 0xxxxx 找不到方法实现

这是一个开发中很常见的奔溃问题,先学习这篇文章,然后用动态方法决议和消息转发解决这个问题。

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()) {
        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);
}

分为以下几步:

part1: 判断cls是否是元类

  • 如果是,调用实例方法的动态方法决议resolveInstanceMethod
  • 如果是元类,调用类方法的动态方法决议resolveClassMethod,如果在元类没有找到或者为空,则在元类实例方法的动态方法决议resolveInstanceMethod中查找, 是因为类方法存储在元类中,是元类的实例方法,所以还需要查找元类中实例方法的动态方法决议

part2: 如果动态方法决议中,将其实现指向了其他方法,则继续查找指定的imp,即继续慢速查找lookUpImpOrForward流程

此时 behavior = 1, LOOKUP_CACHE = 4lookUpImpOrForward函数中形参behavior变成了 1 | 4 = 5 ,这决定了进入lookUpImpOrForward后:

  • fastpath(behavior & LOOKUP_CACHE) = 5 & 4 = 4,条件成立,会优先cache_getImp读取一次缓存
  • slowpath(behavior & LOOKUP_RESOLVER) = 5 & 2 = 0,条件成立,不会进入resolveMethod_locked动态方法决议。
  • lookUpImpOrForward会循环遍历cls继承链的所有类的cache和methodList来寻找imp

流程图:

1.1 实例方法决议

step1: 实例方法在快速查找 -> 慢速查找 都没有找到的情况下,会走到 resolveInstanceMethod 方法,源码如下:

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

    // 1. 查找元类对象(cls->ISA())的类中是否有`resolveInstanceMethod`的imp。
    // (根元类中默认实现了`resolveInstanceMethod`方法,所以永远不会return)
    if (!lookUpImpOrNil(cls, resolve_sel, cls->ISA())) {
        return;
    }
    
    // 2. 发送resolve_sel消息
    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));
        }
    }
}

分步解析:

part1: 查找resolve_sel

if (!lookUpImpOrNil(cls, resolve_sel, cls->ISA())) {
        // Resolver not implemented.
        return;
}

问题lookUpImpOrNil到底做了什么?
解答:
查看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);
}

可以看到在lookUpImpOrNil中又调用了lookUpImpOrForward慢速查找流程

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继承链的所有类的cache和methodList来寻找imp

判断能否在慢速查找流程中找到resolveInstanceMethod方法实现。实际上根本不会进入if条件,因为在NSObject元类存在resolveInstanceMethod类方法。

问题:为什么不会进if条件? NSObject元类中存在resolveInstanceMethod类方法能证明吗?

解答

/// 遍历方法
-(void) printMethodes: (Class)cls {
   // 记录函数个数
   unsigned int count = 0;
   // 读取函数列表
   Method *methodList = class_copyMethodList(cls, &count);
    for (int i = 0; i < count; i++) {
        Method method = methodList[i];
        SEL sel = method_getName(method);
        IMP imp = class_getMethodImplementation(cls, sel);
        NSLog(@"method: %@-%p", NSStringFromSelector(sel), imp);
    }
    free(methodList);
}

//调用
[self printMethodes:objc_getMetaClass("NSObject")];

运行结果

NSObject元类方法列表中可以找到resolveInstanceMethod类方法

part2: 发送resolve_sel消息

// 2. 消息发送
BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
bool resolved = msg(cls, resolve_sel, sel);

part3: 通过慢速查找流程获取用户调用的方法sel(demo中为instanceMethod1)的方法实现imp,此处的获取是为了日志使用

IMP imp = lookUpImpOrNil(inst, sel, cls);
奔溃修改

step1:LBHPerson中新增一个lbhInstanceMethod的实例方法,声明并实现

//.h
@interface LBHPerson : NSObject

- (void)lbhInstanceMethod;

@end


//.m
@implementation LBHPerson

- (void)lbhInstanceMethod
{
    NSLog(@"%s",__func__);
}

@end

step2:LBHPerson类中重写resolveInstanceMethod类方法

@implementation LBHPerson

+ (BOOL)resolveInstanceMethod:(SEL)sel{
    
    NSLog(@"%@ 来了", NSStringFromSelector(sel));
    
    if (sel == @selector(instanceMethod1)) {
        
        //获取lbhInstanceMethod方法的imp
        IMP imp = class_getMethodImplementation(self, @selector(lbhInstanceMethod));
        //获取lbhInstanceMethod的实例方法
        Method lbhInstanceMethod  = class_getInstanceMethod(self, @selector(lbhInstanceMethod));
        //获取lbhInstanceMethod的丰富签名
        const char *type = method_getTypeEncoding(lbhInstanceMethod);
        //将sel的实现指向lbhInstanceMethod
        
        return class_addMethod(self, sel, imp, type);
    }
    
    return [super resolveInstanceMethod:sel];
    //    return NO;
}

@end

运行

崩溃解决了,实际上这么写是比较鸡肋的,都已经知道某个方法没有实现,那直接实现就好了,当然可以将if条件去掉,所有未实现的方法都走这个实现,那么有没有更好的方法呢?继续往下学习。

1.2 类方法决议

类方法在快速查找 -> 慢速查找 都没有找到的情况下,会走到resolveClassMethod 方法,源码如下:

static void resolveClassMethod(id inst, SEL sel, Class cls)
{
    runtimeLock.assertUnlocked();
    ASSERT(cls->isRealized());
    ASSERT(cls->isMetaClass());

    if (!lookUpImpOrNil(inst, @selector(resolveClassMethod:), cls)) {
        // 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;
    bool resolved = msg(nonmeta, @selector(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(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 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));
        }
    }
}

resolveClassMethod方法流程与resolveInstanceMethod方法流程类似。

崩溃解决

LBHPerson中添加一个lbhClassMethod类方法的,重写resolveClassMethod类方法

+ (void)lbhClassMethod
{
    NSLog(@"%s",__func__);
}

+ (BOOL)resolveClassMethod:(SEL)sel{

    NSLog(@"%@ 来了", NSStringFromSelector(sel));
    
    if (sel == @selector(classMethod1)) {

        IMP imp = class_getMethodImplementation(objc_getMetaClass("LBHPerson"), @selector(lbhClassMethod));
        Method lgClassMethod1  = class_getInstanceMethod(objc_getMetaClass("LBHPerson"), @selector(lbhClassMethod));
        const char *type = method_getTypeEncoding(lgClassMethod1);
        
        return class_addMethod(objc_getMetaClass("LBHPerson"), sel, imp, type);
        
    }

    return [super resolveClassMethod:sel];
}
1.3 优化

上面解决方法都是在单独的某个类中重写动态决议方法,这意味着每个类中都需要重写这两个方法,这样太麻烦了,怎么做呢? 相信大家都会。

  • 实例方法: --> 父类 --> 根类 --> nil
  • 类方法 :元类 --> 根元类 --> 根类 --> nil

如果在当前元类中没有找到方法实现,会沿着它们的继承链向上查找,它们都会经过根类即NSObject

问题: 是否可以将上述的两个方法统一整合在一起呢?
解答:是可以的,可以通过NSObject分类的方式来实现统一处理,而且由于类方法的查找,在其继承链,查找的也是实例方法,所以可以将实例方法类方法的统一放在resolveInstanceMethod方法中处理。

+ (BOOL)resolveInstanceMethod:(SEL)sel{
    
    NSLog(@"%@ 来了", NSStringFromSelector(sel));
    
    if (sel == @selector(classMethod1)) {
        
        IMP imp = class_getMethodImplementation(objc_getMetaClass("LBHPerson"), @selector(lbhClassMethod));
        Method lgClassMethod1  = class_getInstanceMethod(objc_getMetaClass("LBHPerson"), @selector(lbhClassMethod));
        const char *type = method_getTypeEncoding(lgClassMethod1);

        return class_addMethod(objc_getMetaClass("LBHPerson"), sel, imp, type);
    
    }else if (sel == @selector(instanceMethod1)) {
        
        //获取lbhInstanceMethod方法的imp
        IMP imp = class_getMethodImplementation(self, @selector(lbhInstanceMethod));
        //获取lbhInstanceMethod的实例方法
        Method lbhInstanceMethod1  = class_getInstanceMethod(self, @selector(lbhInstanceMethod));
        //获取lbhInstanceMethod的丰富签名
        const char *type = method_getTypeEncoding(lbhInstanceMethod1);
        //将sel的实现指向lbhInstanceMethod

        return class_addMethod(self, sel, imp, type);
        
    }
    
//    return [super resolveInstanceMethod:sel];
    return NO;
}

这种方式的实现,正好与源码中针对类方法的处理逻辑是一致的,即完美阐述为什么调用了类方法动态方法决议,还要调用对象方法动态方法决议,其根本原因是类方法是元类中的实例方法

2. 消息转发

我们了解到,如果快速+慢速没有找到方法实现,动态方法决议也不行,就使用消息转发,但是,我们找遍了源码也没有发现消息转发的相关源码,可以通过以下方式来了解:

  • 通过instrumentObjcMessageSends方式打印发送消息的日志
  • 通过hopper/IDA反编译

2.1 instrumentObjcMessageSends

通过lookUpImpOrForward --> log_and_fill_cache --> logMessageSend,在logMessageSend源码下方找到instrumentObjcMessageSends的源码实现。
在main中调用
instrumentObjcMessageSends打印方法调用的日志信息,有以下两点准备工作

1、打开 objcMsgLogEnabled 开关,即调用instrumentObjcMessageSends方法时,传入YES

2、在main中通过extern 声明instrumentObjcMessageSends方法

//最好使用命令行  
extern void instrumentObjcMessageSends(BOOL flag);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        
        LBHPerson *person = [LBHPerson alloc];
        instrumentObjcMessageSends(YES);
        [person instanceMethod1];
//        [LBHPerson classMethod];
        instrumentObjcMessageSends(NO);
        
    }
    return 0;
}
  • 通过logMessageSend源码,了解到消息发送打印信息存储在/tmp 文件夹下,
  • 运行代码,并前往/tmp文件夹,发现有msgSends开头的日志文件,打开发现在崩溃前,执行了以下方法

2.2 通过hopper/IDA反编译

HopperIDA是一个可以帮助我们静态分析可视性文件的工具,可以将可执行文件反汇编成伪代码、控制流程图等,下面以Hopper为例 (针对比较简单的反汇编,demo版本即可)

step1: 在上面的例子中,查看下崩溃的堆栈信息

崩溃堆栈信息

发现___forwarding___来自CoreFoundation

step2: 通过image list,读取整个镜像文件,然后搜索CoreFoundation,查看其可执行文件的路径

step3: 通过文件路径,找到CoreFoundation可执行文件

step4: 打开hopper,选择Try the Demo,然后将上一步的可执行文件拖入hopper进行反汇编,选择x86(64 bits)

hopper选择Demo版本
hopper反汇编

step5: 以下是反汇编后的界面,主要使用上面的三个功能,分别是 汇编、流程图、伪代码

hoppper主要使用的三个功能

step6: 通过左侧的搜索框搜索__forwarding_prep_0___,然后选择伪代码

伪代码-__forwarding_prep_0___

step7: 进入___forwarding___的伪代码实现,首先是查看是否实现forwardingTargetForSelector方法,如果没有响应,跳转至loc_6459b即快速转发没有响应,进入慢速转发流程

伪代码-___forwarding___

step8: 跳转至loc_64a67,在其下方判断是否响应methodSignatureForSelector方法

  • 如果没有响应,跳转至loc_64dd7,则直接报错
  • 如果获取methodSignatureForSelector方法签名为nil,也是直接报错

step9: 如果methodSignatureForSelector返回值不为空,则在forwardInvocation方法中对invocation进行处理

所以,通过上面两种查找方式可以验证,消息转发的方法有3个

步骤 方法
快速转发 forwardingTargetForSelector
慢速转发 methodSignatureForSelector + forwardInvocation

综上所述,消息转发整体的流程如下

消息转发整体流程

3. 消息转发之快速转发

针对前面的崩溃问题,如果动态方法决议也没有找到实现,则需要在LBHPerson中重写forwardingTargetForSelector方法,将LBHPerson的实例方法的接收者指定为LBHStudent 的对象(LBHStudent类中有instanceMethod1的具体实现),如下所示

//LBHPerson
@interface LBHPerson : NSObject
- (void)instanceMethod1;
@end

@implementation LBHPerson
- (id)forwardingTargetForSelector:(SEL)aSelector{
    NSLog(@"%s - %@",__func__,NSStringFromSelector(aSelector));

//     runtime + aSelector + addMethod + imp
    //将消息的接收者指定为LBHStudent,在LBHStudent中查找instanceMethod1的实现
    return [LBHStudent alloc];
}
@end

//LBHStudent
@interface LBHStudent : LBHPerson
@end

@implementation LBHStudent
- (void)instanceMethod1
{
    NSLog(@"%s",__func__);
}
@end

运行结果

快速转发-指定消息接收者

问题: 如果将LBHStudentinstanceMethod1方法注释掉,程序运行并不会崩溃

注释掉LBHStudentinstanceMethod1方法,在forwardingTargetForSelector打上断点,发现程序在不停的执行这个方法,具体原因后续再去查找,不停的执行这个方法是很不好的。

实际上这么写是很鸡肋的,除非把所有方法都写在一个类中,显然这是不现实的,而且指定的类中如果没有这个方法的实现,forwardingTargetForSelector方法会一直被调用

forwardingTargetForSelector方法改一下

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

这么写可以解决方法找不到而一直执行forwardingTargetForSelector
,但是并不能解决崩溃,需要配合慢速转发使用。

4. 消息转发之慢速转发

如果快速转发中还是没有找到,则进入最后的一次挽救机会,即在LBHPerson中重写methodSignatureForSelector,如下所示

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

- (void)forwardInvocation:(NSInvocation *)anInvocation{
    NSLog(@"%s - %@",__func__,anInvocation);
}

运行结果

当然此时可以将快速转发的代码注释掉,只保留慢速转发

也可以处理invocation事务,如下所示,修改invocation的target[LBHStudent alloc],调用 [anInvocation invoke] 触发 LBHStudent类的instanceMethod1实例方法

不过实际开发中这么写比较鸡肋,有种画蛇添足的感觉。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345

推荐阅读更多精彩内容