iOS - objc_msgSend分析

Objective-C 是一個動態(tài)語言,在動態(tài)中創(chuàng)建類和對象婴削、進行消息傳遞和轉(zhuǎn)發(fā)嘁酿。想要更好的理解 Objective-C 那就離不開 Runtime(運行時) 隙券。

什么是Runtime?

Runtime是用C、C++闹司、匯編編寫的一套為OC提供運行時功能的api

初見objc_msgSend

創(chuàng)建一個Student的類

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Student *student = [[Student alloc] init];
        [student study]; 
    }
    return 0;
}

在終端使用clang命令將main.m編譯成main.cpp

clang -rewrite-objc main.m

此時我們會發(fā)現(xiàn)main.m文件的下方會多出一個main.cpp的文件,打開并移動到最下方我們看到如下一段代碼:

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

我們發(fā)現(xiàn)在student創(chuàng)建和調(diào)用study的方法是,都調(diào)用了objc_msgSend,那么objc_msgSend到底是什么?其作用又是什么呢?

通過匯編初探objc_msgSend

通過打斷點,查看匯編我們發(fā)現(xiàn)objc_msgSend的實現(xiàn)是在libobjc動態(tài)鏈接庫中(objc4源碼)娱仔。在libobjc源碼庫中全局搜索objc_msgSend,發(fā)現(xiàn)它的實現(xiàn)是用匯編寫的游桩。
下面我們就以arm64系統(tǒng)下的匯編就行分析,全部搜索objc_msgSend找到objc-msg-arm64.s文件,找到ENTRY _objc_msgSend,方法的進入是從ENTRY開始的:


    ENTRY _objc_msgSend
    UNWIND _objc_msgSend, NoFrame

    cmp p0, #0 //arm64下有31位寄存器; 對比0號寄存器是否為空, 如果為空則代表當前接受著沒有,如果為空則返回nil

#if SUPPORT_TAGGED_POINTERS
    b.le    LNilOrTagged        //  (MSB tagged pointer looks negative)
#else
    b.eq    LReturnZero
#endif
    // [x0]是消息接收者
   //如果我們要對student發(fā)送消息,我們需要用到用到對象方法時我們就可以通過isa找到類Student,如果是類方法是我們則需要isa來找到元類
    ldr p13, [x0]       // p13 = isa
//通過isa尋找類
    GetClassFromIsa_p16 p13     // p16 = class
LGetIsaDone://查找isa完畢
    CacheLookup NORMAL//讀取方法        // calls imp or objc_msgSend_uncached

#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
    b.eq    LReturnZero     // nil check
GetClassFromIsa_p16
.macro GetClassFromIsa_p16 /* src */

#if SUPPORT_INDEXED_ISA
    // Indexed isa
    mov p16, $0         // optimistically set dst = src
