iOS 探索objc_msgSend

iOS開發(fā)中,我們常常會(huì)調(diào)用各種方法,既包括對(duì)象方法也包括類方法牍汹,那我們方法調(diào)用內(nèi)部到底是如何實(shí)現(xiàn)的呢铐维?我們今天就來一起探索一下。

一慎菲、objc_msgSendobjc_msgSendSuper

首先嫁蛇,創(chuàng)建工程,并新建一個(gè)LPPerson類露该,并添加一個(gè)對(duì)象方法和一個(gè)類方法睬棚。并在main.m中完成調(diào)用:

@interface LPPerson : NSObject
@property (nonatomic, copy) NSString *lgName;
@property (nonatomic, strong) NSString *nickName;

- (void)sayHello;
+ (void)sayHi;
}
@implementation LPPerson
- (void)sayHello{
    NSLog(@"%s",__func__);
}
+ (void)sayHi{
    NSLog(@"%s",__func__);
}
@end


@interface LPSon : LPPerson

@end


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

然后我們使用clang編譯器胆数,將main.m編譯成main.cpp看下其內(nèi)部結(jié)構(gòu)见秤。因?yàn)榇a很多,并且main在最后撬碟,所以我們直接滑到最后即可:

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 


        LPPerson *person = ((LPPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("LPPerson"), sel_registerName("alloc"));
        ((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayHello"));
        ((void (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("LPPerson"), sel_registerName("sayHi"));

    }
    return 0;
}

可以看到撵摆,不管是對(duì)象方法還是類方法底靠,包括alloc方法他們都是調(diào)用了一個(gè)叫做objc_msgSend的函數(shù)。它的字面意思就是消息發(fā)送特铝,在Objc源碼中進(jìn)行全局查找:

objc_msgSend(id _Nullable self, SEL _Nonnull op, ...)
    OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
objc_msgSendSuper(struct objc_super * _Nonnull super, SEL _Nonnull op, ...)
    OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);

我們看到有objc_msgSendobjc_msgSendSuper這兩個(gè)函數(shù)暑中,他們的都有兩個(gè)參數(shù):

  • 第一個(gè)參數(shù):表示消息接收者
  • 第二個(gè)參數(shù)SEL:表示需要執(zhí)行的方法

既然我們調(diào)用方法就是執(zhí)行了消息發(fā)送,那我們是不是可以直接調(diào)用objc_msgSend或者objc_msgSendSuper呢鲫剿?
我們實(shí)驗(yàn)一下:

  • 1鳄逾、首先導(dǎo)入#import <objc/message.h>
  • 2、在main.m中添加以下代碼:
#import <objc/message.h>
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        
        LPPerson *person = [LPPerson alloc];
        [person sayHello];
        objc_msgSend(person,sel_registerName("sayHello"));
        [LPPerson sayHi];
        objc_msgSend(objc_getClass("LPPerson"),sel_registerName("sayHi"));
    }
    return 0;
}
  • 3灵莲、但是發(fā)現(xiàn)報(bào)錯(cuò)了:
image.png

這是因?yàn)橄到y(tǒng)默認(rèn)開啟的方法檢查严衬,我們需要手動(dòng)關(guān)閉。在target下選中當(dāng)前target笆呆,選擇buildSetting请琳,然后搜索msg,將Enable Strict Checking of objc_msgSend Calls設(shè)置為NO即可:

image.png

現(xiàn)在直接運(yùn)行:

2020-09-22 16:13:47.379526+0800[44411:14029752] -[LPPerson sayHello]
2020-09-22 16:13:47.380326+0800[44411:14029752] -[LPPerson sayHello]
2020-09-22 16:13:47.380457+0800[44411:14029752] +[LPPerson sayHi]
2020-09-22 16:13:47.380536+0800[44411:14029752] +[LPPerson sayHi]

結(jié)果證明赠幕,直接通過objc_msgSend調(diào)用方法是可以的俄精,objc_msgSendSuper也是一樣的,又興趣的同學(xué)可以自己試驗(yàn)一下榕堰。

總結(jié):方法調(diào)用的本質(zhì)就是消息發(fā)送竖慧,具體是調(diào)用runtime中objc_msgSendobjc_msgSendSuper函數(shù)來實(shí)現(xiàn)的。

那么objc_msgSendobjc_msgSendSuper中又是如何查找方法selimp呢逆屡?接下里我們就來從源碼中一探究竟圾旨,因?yàn)?code>objc_msgSend和objc_msgSendSuper內(nèi)部邏輯實(shí)際是一樣的,所以我們接下來主要分析objc_msgSend原理魏蔗。

