OC底層探索之objc_msgSend

runtime

我們都知道大部分語言是編譯時決議的,而Object-C是在運(yùn)行時決議方淤,這來源于強(qiáng)大的runtime引几。通過runtime可以動態(tài)對類各方面進(jìn)行配置,還有就是消息傳遞爸舒。消息傳遞其實就是通過objc_msgSend按照sel找到函數(shù)imp的過程。

objc_msgSend

新建一個工程稿蹲,在main.m文件夾內(nèi)創(chuàng)建一個LGPerson類。在main函數(shù)內(nèi)部調(diào)用[p study]鹊奖,[p happy]苛聘。

@interface LGPerson : NSObject

- (void)study;
- (void)happy;
+ (void)eat;

@end

@implementation LGPerson

- (void)study {
    NSLog(@"%s",__func__);
}
- (void)happy {
    NSLog(@"%s",__func__);
}
+ (void)eat {
    NSLog(@"%s",__func__);
}

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        LGPerson *p = [LGPerson alloc];
        [p study];
        [p happy];
        
    }
    return NSApplicationMain(argc, argv);
}

通過clang探索objc_msgSend

使用clang編譯,打開該工程目錄輸入clang -rewrite-objc main.m忠聚,然后打開該工程目下main.cpp文件设哗,我們可以看到[p study][p happy]被編譯成了objc_msgSend()函數(shù)两蟀。第一個參數(shù)是接受者网梢,第二個參數(shù)是方法名,系統(tǒng)會根據(jù)這2個參數(shù)找到方法具體實現(xiàn)赂毯。

