iOS 底层探索:objc_msgSend 动态方法决议 & 消息转发

iOS 底层探索: 学习大纲 OC篇

前言

  • OC调用方法,底层是调用 objc_msgSend 发送消息。在发送消息时会经过一系列的快速 查找、慢速查找,如果查找到对应的 IMP,直接返回;如果没有找到,就会进入到方法的动态方法决议和消息转发流程。

  • 这篇文章就是深入探索动态方法决议消息转发

准备工作

一 、 动态方法决议

接着上一篇,在慢速查找流程未找到方法实现时,首先会尝试一次动态方法决议:

    if (slowpath(behavior & LOOKUP_RESOLVER)) {
        //动态方法决议的控制条件
        behavior ^= LOOKUP_RESOLVER; 
        return resolveMethod_locked(inst, sel, cls, behavior);
    }

进入动态方法决议阶段,源码如下

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); //元类执行解析方式
// -- 如果resolveClassMethod找到了,就不会走这里了
//-- 如果没找到,这个if必然会走,之前调用lookUpImpOrForward,已经给该sel的方法缓存了imp = forward_imp
// -- 必然会走到done_nolock,返回一个nil
// -- 类方法在元类中也是以实例方法的形式存在,所以还需再走一遍实例方法的动态方法决议流程
        if (!lookUpImpOrNil(inst, sel, cls)) {
            resolveInstanceMethod(inst, sel, cls);//类执行解析方式
        }
    }
//重新进入 lookUpImpOrForward  根据 behavior | LOOKUP_CACHE进行判断执行方式
    return lookUpImpOrForward(inst, sel, cls, behavior | LOOKUP_CACHE);
}

在resolveInstanceMethod方法中对实例方法动态解析,源码如下:

static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{
    runtimeLock.assertUnlocked();
    ASSERT(cls->isRealized());
    SEL resolve_sel = @selector(resolveInstanceMethod:);
    // 1\. 判断系统是否实现SEL_resolveInstanceMethod方法
    // 即+(BOOL)resolveInstanceMethod:(SEL)sel, 
    // 继承自NSObject的类,默认实现,返回NO
    if (!lookUpImpOrNil(cls, resolve_sel, cls->ISA())) 
    {
        // Resolver not implemented.
        // 不是NSObject的子类,也未实现+(BOOL)resolveInstanceMethod:(SEL)sel,
        // 直接返回,没有动态解析的必要
        return;
    }

    // 2\. 系统给你一次机会 - 你要不要针对 sel 来操作一下下
    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(cls, resolve_sel, sel);
    // 3\.-- 再去查一次imp,如果在cls的继承链上自定义实现了`resolveInstanceMethod`方法并在里面添加了imp,就可以找到imp
    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));
        }
    }
}

在resolveClassMethod方法中对实例方法动态解析,源码如下:

static void resolveClassMethod(id inst, SEL sel, Class cls)
{
    runtimeLock.assertUnlocked();
    ASSERT(cls->isRealized());
    ASSERT(cls->isMetaClass());
//: -- 容错处理,判断该元类的继承链中是否有resolveClassMethod方法
//: -- 如果自定义没实现,则会找到NSObject
    if (!lookUpImpOrNil(inst, @selector(resolveClassMethod:), cls)) {
        // Resolver not implemented.
        return;
    }

    Class nonmeta;
    {
        mutex_locker_t lock(runtimeLock);
//: -- 得到元类/类的对应的类
//: -- 因为我们只能在类里实现resolveClassMethod方法,无法去元类实现,所以这里把消息接受者设置为当前类
        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);
        }
    }
//: -- 发送消息,调用nonmeta中的`resolveClassMethod `方法
    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,如果上面调用用nonmeta中的`resolveClassMethod `方法里面给元类添加了imp,就会直接找到
    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));
        }
    }
}
  • 在经过resolveMethod_locked方法后,进行resolveInstanceMethod,重新进行一遍lookUpImpOrForward;即: 如果动态方法决议中,将其实现指向了其他方法,则继续查找指定的imp,即继续慢速查找lookUpImpOrForward流程

  • 使用动态方法决议流程:
    resolveMethod_locked的流程图
  • 使用动态方法决议,代码举例:

//对象使用
+ (BOOL)resolveInstanceMethod:(SEL)sel{
    
    if (sel == @selector(say666)) {
        NSLog(@"%@ 来了",NSStringFromSelector(sel));
  /**
         添加方法

         @param self 调用该方法的对象
         @param sel 方法的名
         @param IMP 新添加的方法,是c语言实现的
         @param type :方法签名, 新添加的方法的类型,包含函数的返回值以及参数内容类型,eg:void xxx(NSString *name, int size),类型为:v@i
         */
        IMP imp           = class_getMethodImplementation(self, @selector(sayMaster));
        Method sayMMethod = class_getInstanceMethod(self, @selector(sayMaster));
        const char *type  = method_getTypeEncoding(sayMMethod);
        return class_addMethod(self, sel, imp, type);
    }
    
    return [super resolveInstanceMethod:sel];
}

//类使用
+ (BOOL)resolveClassMethod:(SEL)sel{
    NSLog(@"%@ 来了",NSStringFromSelector(sel));
    if (sel == @selector(sayNB)) {

        IMP imp           = class_getMethodImplementation(objc_getMetaClass("LGPerson"), @selector(lgClassMethod));
        Method sayMMethod = class_getInstanceMethod(objc_getMetaClass("LGPerson"), @selector(lgClassMethod));
        const char *type  = method_getTypeEncoding(sayMMethod);
        return class_addMethod(objc_getMetaClass("LGPerson"), sel, imp, type);
    }
    return [super resolveClassMethod:sel];
}

就算没有实现方法say666 ,但是class_addMethod,依然不会崩溃。

优化:根据resolveMethod_locked的流程图,如果是元类调用,最后还是会走实例方法,根据isa走位图,可知元类继承链,最终继承NSObject,所以我们可以给NSObject 写个分类,直接在分类中综合两个方法如下:

+ (BOOL)resolveInstanceMethod:(SEL)sel{
    if (sel == @selector(say666)) {
        NSLog(@"%@ 来了", NSStringFromSelector(sel));
        
        IMP imp = class_getMethodImplementation(self, @selector(sayMaster));
        Method sayMethod  = class_getInstanceMethod(self, @selector(sayMaster));
        const char *type = method_getTypeEncoding(sayMethod);
        return class_addMethod(self, sel, imp, type);
    }else if (sel == @selector(sayNB)) {
        NSLog(@"%@ 来了", NSStringFromSelector(sel));
        
        IMP imp = class_getMethodImplementation(objc_getMetaClass("LGPerson"), @selector(lgClassMethod));
        Method lgClassMethod  = class_getInstanceMethod(objc_getMetaClass("LGPerson"), @selector(lgClassMethod));
        const char *type = method_getTypeEncoding(lgClassMethod);
        return class_addMethod(objc_getMetaClass("LGPerson"), sel, imp, type);
    }
    return NO;
}

风险:使用这方法的风险可能造成其他SDK 或者系统方法也使用了类似的方法,造成冲突。所以我们使用的类名LGPerson一定要特殊唯一。

注 :
使用这种办法的前提是:相关方法代码已经实现,只是在运行时将改方法动态添加到目标类中。。

二、 消息转发机制

在方法查找过程中,经过缓存查找,方法列表查找和动态方法解析,如果经历慢速查找都没有查找到IMP,也没有进行方法动态解析,那么我们还有办法进行方法实现: 消息转发机制。但是我们只知道有消息转发这个机制,但是我们始终找不到 消息转发的实现方法和实现。这个时候:

开启凡人的视角:

  • lldb方式断点查看: 通过进入 [person sayHello]之后的断点查看在 class 为 LGPerson的时候 Sel ,可以发现forwardingTargetForSelectormethodSignatureForSelector。如图:
image.png

开启上帝视角:

  • 方式1:通过instrumentObjcMessageSends方式打印发送消息的日志

  • 方式2:通过hopper/IDA反编译

方式1 :instrumentObjcMessageSends

通过lookUpImpOrForward --> log_and_fill_cache--> logMessageSend,在logMessageSend源码下方找到instrumentObjcMessageSends的源码实现如下:

/***********************************************************************
* instrumentObjcMessageSends
**********************************************************************/
// Define this everywhere even if it isn't used to simplify fork() safety code.
spinlock_t objcMsgLogLock;
#if !SUPPORT_MESSAGE_LOGGING

///开启instrumentObjcMessageSends
void    instrumentObjcMessageSends(BOOL flag)
{
}
#else