二砍的、objc_msgSend原理

進(jìn)入源碼中,我們可以發(fā)現(xiàn)objc_msgSend是使用匯編實(shí)現(xiàn)的莺治,這是因?yàn)閰R編主要的特性是:
速度快:匯編更容易被機(jī)器識(shí)別廓鞠。
方法參數(shù)的動(dòng)態(tài)性:匯編調(diào)用函數(shù)時(shí)傳遞的參數(shù)是不確定的帚稠,那么發(fā)送消息時(shí),直接調(diào)用一個(gè)函數(shù)就可以發(fā)送所有的消息:
而在iOS中床佳,方法查找有兩種實(shí)現(xiàn)方式:

  • 快速查找滋早,從cache中查找,也就是我們前面講到的cache_t中存儲(chǔ)的緩存
  • 慢速查找砌们,從methodList中查找以及消息轉(zhuǎn)發(fā)杆麸,下一篇我們會(huì)講到

Objc源碼中搜索objc_msgSend,前面提到了objc_msgSend是基于匯編的浪感,所以我們直接以.s結(jié)尾的文件角溃,然后找到ENTRY _objc_msgSend即可:

image.png

    ENTRY _objc_msgSend
    UNWIND _objc_msgSend, NoFrame
        ///P0是objc_msgSend的第一個(gè)參數(shù),即消息接受者篮撑,這里需要判斷消息接受者是否為空
    cmp p0, #0          // nil check and tagged pointer check
 ///判斷是支持tagged_pointer
#if SUPPORT_TAGGED_POINTERS
    b.le    LNilOrTagged        //  (MSB tagged pointer looks negative)
//再次判斷消息接受者是否為空
#else
    b.eq    LReturnZero
#endif
        ///獲取當(dāng)前消息接受者的isa
    ldr p13, [x0]       // p13 = isa
        ///獲取當(dāng)前消息接受者的class
    GetClassFromIsa_p16 p13     // p16 = class
LGetIsaDone:
    // calls imp or objc_msgSend_uncached
        ///緩存中尋找imp
    CacheLookup NORMAL, _objc_msgSend

接下來减细,我們繼續(xù)查看CacheLookup的源碼:
全局搜索CacheLookup,同樣找.s結(jié)尾的文件赢笨,如下圖所示:

image.png

然后進(jìn)入源碼中:

.macro CacheLookup
LLookupStart$1:

    // p1 = SEL, p16 = isa    
        //第一步:通過內(nèi)存平移16字節(jié)獲取當(dāng)前的mask_buckets
    ldr p11, [x16, #CACHE]              // p11 = mask|buckets

#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
        //第二步:獲取buckets    通過p11 & 0x0000ffffffffffff 得到后48位 buckets
    and p10, p11, #0x0000ffffffffffff   // p10 = buckets
         //第三步:獲取hash 搜索下標(biāo):邏輯右移48位 得到mask未蝌;然后p1 & mask給p12 得到hash存儲(chǔ)的key
    and p12, p1, p11, LSR #48       // x12 = _cmd & mask
         ///此處不會(huì)執(zhí)行
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
    and p10, p11, #~0xf         // p10 = buckets
    and p11, p11, #0xf          // p11 = maskShift
    mov p12, #0xffff
    lsr p11, p12, p11               // p11 = mask = 0xffff >> p11
    and p12, p1, p11                // x12 = _cmd & mask
#else
#error Unsupported cache mask storage for ARM64.
#endif

    //第四步:p12是獲取到的下標(biāo),然后邏輯左移4位茧妒,再由p10(buckets)平移萧吠,得到對(duì)應(yīng)的bucket保存到p12中
    add p12, p10, p12, LSL #(1+PTRSHIFT)
                     // p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
     ///第五步:1、將p12屬性imp 和 sel分別賦值為p17 和 p9
    ldp p17, p9, [x12]      // {imp, sel} = *bucket
///第五步:2桐筏、判斷當(dāng)前bucket的sel和傳入的sel是否相等
1:  cmp p9, p1          // if (bucket->sel != _cmd)
///第五步:3纸型、如果不相同,則跳入2f
    b.ne    2f          //     scan more
///第五步:4梅忌、如果相同狰腌,命中緩存,直接返回imp
    CacheHit $0         // call or return imp
///第五步:5牧氮、 沒有找到 進(jìn)入2f 
2:  // not hit: p12 = not-hit bucket
    CheckMiss $0            // miss if bucket->sel == 0
///第五步:6琼腔、如果p12(在第四步獲取到的bucket) == p10(在第二步獲取到的buckets),說明p12指針已經(jīng)到了buckets的首地址了踱葛。
    cmp p12, p10        // wrap if bucket == buckets
///第五步:7丹莲、如果相等 跳入3f
    b.eq    3f
    ldp p17, p9, [x12, #-BUCKET_SIZE]!  // {imp, sel} = *--bucket
    b   1b          // loop

3:  // wrap: p12 = first bucket, w11 = mask
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
///第五步:8、再將p12的指針指到buckets的最后一個(gè)元素
    add p12, p12, p11, LSR #(48 - (1+PTRSHIFT))
                    // p12 = buckets + (mask << 1+PTRSHIFT)
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
    add p12, p12, p11, LSL #(1+PTRSHIFT)
                    // p12 = buckets + (mask << 1+PTRSHIFT)
#else
#error Unsupported cache mask storage for ARM64.
#endif

    // Clone scanning loop to miss instead of hang when cache is corrupt.
    // The slow path may detect any corruption and halt later.
///第五步:9尸诽、然后在繼續(xù)查找甥材,直到找到或者再次 bucket 與 buckets再次相等,跳出循環(huán)性含。
    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

LLookupEnd$1:
LLookupRecover$1:
3:  // double wrap
    JumpMiss $0

.endmacro


上述流程大概分為5個(gè)步驟洲赵。接下來我們具體分析下:

  • 第一步:獲取mask_buckets
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
......
}

前面我們已經(jīng)分析過objc_class,知道其內(nèi)部結(jié)構(gòu),所以我們?cè)谀玫疆?dāng)前類的首地址后板鬓,因?yàn)?code>isa和superclass各占8個(gè)字節(jié)悲敷,所以我們?cè)谀玫疆?dāng)前類的首地址后究恤,我們平移16個(gè)字節(jié)俭令,即可獲取到cache的地址。

  • 第二步:獲取buckets