//判斷當前是否為non-pointer isa 
    tbz p16, #ISA_INDEX_IS_NPI_BIT, 1f  // done if not non-pointer isa
    // isa in p16 is indexed
    adrp    x10, _objc_indexed_classes@PAGE
    add x10, x10, _objc_indexed_classes@PAGEOFF
    ubfx    p16, p16, #ISA_INDEX_SHIFT, #ISA_INDEX_BITS  // extract index
    ldr p16, [x10, p16, UXTP #PTRSHIFT] // load class from array
1:

#elif __LP64__
    // 64-bit packed isa
    and p16, $0, #ISA_MASK

#else
    // 32-bit raw isa
    mov p16, $0

#endif

.endmacro
CacheLookup
.macro CacheLookup
    // p1 = SEL, p16 = isa
    ldp p10, p11, [x16, #CACHE]//在cache_t一文中我們提到了要拿到方法緩存需要首地址平移16個字節(jié)    // p10 = buckets, p11 = occupied|mask
#if !__LP64__//w11用w是因為mask是32位的,不需要用64位的
    and w11, w11, 0xffff    // p11 = mask
#endif
/**
static inline mask_t cache_hash(cache_key_t key, mask_t mask) 
{
    return (mask_t)(key & mask);
}
*/
    and w12, w1, w11        // x12 = _cmd & mask
    add p12, p10, p12, LSL #(1+PTRSHIFT)
                     // p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))

    ldp p17, p9, [x12]      // {imp, sel} = *bucket
1:  cmp p9, p1          // if (bucket->sel != _cmd)
    b.ne    2f  //沒找到就繼續(xù)找2流程        //     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//找到了,再去走3流程,是為了存一份緩存,方便下一次填充緩存
    ldp p17, p9, [x12, #-BUCKET_SIZE]!  // {imp, sel} = *--bucket
    b   1b          // loop

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
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
    
.endmacro
如果緩存沒有命中的話會走CheckMiss方法
//CacheLookup NORMAL|GETIMP|LOOKUP 
.macro CheckMiss
    // miss if bucket->sel == 0
.if $0 == GETIMP
    cbz p9, LGetImpMiss
.elseif $0 == NORMAL
    cbz p9, __objc_msgSend_uncached
.elseif $0 == LOOKUP
    cbz p9, __objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro
NORMAL 模式下走__objc_msgSend_uncached方法
STATIC_ENTRY __objc_msgSend_uncached
    UNWIND __objc_msgSend_uncached, FrameWithNoSaves

    // THIS IS NOT A CALLABLE C FUNCTION
    // Out-of-band p16 is the class to search
    
    MethodTableLookup
    TailCallFunctionPointer x17

    END_ENTRY __objc_msgSend_uncached

有上面代碼可知,接下來會進入MethodTableLookup流程:

MethodTableLookup
//方法存在bits->rw->ro的methodList中
.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

由上面匯編可知,其最終要查找的是__class_lookupMethodAndLoadCache3方法,當我們在匯編中并搜索不到該方法的實現(xiàn),通過前面x0..x8, q0..q7的準備工作我們猜想,其可能是為調(diào)用C或者C++的方法做準備,當我們?nèi)炙阉鱛class_lookupMethodAndLoadCache3時發(fā)現(xiàn)在objc-rutime-new.mm文件中找到了該方法的實現(xiàn)

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

這個函數(shù)開始就是我們熟知的C/C++的代碼了牲迫,我們終于也不用再看匯編部分的代碼了,消息發(fā)送從快速查找過渡到了慢速查找流程。

總結(jié):

在編譯期調(diào)用 objc_msgSend 函數(shù) , 在匯編代碼執(zhí)行緩存查找 sel 對應的 imp , 找到就會返回調(diào)用 , 找不到則由快速查找過渡到了慢速查找流程借卧。

拓展isKindOfClass和isMemberOfClass
int main(int argc, const char * argv[]) {
    @autoreleasepool {

        BOOL re1 = [(id)[NSObject class] isKindOfClass:[NSObject class]];       //
        BOOL re2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]];     //
        BOOL re3 = [(id)[LGPerson class] isKindOfClass:[LGPerson class]];       //
        BOOL re4 = [(id)[LGPerson class] isMemberOfClass:[LGPerson class]];     //
        NSLog(@" re1 :%hhd\n re2 :%hhd\n re3 :%hhd\n re4 :%hhd\n",re1,re2,re3,re4);

        BOOL re5 = [(id)[NSObject alloc] isKindOfClass:[NSObject class]];       //
        BOOL re6 = [(id)[NSObject alloc] isMemberOfClass:[NSObject class]];     //
        BOOL re7 = [(id)[LGPerson alloc] isKindOfClass:[LGPerson class]];       //
        BOOL re8 = [(id)[LGPerson alloc] isMemberOfClass:[LGPerson class]];     //
        NSLog(@" re5 :%hhd\n re6 :%hhd\n re7 :%hhd\n re8 :%hhd\n",re5,re6,re7,re8);
    }
    return 0;
}
+ (BOOL)isKindOfClass:(Class)cls;
+ (BOOL)isKindOfClass:(Class)cls {
    for (Class tcls = object_getClass((id)self); tcls; tcls = tcls->superclass) {
        if (tcls == cls) return YES;
    }
    return NO;
}

由self獲取到元類,如果相等就返回YES,不相等就繼續(xù)找元類的父類也就是NSObject, [NSObject class]的類明顯就是NSObject因此[(id)[NSObject class] isKindOfClass:[NSObject class]]返回值為1,同理[(id)[LGPerson class] isKindOfClass:[LGPerson class]]的類為LGPerson,無論是元類還是元類的父類都不可能為LGPerson,因此此處返回為0;

+ (BOOL)isMemberOfClass:(Class)cls;
+ (BOOL)isMemberOfClass:(Class)cls {
//就是元類和類做對比,如果相等返回1,不想等返回0
    return object_getClass((id)self) == cls;
}

