Objc4-818底层探索(十):msgSend消息发送(三): 动态方法决议

建议先看下

Objc4-818底层探索(九):msgSend消息发送(二): 慢速查找

名词解释:
  • sel: 方法编号, 可以理解成一本书的目录, 通过目录可以找到页码(imp)
  • imp: 函数指针, 可以理解成数的页面, 根据页面可找到内容(函数实现)
  • resolve: 决定, 决议
  • instance: 实例
  • method: 方法

forward_imp

先看例子:

例子1

例子2

通常我们.h只写方法声明, .m不写方法实现 或者 利用函数performSelector:@selector(XXX), 但是都没有实现方法, 就会报方法找不到错误unrecognized selector sent to instance XXX

其实这个错误来自于lookUpImpOrForward中的 forward_imp方法

const IMP forward_imp = (IMP)_objc_msgForward_impcache; // 汇编方法

全局搜索objc_msgForward_impcache, 这个方法在汇编里面

__objc_msgForward_impcache

关于下划线知识点

1、C/C++调用汇编: 查找汇编时,C/C++调用的方法需要多加一个下划线( ___ )
2、汇编 调用 C/C++方法: 查找C/C++方法,需要将汇编调用的方法去掉一个下划线(___)

__objc_msgForward_impcache里面调用__objc_msgForward方法进入__objc_msgForward看下源码

    ENTRY __objc_msgForward // 进入 __objc_msgForward

    adrp    x17, __objc_forward_handler@PAGE  // 调用__objc_forward_handler方法
    ldr p17, [x17, __objc_forward_handler@PAGEOFF]
    TailCallFunctionPointer x17 // 返回函数指针
    
    END_ENTRY __objc_msgForward // 结束 __objc_msgForward

其中

  • adrp: 以页@PAGE(也有写P的)为单位的大范围的地址读取指令
    adrp

通俗讲为: ADRP指令可以理解成先进行PC + imm(偏移值)然后找到lable所在的一个4KB的页,然后取得label的基址,再进行偏移去寻址