同樣的部宿,我們知道在arm64也就是真機(jī)中抄腔,cache的首地址是_maskAndBuckets,我們查看_maskAndBuckets的源碼:

{
    uintptr_t buckets = (uintptr_t)newBuckets;
    uintptr_t mask = (uintptr_t)newMask;
    
    ASSERT(buckets <= bucketsMask);
    ASSERT(mask <= maxMask);
    //maskShift 是 48 
    //將mask左移48位只留下16位理张,剩余的補(bǔ)0赫蛇,
    _maskAndBuckets.store(((uintptr_t)newMask << maskShift) | (uintptr_t)newBuckets, std::memory_order_relaxed);
    _occupied = 0;
}

通過源碼我們可以發(fā)現(xiàn),mask有左右48位雾叭,所以·高16位 | 低48位 = mask | buckets
因此悟耘,我們將p11 & 0x0000ffffffffffff獲取到低48位,即buckets织狐。

  • 第三步:獲取hash 搜索下標(biāo)

在前面cache_t我們有分析到暂幼,方法存儲(chǔ)到cache中,是使用hash算法存儲(chǔ)移迫,其中開始下標(biāo)則是 sel & mask旺嬉。

static inline mask_t cache_hash(SEL sel, mask_t mask) 
{
    return (mask_t)(uintptr_t)sel & mask;
}

所以我們要拿到下標(biāo),就需要分別拿到masksel

  • mask:上面有看到在_maskAndBucketsmask左移48位厨埋,所以我們要取到mask邪媳,只需要_maskAndBuckets右移48位即可

  • selobject_msgSend中傳入的兩個(gè)參數(shù),第一個(gè)是消息接受者荡陷,即isa雨效,也就是P0。第二個(gè)就是sel废赞,即P1

  • 第四步:根據(jù)下標(biāo)找到對(duì)應(yīng)的bucket
#if __arm64__

#if __LP64__
// true arm64

#define SUPPORT_TAGGED_POINTERS 1
#define PTR .quad
#define PTRSIZE 8
#define PTRSHIFT 3  // 1<<PTRSHIFT == PTRSIZE
// "p" registers are pointer-sized
#define UXTP UXTX
...

搜索源碼找到PTRSHIFT设易,發(fā)現(xiàn)它是一個(gè)宏定義,值是3蛹头。而我們知道顿肺,buckets是一個(gè)數(shù)組,如果想得到數(shù)組中的元素 我們可以根據(jù)首地址進(jìn)行指針平移獲取到對(duì)應(yīng)下標(biāo)的值渣蜗。

將第三步獲取的P12開始下標(biāo) 邏輯左移4位 或者 可以理解為 bucket是有selimp兩個(gè)屬性組成屠尊,每個(gè)屬性都是8個(gè)字節(jié)的大小,所以bucket的大小是16耕拷。