LGPerson *p = ((LGPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("LGPerson"), sel_registerName("alloc"));
        ((void (*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("study"));
        ((void (*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("happy"));

我們把 [p study]直接改為編譯后的方法((void (*)(id, SEL))(void *)objc_msgSend)((id)p, NSSelectorFromString(@"study")),直接運(yùn)行驗證战虏,我們可以看到study方法是可以執(zhí)行拣宰。

編譯后的方法驗證

我們在study方法加個參數(shù)str,看看clang編譯文件烦感,打開該工程目錄輸入clang -rewrite-objc main.m巡社,然后打開該工程目下main.cpp文件。

加上參數(shù)的objc_msgSend

我們可以看到如果有參數(shù)的話手趣,系統(tǒng)編譯會自動加上相應(yīng)的參數(shù)晌该。

((void (*)(id, SEL, NSString *))(void *)objc_msgSend)((id)p, sel_registerName("study:"), (NSString *)&__NSConstantStringImpl__var_folders_mx_2ljwkcpn0kg_4m1bgs1507f00000gn_T_main_5b862f_mi_3);

objc_msgSend的種類

我們查看main.cpp文件的頂部。我們發(fā)現(xiàn)有5種objc_msgSend绿渣。

  • objc_msgSend : 發(fā)消息給本類
  • objc_msgSendSuper : 發(fā)消息給父類
  • objc_msgSend_stret : 發(fā)消息返回值是一個結(jié)構(gòu)體
  • objc_msgSendSuper_stret : 發(fā)消息給父類朝群,返回值是一個結(jié)構(gòu)體
  • objc_msgSend_fpret : 發(fā)消息返回值是一個浮點(diǎn)型的
    objc_msgSend的種類

objc_msgSendSuper

首先我們來個經(jīng)典的面試題,新建一個LGPerson中符,還是申明一個study方法姜胖,再新建一個LGTeacher繼承自LGPerson,在LGTeacher的init方法里面打印[self class][super class]舟茶。

@interface LGPerson : NSObject

-(void)study;

@end
@interface LGTeacher : LGPerson

@end
@implementation LGTeacher

-(instancetype)init {
    if (self = [super init]) {
        NSLog(@"%@",[self class]);
        NSLog(@"%@",[super class]);
    }
    return self;
}
@end

初始化一個LGTeacher的實例對象谭期,我們通常會認(rèn)為第一個打印是LGTeacher,第二的打印位LGPerson吧凉∷沓觯看看是不是啊,運(yùn)行阀捅。

初始化LGTeacher實例對象

我們可以看到運(yùn)行結(jié)果2個打印都是LGTeacher胀瞪。這是為什么呢?使用clang編譯饲鄙,打開該工程目錄輸入clang -rewrite-objc LGTeacher.m凄诞,打開LGTeacher.cpp文件,找到init函數(shù),然后我們可以看到了[super class]其實就是調(diào)用了objc_msgSendSuper函數(shù)忍级。
運(yùn)行結(jié)果

objc_msgSendSuper

那我看蘋果官方文檔是怎么解釋objc_msgSendSuper這個函數(shù)的帆谍,首先打開Xcode菜單欄的help然后點(diǎn)擊Developer Documentation,選擇Objective-C轴咱,然后點(diǎn)擊搜索框搜索objc_msgSendSuper汛蝙。
Developer Documentation

objc_msgSendSuper官方文檔

查看objc_msgSendSuper官方文檔。

  • Parameters
    super
    A pointer to an objc_super data structure. Pass values identifying the context the message was sent to, including the instance of the class that is to receive the message and the superclass at which to start searching for the method implementation.
    op
    A pointer of type SEL. Pass the selector of the method that will handle the message.
    ...
    A variable argument list containing the arguments to the method.

  • Return Value
    The return value of the method identified by op.

  • Discussion
    When it encounters a method call, the compiler generates a call to one of the functions objc_msgSend, objc_msgSend_stret, objc_msgSendSuper, or objc_msgSendSuper_stret. Messages sent to an object’s superclass (using the super keyword) are sent using objc_msgSendSuper; other messages are sent using objc_msgSend. Methods that have data structures as return values are sent using objc_msgSendSuper_stret and objc_msgSend_stret.

  • 參數(shù)
    父類
    指向 objc_super 數(shù)據(jù)結(jié)構(gòu)的指針朴肺。 傳遞標(biāo)識消息發(fā)送到的上下文的值窖剑,包括要接收消息的類的實例和開始搜索方法實現(xiàn)的超類。
    操作
    SEL 類型的指針戈稿。 傳遞將處理消息的方法的選擇器西土。
    ...
    包含方法參數(shù)的變量參數(shù)列表。

  • 返回值
    op 標(biāo)識的方法的返回值鞍盗。

  • 討論
    當(dāng)遇到方法調(diào)用時需了,編譯器生成對函數(shù) objc_msgSend跳昼、objc_msgSend_stret、objc_msgSendSuper 或 objc_msgSendSuper_stret 之一的調(diào)用援所。 發(fā)送到對象超類的消息(使用 super 關(guān)鍵字)使用 objc_msgSendSuper 發(fā)送庐舟; 其他消息使用 objc_msgSend 發(fā)送。 將數(shù)據(jù)結(jié)構(gòu)作為返回值的方法使用 objc_msgSendSuper_stret 和 objc_msgSend_stret 發(fā)送住拭。
    我們查看蘋果官方文件可以查看到(使用 super 關(guān)鍵字)使用 objc_msgSendSuper 發(fā)送挪略,所以說我們前文[super class]是實際是調(diào)用objc_msgSendSuper函數(shù)的,接受者是類的實例滔岳。
    我們可以看到main.app內(nèi)部[super class],其實接受者還是self一個LGTeacher的實例對象杠娱,所以說消息的接受者還是LGTeacher的實例對象,所以[super class]輸出為LGTeacher谱煤。

    main.app

重寫objc_msgSendSuper

我們在子類LGTeacher重寫父類LGPersonstudy方法摊求,然后不進(jìn)行調(diào)用,然后我們重寫objc_msgSendSuper刘离。查看objc_super結(jié)構(gòu)體我們可以發(fā)現(xiàn)它需要傳一個receiver(接受者)和super_class(父類)室叉。receiver還是LGTeachersuper_classLGPerson.class硫惕。

    struct objc_super lg_objc_super;
    lg_objc_super.receiver = self;
    lg_objc_super.super_class = LGPerson.class;

    void* (*objc_msgSendSuperTyped)(struct objc_super *self,SEL _cmd) = (void *)objc_msgSendSuper;
    objc_msgSendSuperTyped(&lg_objc_super,@selector(study));
重寫objc_msgSendSuper方法

子類調(diào)用study方法

objc_super結(jié)構(gòu)體

運(yùn)行我們可以發(fā)現(xiàn)是可以調(diào)用LGPerson的study方法茧痕。

study方法調(diào)用

那我們把super_class改為NSObject.class試試;

lg_objc_super.super_class = NSObject.class;

運(yùn)行,我們可以看到是找不到這個study方法的恼除,因為使用objc_msgSendSuper時候它會直接從它的super_class直接找相應(yīng)的方法實現(xiàn)踪旷,我們設(shè)置的是NSObject.classNSObject沒有實現(xiàn)study方法豁辉,所以就直接就找不到study方法的實現(xiàn)了令野。

找不到方法

方法的快速查找

我們是結(jié)合objc4-838進(jìn)行探索的,我們?nèi)炙阉?code>objc_msgSend徽级,可以看到有很多气破,我們找到真機(jī)arm64架構(gòu)下的,objc_msgSend是由匯編寫的餐抢,為什么會采用匯編呢堵幽,是因為匯編效率性能高,可以節(jié)約不少的時間弹澎。

objc_msgSend

我們新建個工程,連上真機(jī)努咐,在[t study]處打上斷點(diǎn)苦蒿,然后再加上objc_msgSend符號斷點(diǎn)。運(yùn)行至斷點(diǎn)處打開objc_msgSend符號斷點(diǎn)渗稍。我們就可以看到匯編了佩迟。
objc_msgSend符號

然后在加上符號

進(jìn)入objc_msgSend函數(shù)

  • objc_msgSend匯編解析
//進(jìn)入objc_msgSend流程
    ENTRY _objc_msgSend
//流程開始团滥,無需frame
    UNWIND _objc_msgSend, NoFrame

//判斷p0(消息接受者)是否存在,不存在則重新開始執(zhí)行objc_msgSend
    cmp p0, #0          // nil check and tagged pointer check

//如果支持小對象類型报强。返回小對象或空
#if SUPPORT_TAGGED_POINTERS
//b是進(jìn)行跳轉(zhuǎn)灸姊,b.le是小于判斷,也就是小于的時候LNilOrTagged
    b.le    LNilOrTagged        //  (MSB tagged pointer looks negative)
#else
//等于秉溉,如果不支持小對象力惯,就LReturnZero
    b.eq    LReturnZero
#endif
//通過p13取isa
    ldr p13, [x0]       // p13 = isa
//通過isa取class并保存到p16寄存器中
    GetClassFromIsa_p16 p13, 1, x0  // p16 = class
//LGetIsaDone是一個入口
LGetIsaDone:
    // calls imp or objc_msgSend_uncached
//進(jìn)入到緩存查找或者沒有緩存查找方法的流程
    CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached

#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
// nil check判空處理,直接退出
    b.eq    LReturnZero     // nil check
    GetTaggedClass
    b   LGetIsaDone
// SUPPORT_TAGGED_POINTERS
#endif

我們可以看到是和objc4-838objc_msgSend函數(shù)匯編是一樣的召嘶。

  1. 首先進(jìn)入objc_msgSend流程父晶,cmp p0,判斷p0是否存在,我們讀取寄存器看看,可以看到x0是一個LGTeacher的實例對象,x1study方法弄跌。
    讀取寄存器x0 x1

    2.ldr p13, [x0]通過p13isa指針地址甲喝,我們可以打印x13地址為0x000021a1047b96e1,然后我們通過x0打印對象的isa铛只,可以看到x13就是對象的isa指針埠胖。
    isa

    3.GetClassFromIsa_p16 p13, 1, x0:通過isa&掩碼(0xffffffff8)獲取class并保存到p16寄存器中,我們打印x16可以看到x16是類對象地址淳玩。
    image.png

    4.CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached開始找方法直撤,取出x16class移到x15,通過x16cache凯肋。
CacheLookup

5.ldr x11, [x16, #0x10]x16通過內(nèi)存平移可以得到x11,x11就是之前cache詳解內(nèi)的第一個8字節(jié)成員_bucketsAndMaybeMask谊惭。

驗證x11

6.and x10, x11, #0xfffffffffffex11&0xfffffffffffe就得到buckets的首地址x10。然后在cache進(jìn)行方法查找侮东。如果找到了會進(jìn)行CacheHit(緩存命中)圈盔,找不到的話會調(diào)用_objc_msgSend_uncached函數(shù)。
CacheHit

_objc_msgSend_uncached函數(shù)

總結(jié):
當(dāng)調(diào)用objc_msgSend(receiver, sel)時:

  • receiver是否存在
  • 通過receiverisa指針獲取類對象
  • 通過類對象內(nèi)存平移獲取cache
  • 通過cache找到buckets
  • 根據(jù)buckets找相應(yīng)的sel
  • 如果有相應(yīng)的selCacheHit函數(shù)
  • 如果沒有相應(yīng)的sel會調(diào)用_objc_msgSend_uncached函數(shù)

方法的慢速查找

方法在cache內(nèi)找不到就是調(diào)用_objc_msgSend_uncached函數(shù)悄雅,我們在_objc_msgSend_uncached函數(shù)內(nèi)部可以看到它會調(diào)用MethodTableLookup函數(shù)驱敲,MethodTableLookup函數(shù)會調(diào)用_lookUpImpOrForward_lookUpImpOrForward函數(shù)在匯編內(nèi)是看不到宽闲,那我們直接在源碼內(nèi)搜索lookUpImpOrForward众眨。

_objc_msgSend_uncached

MethodTableLookup

我們可以看到lookUpImpOrForward函數(shù)內(nèi)部首先會判斷cache內(nèi)部有沒有方法,因為在多線程環(huán)境下現(xiàn)在cache可能有該方法容诬。
判斷cache內(nèi)部有沒有方法

如果本類沒有的話會調(diào)用getMethodNoSuper_nolock函數(shù)娩梨,然后依次調(diào)用search_method_list_inlinefindMethodInSortedMethodList函數(shù)览徒。我們可以看到findMethodInSortedMethodList是采用二分查找來獲取方法的狈定。
lookUpImpOrForward

getMethodNoSuper_nolock
search_method_list_inline

findMethodInSortedMethodList

當(dāng)我們在本類里面找到方法時,它會進(jìn)行goto done,然后會進(jìn)入log_and_fill_cache函數(shù)纽什,可以看到log_and_fill_cache函數(shù)內(nèi)部調(diào)用了cache.insert()方法措嵌,也就是加入緩存里面了。需要注意的是哪個類調(diào)用就會加入哪個類的cache里面芦缰,也就是如果子類調(diào)用父類的方法企巢,之后該方法會緩存到子類的cache
done

log_and_fill_cache

如果本類也沒有方法让蕾,通過curClass = curClass->getSuperclass()浪规,把curClass轉(zhuǎn)換成父類,然后找父類的cache涕俗,cache如果沒有在通過二分查找找方法列表罗丰。如果再沒有把curClass轉(zhuǎn)換成父類的父類依次查找。
curClass->getSuperclass()

總結(jié):
消息的慢速查找流程:

  • 調(diào)用lookUpImpOrForward
  • 看本類的cache里面有沒有該方法
  • 如果沒有看本類的methodList有沒有該方法(二分查找)
  • 如果沒有看父類的cache里面有沒有該方法
  • 如果沒有看父類的methodList有沒有該方法(二分查找)
  • 然后逐級向上查找
  • 當(dāng)父類是nil的時候也就是查找到NSObject的時候再姑,都沒有的話就會進(jìn)入消息轉(zhuǎn)發(fā)流程萌抵。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市元镀,隨后出現(xiàn)的幾起案子绍填,更是在濱河造成了極大的恐慌,老刑警劉巖栖疑,帶你破解...
    沈念sama閱讀 222,183評論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件讨永,死亡現(xiàn)場離奇詭異,居然都是意外死亡遇革,警方通過查閱死者的電腦和手機(jī)卿闹,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,850評論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來萝快,“玉大人锻霎,你說我怎么就攤上這事【句觯” “怎么了旋恼?”我有些...
    開封第一講書人閱讀 168,766評論 0 361
  • 文/不壞的土叔 我叫張陵,是天一觀的道長奄容。 經(jīng)常有香客問我冰更,道長,這世上最難降的妖魔是什么昂勒? 我笑而不...
    開封第一講書人閱讀 59,854評論 1 299
  • 正文 為了忘掉前任蜀细,我火速辦了婚禮,結(jié)果婚禮上戈盈,老公的妹妹穿的比我還像新娘奠衔。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,871評論 6 398
  • 文/花漫 我一把揭開白布涣觉。 她就那樣靜靜地躺著,像睡著了一般血柳。 火紅的嫁衣襯著肌膚如雪官册。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,457評論 1 311
  • 那天难捌,我揣著相機(jī)與錄音膝宁,去河邊找鬼。 笑死根吁,一個胖子當(dāng)著我的面吹牛员淫,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播击敌,決...
    沈念sama閱讀 40,999評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼介返,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了纽哥?” 一聲冷哼從身側(cè)響起猖腕,我...
    開封第一講書人閱讀 39,914評論 0 277
  • 序言:老撾萬榮一對情侶失蹤腕够,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后徘公,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,465評論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡哮针,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,543評論 3 342
  • 正文 我和宋清朗相戀三年关面,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片十厢。...
    茶點(diǎn)故事閱讀 40,675評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡等太,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出寿烟,到底是詐尸還是另有隱情澈驼,我是刑警寧澤,帶...
    沈念sama閱讀 36,354評論 5 351
  • 正文 年R本政府宣布筛武,位于F島的核電站缝其,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏徘六。R本人自食惡果不足惜内边,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,029評論 3 335
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望待锈。 院中可真熱鬧漠其,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,514評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至柴信,卻和暖如春套啤,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背随常。 一陣腳步聲響...
    開封第一講書人閱讀 33,616評論 1 274
  • 我被黑心中介騙來泰國打工潜沦, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人绪氛。 一個月前我還...
    沈念sama閱讀 49,091評論 3 378
  • 正文 我出身青樓唆鸡,卻偏偏與公主長得像,于是被迫代替她去往敵國和親枣察。 傳聞我的和親對象是個殘疾皇子争占,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,685評論 2 360

推薦閱讀更多精彩內(nèi)容