元類和類肯定是不一樣的,因此[(id)[NSObject class] isMemberOfClass:[NSObject class]]和[(id)[LGPerson class] isMemberOfClass:[LGPerson class]]返回值皆為0;

- (BOOL)isKindOfClass:(Class)cls;
- (BOOL)isKindOfClass:(Class)cls {
    for (Class tcls = [self class]; tcls; tcls = tcls->superclass) {
        if (tcls == cls) return YES;
    }
    return NO;
}

第一步都是對比類,例如: [(id)[NSObject alloc] isKindOfClass:[NSObject class]]第一步就是對比該類是不是NSObject類,如果是直接返回1,明顯[NSObject alloc]就是NSObject類,所以此時返回為1,同理 [(id)[LGPerson alloc] isKindOfClass:[LGPerson class]] 返回值也為1;

- (BOOL)isMemberOfClass:(Class)cls;
- (BOOL)isMemberOfClass:(Class)cls {
    return [self class] == cls;
}

對比cls和[self class]是都相同,和isKindOfClass區(qū)別在于, isKindOfClass如果第一步不同的時候還會往父類去查找,容錯更高一些,明顯[(id)[NSObject alloc] isMemberOfClass:[NSObject class]]和[(id)[LGPerson alloc] isMemberOfClass:[LGPerson class]]返回值皆為1;

至此我們可以知道答應的結(jié)果應該是:

2020-01-27 23:03:06.936314+0800 Test[8438:1379585]  re1 :1
 re2 :0
 re3 :0
 re4 :0
2020-01-27 23:03:06.938168+0800 Test[8438:1379585]  re5 :1
 re6 :1
 re7 :1
 re8 :1
最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末盹憎,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子谓娃,更是在濱河造成了極大的恐慌脚乡,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,826評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件滨达,死亡現(xiàn)場離奇詭異奶稠,居然都是意外死亡,警方通過查閱死者的電腦和手機捡遍,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,968評論 3 395
  • 文/潘曉璐 我一進店門锌订,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人画株,你說我怎么就攤上這事辆飘。” “怎么了谓传?”我有些...
    開封第一講書人閱讀 164,234評論 0 354
  • 文/不壞的土叔 我叫張陵蜈项,是天一觀的道長。 經(jīng)常有香客問我续挟,道長紧卒,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,562評論 1 293
  • 正文 為了忘掉前任诗祸,我火速辦了婚禮跑芳,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘直颅。我一直安慰自己博个,他們只是感情好,可當我...
    茶點故事閱讀 67,611評論 6 392
  • 文/花漫 我一把揭開白布功偿。 她就那樣靜靜地躺著盆佣,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上罪塔,一...
    開封第一講書人閱讀 51,482評論 1 302
  • 那天投蝉,我揣著相機與錄音,去河邊找鬼征堪。 笑死瘩缆,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的佃蚜。 我是一名探鬼主播庸娱,決...
    沈念sama閱讀 40,271評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼谐算!你這毒婦竟也來了熟尉?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,166評論 0 276
  • 序言:老撾萬榮一對情侶失蹤洲脂,失蹤者是張志新(化名)和其女友劉穎斤儿,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體恐锦,經(jīng)...
    沈念sama閱讀 45,608評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡往果,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,814評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了一铅。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片陕贮。...
    茶點故事閱讀 39,926評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖潘飘,靈堂內(nèi)的尸體忽然破棺而出肮之,到底是詐尸還是另有隱情,我是刑警寧澤卜录,帶...
    沈念sama閱讀 35,644評論 5 346
  • 正文 年R本政府宣布戈擒,位于F島的核電站,受9級特大地震影響艰毒,放射性物質(zhì)發(fā)生泄漏筐高。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,249評論 3 329
  • 文/蒙蒙 一现喳、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧犬辰,春花似錦嗦篱、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,866評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春浴栽,著一層夾襖步出監(jiān)牢的瞬間荒叼,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,991評論 1 269
  • 我被黑心中介騙來泰國打工典鸡, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留被廓,地道東北人。 一個月前我還...
    沈念sama閱讀 48,063評論 3 370
  • 正文 我出身青樓萝玷,卻偏偏與公主長得像嫁乘,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子球碉,可洞房花燭夜當晚...
    茶點故事閱讀 44,871評論 2 354