buckets指針平移上一步得到的值讼昆,然后將平移后的bucket存到p12中。

  • 第五步:根據(jù)bucket中的sel查找
    • 1骚烧、將bucket中的屬性屬性impsel分別賦值為p17p9
    • 2浸赫、判斷當(dāng)前bucketsel和傳入的sel是否相等:如果相等返回對(duì)應(yīng)imp=>p17;不相等進(jìn)入2f闰围。
    • 3、此時(shí)是不相等既峡,2f部分羡榴,這是一個(gè)循環(huán)。由于匯編中的查找是向上查找运敢,所以p12-1獲取到上一個(gè)bucket指針校仑。如果當(dāng)前p12 bucketbuckets的首地址(第一個(gè)元素)相等,那么就直接跳入3f部分传惠。
    • 4迄沫、此時(shí)是p12 bucketbuckets的首地址(第一個(gè)元素)相等,3f部分卦方。
    • 5羊瘩、maskbuckets數(shù)組的個(gè)數(shù)減一,將mask左移4位盼砍,
    • 6尘吗、將buckets首地址地址平移上一步的結(jié)果,就到了buckets的最后一位衬廷,再將buckets最后一位的指針地址賦值給p12摇予,
    • 7、然后在繼續(xù)進(jìn)行比較sel吗跋,如果有相等就返回相應(yīng)的imp侧戴,如果沒有相等則就繼續(xù)向上查詢。
    • 8跌宛、 如果p12又一次指到的首地址酗宋,那么說明整個(gè)buckets中不存在方法sel,則退出循環(huán)疆拘,并返回
      具體流程可以參考下圖:
      objc_msgSend流程分析.png

覺得不錯(cuò)記得點(diǎn)贊哦蜕猫!聽說看完點(diǎn)贊的人逢考必過,逢獎(jiǎng)必中哎迄。?( ′???` )比心

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末回右,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子漱挚,更是在濱河造成了極大的恐慌翔烁,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,734評(píng)論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件旨涝,死亡現(xiàn)場(chǎng)離奇詭異蹬屹,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,931評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門慨默,熙熙樓的掌柜王于貴愁眉苦臉地迎上來贩耐,“玉大人,你說我怎么就攤上這事厦取〕碧” “怎么了?”我有些...
    開封第一講書人閱讀 164,133評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵蒜胖,是天一觀的道長(zhǎng)消别。 經(jīng)常有香客問我抛蚤,道長(zhǎng)台谢,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,532評(píng)論 1 293
  • 正文 為了忘掉前任岁经,我火速辦了婚禮朋沮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘缀壤。我一直安慰自己樊拓,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,585評(píng)論 6 392
  • 文/花漫 我一把揭開白布塘慕。 她就那樣靜靜地躺著筋夏,像睡著了一般。 火紅的嫁衣襯著肌膚如雪图呢。 梳的紋絲不亂的頭發(fā)上条篷,一...
    開封第一講書人閱讀 51,462評(píng)論 1 302
  • 那天,我揣著相機(jī)與錄音蛤织,去河邊找鬼赴叹。 笑死,一個(gè)胖子當(dāng)著我的面吹牛指蚜,可吹牛的內(nèi)容都是我干的乞巧。 我是一名探鬼主播,決...
    沈念sama閱讀 40,262評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼摊鸡,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼绽媒!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起免猾,我...
    開封第一講書人閱讀 39,153評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤是辕,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后掸刊,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體免糕,經(jīng)...
    沈念sama閱讀 45,587評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,792評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了石窑。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片牌芋。...
    茶點(diǎn)故事閱讀 39,919評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖松逊,靈堂內(nèi)的尸體忽然破棺而出躺屁,到底是詐尸還是另有隱情,我是刑警寧澤经宏,帶...
    沈念sama閱讀 35,635評(píng)論 5 345
  • 正文 年R本政府宣布犀暑,位于F島的核電站,受9級(jí)特大地震影響烁兰,放射性物質(zhì)發(fā)生泄漏耐亏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,237評(píng)論 3 329
  • 文/蒙蒙 一沪斟、第九天 我趴在偏房一處隱蔽的房頂上張望广辰。 院中可真熱鬧,春花似錦主之、人聲如沸择吊。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,855評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽几睛。三九已至,卻和暖如春粤攒,著一層夾襖步出監(jiān)牢的瞬間所森,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,983評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工琼讽, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留必峰,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,048評(píng)論 3 370
  • 正文 我出身青樓钻蹬,卻偏偏與公主長(zhǎng)得像吼蚁,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子问欠,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,864評(píng)論 2 354