这里就是 adrp指令计算出这个__objc_forward_handler@PAGE(偏移量), 之后将基址存到寄存器X17中, 关键就是__objc_forward_handler 方法

  • ldr: LDR R0,[R1, #8] ;将存储器地址为R1+8的字数据读入寄存器R0。这里实际上将x17又去除偏移量, 给到p17中



可看到其实关键是调用了一个__objc_forward_handler, 我们全局搜索一下可发现是一个C++方法, 在objc-runtime.mm中, 看下_objc_forward_handler

_objc_forward_handler

可看到报错信息... unrecognized selector sent to instance XXX...就是我们经典的报错方法, 并且其中可看到底层是不区分类方法 +对象方法/实例方法 -的。"+"/ "-"在这里是人为拼接上的。我们之前在Clang也看过类方法在是元类调用实例方法形式存在的

            if (slowpath((curClass = curClass->getSuperclass()) == nil)) {
                // No implementation found, and method resolver didn't help.
                // Use forwarding.
                imp = forward_imp;
                break;
            }

调用地方需要返回再看lookUpImpOrForward方法, 如果父类链都没找到就令imp = forward_imp;最终方法如果没有找到就会往下走return impreturn forward_imp, 报那个经典方法找不到错误。



动态方法决议

慢速查找流程图
  • resolve: 决定, 决议
  • instance: 实例
  • method: 方法
    resolve名词解释

实际上, 当父类链查找完, 也没有找到imp时并不是直接 return imp (imp = forward_imp)中间还会进行一次动态方法决议resolveInstanceMethod操作。

    if (slowpath(behavior & LOOKUP_RESOLVER)) {
        behavior ^= LOOKUP_RESOLVER;
        return resolveMethod_locked(inst, sel, cls, behavior);
    }

我们先看下resolveMethod_locked方法


/***********************************************************************
* resolveMethod_locked
* Call +resolveClassMethod or +resolveInstanceMethod.
*
* Called with the runtimeLock held to avoid pressure in the caller
* Tail calls into lookUpImpOrForward, also to avoid pressure in the callerb
**********************************************************************/
static NEVER_INLINE IMP
resolveMethod_locked(id inst, SEL sel, Class cls, int behavior)
{
  ...
   
    if (! cls->isMetaClass()) {
   // 当前类不是元类, 即查找的是实例方法
        // try [cls resolveInstanceMethod:sel]
        
        // 实例方法的动态方法决议流程
        resolveInstanceMethod(inst, sel, cls);
    } 
    else {
    // 当前类是元类, 即查找的是类方法
        // try [nonMetaClass resolveClassMethod:sel]
        // and [cls resolveInstanceMethod:sel]

        // 类方法的动态方法决议流程
        resolveClassMethod(inst, sel, cls);
        if (!lookUpImpOrNilTryCache(inst, sel, cls)) {
            resolveInstanceMethod(inst, sel, cls);
        }
    }

    // chances are that calling the resolver have populated the cache
    // so attempt using it

    // 如果有动态方法决议, 处理了, 就调用lookUpImpOrForwardTryCache重新帮你处理下
    return lookUpImpOrForwardTryCache(inst, sel, cls, behavior);
}

可以理解成, 当前方法都imp没找到, 苹果系统给一次补救机会即动态方法决议

  • 实例方法resolveInstanceMethod

  • 类方法resolveClassMethod

resolveInstanceMethod

先看下源码

/***********************************************************************
* resolveInstanceMethod
* Call +resolveInstanceMethod, looking for a method to be added to class cls.
* cls may be a metaclass or a non-meta class.
* Does not check if the method already exists.
**********************************************************************/
static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{
   ...
    // 定义一个resolveInstanceMethod 的方法编号resolve_sel
    SEL resolve_sel = @selector(resolveInstanceMethod:);

    // 判断当前类, 父类等(NSObject默认有决议方法) 是否有决议方法resolveInstanceMethod, 根本没写就直接返回, 这也是为什么我上面例子为何报 unrecognized selector sent to instance XXX
    if (!lookUpImpOrNilTryCache(cls, resolve_sel, cls->ISA(/*authenticated*/true))) {
        // Resolver not implemented.
        return;
    }

    // 
    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(cls, resolve_sel, sel);

    // Cache the result (good or bad) so the resolver doesn't fire next time.
    // +resolveInstanceMethod adds to self a.k.a. cls
    IMP imp = lookUpImpOrNilTryCache(inst, sel, cls);

   ....
}

通俗讲, 动态方法决议其实调用了一个决议方法, 拿实例方法举例就是resolveInstanceMethod, 如果当前imp没有, 会去父类链查找(NSObject有)。resolveInstanceMethod, 而这个方法里面处理了我们查找的sel, 实现、转发等等, 那么系统也算你成功并不会报错。

我们拆分来看

if (!lookUpImpOrNilTryCache(cls, resolve_sel, cls->ISA(/authenticated/true)))

定义一个决议方法SELresolve_sel, 然后走下面if判断, 是否存在

    SEL resolve_sel = @selector(resolveInstanceMethod:);
    // 判断是否存在动态决议resolveInstanceMethod方法
    if (!lookUpImpOrNilTryCache(cls, resolve_sel, cls->ISA(/*authenticated*/true))) {
        // Resolver not implemented.
        // 决议者或者决议方法不存在就直接retrun出去
        return;
    }

其中第三个参数 cls->ISA(/*authenticated*/true)cls->ISA(true)

inline Class
objc_object::ISA(bool authenticated)
{
    ASSERT(!isTaggedPointer());
    return isa.getDecodedClass(authenticated);
}

往下调用isa.getDecodedClass(true)

#if __ARM_ARCH_7K__ >= 2  ||  (__arm64__ && !__LP64__)
#   define SUPPORT_INDEXED_ISA 1
#else
#   define SUPPORT_INDEXED_ISA 0

inline Class
isa_t::getDecodedClass(bool authenticated) {
#if SUPPORT_INDEXED_ISA //真机为1, 否则为0
    // 判断是否为nonpointer, 通常已经初始化完毕的类,  isa中nonpointer为nil
    if (nonpointer) {
        return classForIndex(indexcls);
    } 
    // 返回当前类
    return (Class)cls;
#else
    // 非真机getClass(true)
    return getClass(authenticated);
#endif
}
nonpointer判断

返回lookUpImpOrNilTryCache 可发现是调用_lookUpImpTryCache方法

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

推荐阅读更多精彩内容