///将方法执行记录通过打印出来
bool objcMsgLogEnabled = false;
static int objcMsgLogFD = -1;
bool logMessageSend(bool isClassMethod,
                    const char *objectsClass,
                    const char *implementingClass,
                    SEL selector)
{
 .........
}
///结束instrumentObjcMessageSends
void instrumentObjcMessageSends(BOOL flag)
{
 .........
// SUPPORT_MESSAGE_LOGGING
#endif

所以,在main中调用instrumentObjcMessageSends打印方法调用的日志信息犹如上面的格式:

  • 1、在main中通过extern 声明instrumentObjcMessageSends方法
  • 2、打开 objcMsgLogEnabled 开关,即调用instrumentObjcMessageSends方法时,传入YES

不带源码的工程中这样实现:

///extern 修饰 我们可以拿来调用 苹果提供了打印方法流程的方法 
extern void instrumentObjcMessageSends(BOOL flag);
int main(int argc, const char * argv[]) {
    @autoreleasepool {

        LGPerson *person = [LGPerson alloc];
//开启
        instrumentObjcMessageSends(YES);
//去logMessageSend找打印信息
        [person sayHello];
//关闭
        instrumentObjcMessageSends(NO);
    }
    return 0;
}

我们在objc4源码工程再去找logMessageSend方法:

/***********************************************************************
* 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
    ........
    return false;
}

运行不带源码的工程,并前往/tmp/msgSends 目录,发现有msgSends开头的日志文件;
快捷键:command + shift + g ---> 输入 /tmp/msgSends -->回车
打开msgSends.文件:

可以看出 ,崩溃前执行了
两次动态方法决议:resolveInstanceMethod方法
两次消息快速转发:forwardingTargetForSelector方法
两次消息慢速转发:methodSignatureForSelector + resolveInstanceMethod

方式2:通过hopper/IDA反编译找到forwardingTargetForSelector等转发方法的实现

  • Hopper和IDA是一个可以帮助我们静态分析可视性文件的工具,可以将可执行文件反汇编成伪代码、控制流程图等,下面以mac端Hopper为例(Hopper :要收费的,IDA:是Windows端的)。

  • 通过打印崩溃的堆栈信息进行查看崩溃信息如下:

通过路径/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation 获取到CoreFoundation的可执行文件:

CoreFoundation
  • 通过 hopper 进行反编译,步骤如下:
    打开hopper,选择Try the Demo,然后将上一步的可执行文件拖入hopper进行反汇编,选择x86(64 bits)如图:

next:先搜索__forwarding_prep_0___

image.png

跳转至:___forwarding___

  • 终于找到了forwardingTargetForSelector:方法了:咋们再来看看 其执行流程,简单分析如下:
    ___forwarding___简单流程

开启上帝视角之后,我们通过汇编分析,可以清晰看到转发流程方法:

【快速转发】forwardingTargetForSelector
【慢速转发】methodSignatureForSelector —>forwardInvocation

注:forwardInvocation :消息重定向,以后再分析。

动态方法决议和消息转发整体的流程如下:

注: 不论是否进行动态决议都会重新lookUpImpOrForward,这里会有behavior的判断,是否经历过动态决议处理。所以开始有一次动态决议处理方法的机会。

三、 实际举例验证:

快速转发实现
慢速转发实现

注:如果不进行快速消息转发 也不进行慢速转发就会崩溃,可以试试。

四、 总结

    1. 消息转发机制是在汇编中实现的,并且属于CoreFoundation框架中,不开源的。我们可以通过反汇编的方式去查看;
    1. 在我们没有实现方法的时候,慢速查找也找不到方法的时候,我们有三次机会去实现方法:
    • 1). 动态方法决议:实现resolveInstanceMethod
    • 2). 快速消息转发:实现forwardingTargetForSelector
    • 3). 慢速消息转发:实现methodSignatureForSelector —>forwardInvocation
    1. 如果objc_msgSend快速查找和慢速查找失败,未实现动态方法决议和消息转发,则程序直接报错崩溃unrecognized selector sent to instance

五、 拓展

    1. objc_msgForward_impcache 的转换 ;

objc_msgForward_impcache调用

IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
    const IMP forward_imp = (IMP)_objc_msgForward_impcache;
    IMP imp = nil;
    Class curClass;
......

_objc_msgForward_impcache源码:

STATIC_ENTRY __objc_msgForward_impcache
    // No stret specialization.
    b   __objc_msgForward
    END_ENTRY __objc_msgForward_impcache

    ENTRY __objc_msgForward
    adrp    x17, __objc_forward_handler@PAGE
    ldr p17, [x17, __objc_forward_handler@PAGEOFF]
    TailCallFunctionPointer x17
    END_ENTRY __objc_msgForward

_objc_msgForward_impcache 只是个内部的函数指针,只存储于类的方法缓存中,需要被转化为_objc_msgForward 才能被外部调用。

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