IOS底层原理之objc_msgSend

一、clang指令探查方法调用

Clang是一个由Apple主导编写,基于LLVM的C/C++/Objective-C编译器。如果你不知道clang,可以在这里找到你想要的。

在工程目录中的main.m文件目录下进入到终端,输入如下命令

clang -rewrite-objc main.m -o main.cpp

该命令会将main.m编译成C++的代码,但是不同平台支持的代码肯定是不一样的。

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc OC源文件 -o 输出的CPP文件
如果需要链接其他框架,使用-framework参数。比如-framework UIKit

在终端输入命令以后,会生成一个main.cpp文件。打开main.cpp文件,直接将代码拉到最下面,我们会看到这样的一段代码。

int main(int argc, const char *argv[])
{
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        Person *person = ((Person *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person"), sel_registerName("alloc"));
        ((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayHello"));
    }
    return 0;
}

可以看到在OC层面调用的sayHello方法于底层而言是调用了一个objc_msgSend方法,那么我们可以确认的是方法的调用其实是调用的objc_msgSend。

二、objc_msgSend底层实现

苹果公司开源了objc_msgSend的底层![代码]{https://opensource.apple.com/source/objc4/objc4-750/runtime/Messengers.subproj/},是用汇编语言编写的,其目的就是为了提高函数的执行速度。苹果公司提供诸多平台架构的汇编代码,我这里是针对arm64平台的汇编代码(objc-msg-arm64.s)进行分析。

1. 函数入口

全局搜索ENTRY _objc_msgSend,这个就是objc_msgSend汇编代码的入口。

ENTRY _objc_msgSend
    UNWIND _objc_msgSend, NoFrame
    cmp p0, #0          // nil check and tagged pointer check
#if SUPPORT_TAGGED_POINTERS
    b.le    LNilOrTagged        //  (MSB tagged pointer looks negative)
#else
    b.eq    LReturnZero
#endif

cmp是一个判断指令。在这里判断p0是否是0,如果为0则表示传入的对象为nil,立即返回。
实际上SUPPORT_TAGGED_POINTERS的值定义为1,其定义在arm64-asm.h里面。b.le指令用来判断上面的cmp的值是否小于等于执行标号,否则直接往下走。如果p0<0,则表示传入的对象是tagged pointer。在这里我们不去讨论tagged pointer的情况。程序继续往下走,执行如下代码

ldr p13, [x0]       // p13 = isa
GetClassFromIsa_p16 p13     // p16 = class

在这里将x0指向内存地址的值isa赋值给p13,然后通过GetClassFromIsa_p16拿到class的地址。接下来CacheLookup流程,从缓存中查询。

2、CacheLookup

来到CacheLookup流程,已经将class的地址赋值给了p16。

ldp p10, p11, [x16, #CACHE] // p10 = buckets, p11 = occupied|mask
#define SUPERCLASS       __SIZEOF_POINTER__
#define CACHE            (2 * __SIZEOF_POINTER__)

这里将class地址的偏移CACHE得到的地址给到p10和p11。superclass占用8个字节,所以这里的偏移量是16字节。而类的底层定义是一个结构体:

struct objc_class : objc_object {
    Class ISA;
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
    ...
};

isa占用8字节,superclass占用8字节,所以类地址偏移16字节可以得到cache。而对于cache_t结构体的定义如下:

struct cache_t {
    struct bucket_t *_buckets;
    mask_t _mask;
    mask_t _occupied;
    ...
};

buckets是结构体指针,占用8字节,mask占4字节,occupied占4字节,因此p16偏移16字节后得到buckets存储在p10,p11存了mask和occupied,其中低32位表示mask,高32位表示occupied。

and w12, w1, w11        // x12 = _cmd & mask
add p12, p10, p12, LSL #(1+PTRSHIFT)  // p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))

p1是SEL,其低32位w1表示的时候SEL对应的key,将key和mask相与得到函数方法在buckets哈希表中的索引。p10是buckets的首地址,而bucket_t结构体占用16字节,所以buckets的首地址加上索引向左偏移4字节得到的值就是函数方法在缓存中的地址。因此p12就是函数方法对应的bucket地址。

ldp p17, p9, [x12]      // {imp, sel} = *bucket

将bucket装在到p17和p9中,p17中存放imp,p9中存放key也就是sel。

1:  cmp p9, p1          // if (bucket->sel != _cmd)
    b.ne    2f          //     scan more
    CacheHit $0         // call or return imp

将找到的sel和传入的sel进行比较,如果相同就表示已经找到了执行CacheHit,否则执行2继续查找。

2:  // not hit: p12 = not-hit bucket
    CheckMiss $0            // miss if bucket->sel == 0
    cmp p12, p10        // wrap if bucket == buckets
    b.eq    3f
    ldp p17, p9, [x12, #-BUCKET_SIZE]!  // {imp, sel} = *--bucket
    b   1b          // loop

在这一步对buckets的首地址p10和我们找到的bucket的地址p12进行比较 ,如果不相等则查找前一个bucket,并跳回到1执行,否则跳到3执行。

3:  // wrap: p12 = first bucket, w11 = mask
    add p12, p12, w11, UXTW #(1+PTRSHIFT)
                                // p12 = buckets + (mask << 1+PTRSHIFT)

    // Clone scanning loop to miss instead of hang when cache is corrupt.
    // The slow path may detect any corruption and halt later.
    ldp p17, p9, [x12]      // {imp, sel} = *bucket

在这里其实拿到的就是buckets中的第一个bucket,p12 = first bucket。继续往下执行。接下来的操作其实和上面的执行流程是一样的,唯一不同的是3执行的是JumpMiss

1:  cmp p9, p1          // if (bucket->sel != _cmd)
    b.ne    2f          //     scan more
    CacheHit $0         // call or return imp
2:  // not hit: p12 = not-hit bucket
    CheckMiss $0            // miss if bucket->sel == 0
    cmp p12, p10        // wrap if bucket == buckets
    b.eq    3f
    ldp p17, p9, [x12, #-BUCKET_SIZE]!  // {imp, sel} = *--bucket
    b   1b          // loop
3:  // double wrap
    JumpMiss $0

3、CacheHit

从上面的流程分析我们知道了如果在缓存中找到了和传入的一直的函数方法就会执行CacheHit。我们来看下CacheHit做了什么。

// CacheHit: x17 = cached IMP, x12 = address of cached IMP
.macro CacheHit
.if $0 == NORMAL
    TailCallCachedImp x17, x12  // authenticate and call imp
.elseif $0 == GETIMP
    mov p0, p17
    AuthAndResignAsIMP x0, x12  // authenticate imp and re-sign as IMP
    ret             // return IMP
.elseif $0 == LOOKUP
    AuthAndResignAsIMP x17, x12 // authenticate imp and re-sign as IMP
    ret             // return imp via x17
.else
.abort oops
.endif
.endmacro

走这一步是已经在缓存找到了相应的函数方法,p17(x17)中存储了imp,p12(x12)中存放了imp的地址,TailCallCachedImp直接调用函数方法。

4、JumpMiss

.macro JumpMiss
.if $0 == GETIMP
    b   LGetImpMiss
.elseif $0 == NORMAL
    b   __objc_msgSend_uncached
.elseif $0 == LOOKUP
    b   __objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro

走到JumpMiss来则表示在缓存中并没有找到对应的函数方法,则会跳到__objc_msgSend_uncached执行MethodTableLookup

5、MethodTableLookup

.macro MethodTableLookup
    
    // push frame
    SignLR
    stp fp, lr, [sp, #-16]!
    mov fp, sp

    // save parameter registers: x0..x8, q0..q7
    sub sp, sp, #(10*8 + 8*16)
    stp q0, q1, [sp, #(0*16)]
    stp q2, q3, [sp, #(2*16)]
    stp q4, q5, [sp, #(4*16)]
    stp q6, q7, [sp, #(6*16)]
    stp x0, x1, [sp, #(8*16+0*8)]
    stp x2, x3, [sp, #(8*16+2*8)]
    stp x4, x5, [sp, #(8*16+4*8)]
    stp x6, x7, [sp, #(8*16+6*8)]
    str x8,     [sp, #(8*16+8*8)]

    // receiver and selector already in x0 and x1
    mov x2, x16
    bl  __class_lookupMethodAndLoadCache3

    // IMP in x0
    mov x17, x0
    
    // restore registers and return
    ldp q0, q1, [sp, #(0*16)]
    ldp q2, q3, [sp, #(2*16)]
    ldp q4, q5, [sp, #(4*16)]
    ldp q6, q7, [sp, #(6*16)]
    ldp x0, x1, [sp, #(8*16+0*8)]
    ldp x2, x3, [sp, #(8*16+2*8)]
    ldp x4, x5, [sp, #(8*16+4*8)]
    ldp x6, x7, [sp, #(8*16+6*8)]
    ldr x8,     [sp, #(8*16+8*8)]

    mov sp, fp
    ldp fp, lr, [sp], #16
    AuthenticateLR

.endmacro

MethodTableLookup中的这些操作其实是在从bits中的方法列表去找函数方法,这篇文章中有分析bits。最终跳到__class_lookupMethodAndLoadCache3去执行。从这里开始进入到方法的查找流程。

三、方法查找

从上面的objc_msgSend汇编源码分析来看,当在缓存cache中未能命中方法的时候,最终会走到__class_lookupMethodAndLoadCache3。__class_lookupMethodAndLoadCache3对应上层C实现的_class_lookupMethodAndLoadCache3方法,该方法定义在objc-runtime-new.mm中。

1、_class_lookupMethodAndLoadCache3方法

IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
    return lookUpImpOrForward(cls, sel, obj, 
                              YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}

obj 类的实例对象
sel 方法的名称
cls 类

_class_lookupMethodAndLoadCache3调用了lookUpImpOrForward方法,我们看到这的cache传入的是NO,表示函数方法没有缓存命中,resolver是消息的接受者。

2、lookUpImpOrForward方法的准备工作

lookUpImpOrForward的代码实现如下:

IMP lookUpImpOrForward(Class cls, SEL sel, id inst, 
                       bool initialize, bool cache, bool resolver)
{
    IMP imp = nil;
    bool triedResolver = NO;

    runtimeLock.assertUnlocked();

    // Optimistic cache lookup
    if (cache) {
        imp = cache_getImp(cls, sel);
        if (imp) return imp;
    }

    // runtimeLock is held during isRealized and isInitialized checking
    // to prevent races against concurrent realization.

    // runtimeLock is held during method search to make
    // method-lookup + cache-fill atomic with respect to method addition.
    // Otherwise, a category could be added but ignored indefinitely because
    // the cache was re-filled with the old value after the cache flush on
    // behalf of the category.

    runtimeLock.lock();
    checkIsKnownClass(cls);

    if (!cls->isRealized()) {
        realizeClass(cls);
    }

    if (initialize  &&  !cls->isInitialized()) {
        runtimeLock.unlock();
        _class_initialize (_class_getNonMetaClass(cls, inst));
        runtimeLock.lock();
        // If sel == initialize, _class_initialize will send +initialize and 
        // then the messenger will send +initialize again after this 
        // procedure finishes. Of course, if this is not being called 
        // from the messenger then it won't happen. 2778172
    }
  ...
}

这里的代码是lookUpImpOrForward函数方法的部分代码,这样一段代码做了一下几步准备工作。

  1. runtimeLock.lock() 加锁避免在多线程的情况下出现错乱的情况。
  2. checkIsKnownClass(cls) 判断class的有效性。
  3. realizeClass(cls)class_rw_tclass_ro_t中加载方法,具体的可以参阅realizeClass方法的实现。

做好上面的准备工作后,程序会执行retry的代码开始方法的查找。其实这里还是会到类的缓存中再去查找一遍。

3、 再去缓存中查找

为了避免在多线程的情况下可能存在方法缓存慢于方法命中的情况,会再次去缓存中查找一次方法。

imp = cache_getImp(cls, sel);
if (imp) goto done;

在这里cache_getImp其实是汇编中_cache_getImp上层C代码映射。

STATIC_ENTRY _cache_getImp
GetClassFromIsa_p16 p0
CacheLookup GETIMP

重新走CacheLookup流程从缓存中查找,如果在缓存中有查找到则直接goto done释放锁,返回imp,结束方法调用,否则会先从本类中开始查找方法。

4、本类中查找

如果方法在缓存中未能找到,会在本类的方法列表中查找方法的实现。

 // Try this class's method lists.
Method meth = getMethodNoSuper_nolock(cls, sel);
if (meth) {
    log_and_fill_cache(cls, meth->imp, sel, inst, cls);
    imp = meth->imp;
    goto done;
}

这段是在本类的方法列表中查找方法实现的代码。调用getMethodNoSuper_nolock从方法列表中查找,如果查找到则调用log_and_fill_cache方法进行方法的缓存,goto done释放锁,返回imp。这里的查找算法是一个二分查找算法。如果本类中没有方法的实现,便会从类的父类中查找方法的实现。

5、父类中查找

如果我们调用的方法在本类中未能实现,则会从父类的方法列表中查找。

 {
        unsigned attempts = unreasonableClassCount();
        for (Class curClass = cls->superclass;
             curClass != nil;
             curClass = curClass->superclass)
        {
            // Halt if there is a cycle in the superclass chain.
            if (--attempts == 0) {
                _objc_fatal("Memory corruption in class list.");
            }
            
            // Superclass cache.
            imp = cache_getImp(curClass, sel);//父类缓存中查找
            if (imp) {
                if (imp != (IMP)_objc_msgForward_impcache) {//是否是消息转发的方法
                    // Found the method in a superclass. Cache it in this class.
                    log_and_fill_cache(cls, imp, sel, inst, curClass);
                    goto done;
                }
                else {
                    // Found a forward:: entry in a superclass.
                    // Stop searching, but don't cache yet; call method 
                    // resolver for this class first.
                    break;
                }
            }
            
            // Superclass method list.
            Method meth = getMethodNoSuper_nolock(curClass, sel);
            if (meth) {
                log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
                imp = meth->imp;
                goto done;
            }
        }
    }

1、先从父类的缓存中查找,如果在缓存中有找到,则会先判断是否是消息转发的方法。
2、如果是消息转发的方法则会走消息转发的流程,终止方法的查找。
3、如果是非消息转发的方法则会调用log_and_fill_cache进行方法的缓存,终止方法的查找并执行方法。
4、如果在父类的缓存中没有找到,则会从父类的方法列表中查找,如果找到了则会调用log_and_fill_cache进行方法的缓存,终止方法的查找并执行方法。
5、如果在父类的方法列表中没有找到,重复执行1、3、4步骤,直到父类为nil为止。
6、如果直到父类为nil还是未能找到方法的实现,则会走动态方法解析流程。

四、总结

1、方法调用的底层实现是objc_msgSend,即方法的本质是消息发送。
2、objc_msgSend是用汇编实现的。objc_msgSend从缓存中查找方法,如果有查找到就会执行方法,否则会去调用的_class_lookupMethodAndLoadCache3这样的一个C函数进行方法的查找。
3、_class_lookupMethodAndLoadCache3方法中会做一些准备的工作,然后会再次汇编查找一次缓存,如果找到就执行方法,否则会从本类的方法列表中查找。
4、在本类的方法列表中没有找到则去父类的缓存中查找,如果有查找到则会判断是否走消息转发流程。否则去父类的方法列表中查找。
5、如果在本类缓存、本类方法列表、父类缓存、父类方法列表中都未找到,走动态方法解析流程。

五、参考资料

汇编指令
方法缓存cache_t
深入OC底层探索NSObject的结构
动态方法解析和消息转发

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念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

推荐阅读更多精彩内容