iOS 函數(shù)耗時統(tǒng)計原理分析

起源

??公司項目里帅腌,開發(fā)中長期在使用DiDi的DoraemonKit庫里的一些調(diào)試小工具, 剛好最近需要對一些代碼耗時做分析懒豹,想到了,其中包含的DoraemonTimeProfiler類可以通過動態(tài)方式,無侵入的對函數(shù)的耗時進行統(tǒng)計竹椒,一時好奇苹熏,想看看具體是如何做到的勒葱。

基本邏輯

??打開源碼菇夸,其實這部分代碼并不多共苛,從邏輯上非常易于理解判没。我們知道OC方法的執(zhí)行,本質(zhì)上就是通過調(diào)用objc_msgSend函數(shù)來發(fā)送消息隅茎,所以基本思路是對objc_msgSend方法進行hook澄峰,通過對objc_msgSend方法執(zhí)行開始前和結(jié)束后分別記錄時間,再進行計算就可以得出方法的調(diào)用時長辟犀。

??但由于為objc_msgSend是基于匯編實現(xiàn)的俏竞,在編譯期間就決定了函數(shù)地址,所以無法通過動態(tài)的method_swizzle來進行替換堂竟。這里采用了fishhook庫動態(tài)的對objc_msgSend進行了hook魂毁,
fishhook簡單來講,就是利用動態(tài)庫的共享緩存庫在運行時進行綁定的原理出嘹,把我們的hook代碼注入進去席楚,具體的原理,不是這篇文章的重點疚漆,就不做分析了酣胀。

fishhook對objc_msgSend進行hook的代碼如下:

void dtp_hook_begin(void) {
    _call_record_enabled = true;
    pthread_key_create(&_thread_key, &release_thread_call_stack);

    doraemon_rebind_symbols((struct doraemon_rebinding[1]){"objc_msgSend", (void *)hook_objc_msgSend, (void **)&orig_objc_msgSend},1);
}

void dtp_hook_end(void) {
    _call_record_enabled =false;
    doraemon_rebind_symbols((struct doraemon_rebinding[1]){"objc_msgSend", (void*)orig_objc_msgSend,NULL},1);
}

自定義objc_msgSend如何實現(xiàn)

??上面分析了如何對objc_msgSend進行hook,接下來就到了本文的重點娶聘,這個hook_objc_msgSend方法闻镶,應該如何編寫來進行時間統(tǒng)計呢?

DoraemonTimeProfiler給出的答案是這樣的:

__attribute__ ((__naked__))
static void hook_objc_msgSend() {
    //before之前保存objc_msgSend的參數(shù)
    save()

    //將objc_msgSend執(zhí)行的下一個函數(shù)地址傳遞給before_objc_msgSend的第二個參數(shù)x0 self, x1 _cmd, x2: lr address
    __asm volatile ("mov x2, lr\n");
    __asm volatile ("mov x3, x4\n");

    // 執(zhí)行before_objc_msgSend   blr 除了從指定寄存器讀取新的 PC 值外效果和 bl 一...
    call(blr, &before_objc_msgSend)

    // 恢復objc_msgSend參數(shù)丸升,并執(zhí)行
    load()
    call(blr, orig_objc_msgSend)

    //after之前保存objc_msgSend執(zhí)行完成的參數(shù)
    save()

    //調(diào)用 after_objc_msgSend
    call(blr, &after_objc_msgSend)
    
    //將after_objc_msgSend返回的參數(shù)放入lr,恢復調(diào)用before_objc_msgSend前的lr地址
    // x0 是整數(shù)/指針args的第一個arg傳遞寄存器 x0 是整數(shù)/指針值的(第一個)返回值寄存器
    __asm volatile ("mov lr, x0\n");

    //恢復objc_msgSend執(zhí)行完成的參數(shù)
    load()

    //方法結(jié)束,繼續(xù)執(zhí)行l(wèi)r
    ret()
}

代碼不長铆农,我們來一步一步進行分析。

首先這里為什么實現(xiàn)都是C和匯編狡耻,是因為objc_msgSend本身是基于匯編進行實現(xiàn)墩剖,所以hook的方法對于objc_msgSend相關(guān)的部分都必須是基于匯編來實現(xiàn)。

__attribute__ ((__naked__))

這里聲明是告訴編譯器在函數(shù)調(diào)用的時候不使用棧保存參數(shù)信息夷狰,由于objc_msgSend本身使用了該修飾符岭皂,所以這里我們也必須同樣的進行修飾。

這里簡單介紹下:
在arm64匯編中沼头,在小于9個參數(shù)時爷绘,通過x0-x8寄存器對參數(shù)進行保存书劝,當超過時,會通過椡林粒空間進行存儲购对,objc_msgSend使用此聲明猜測可能是基于性能考慮。

save()

保存objc_msgSend本身的方法棧信息陶因。因為在objc_msgSend方法執(zhí)行前骡苞,我們會執(zhí)行方法before_objc_msgSend,從而對寄存器造成污染楷扬,為了確保能夠正確的執(zhí)行objc_msgSend方法解幽,需要對當前的寄存器狀態(tài)進行保存,等我們的方法執(zhí)行完畢后毅否,再對寄存器進行恢復亚铁,從而保證OC方法的正確執(zhí)行。

call(blr, &before_objc_msgSend)

通過blr匯編語句螟加,執(zhí)行before_objc_msgSend方法徘溢,從而在OC方法執(zhí)行前,記錄方法開始的時間捆探。

before_objc_msgSend的實現(xiàn)簡單描述如下:

before_objc_msgSend {
    獲取當前堆棧信息
    更新記錄的開始執(zhí)行時間到堆棧信息中
}

具體實現(xiàn)代碼參考源碼:
static inline void push_call_record(id _self, Class _cls, SEL _cmd, uintptr_t lr)

load()

恢復我們通過save()保存的objc_msgSend方法信息然爆。

call(blr, orig_objc_msgSend)

調(diào)用原始的objc_msgSend,開始OC方法的執(zhí)行黍图。

save()
(此時方法執(zhí)行完畢)
和之前的save()原因一樣曾雕,這里是因為objc_msgSend方法執(zhí)行完畢后,我們需要記錄結(jié)束時間助被,會對寄存器造成污染剖张,所以需要在after_objc_msgSend方法執(zhí)行前,對寄存器狀態(tài)進行保存揩环。

call(blr, &after_objc_msgSend)

通過匯編語句搔弄,調(diào)用after_objc_msgSend方法,進行方法的耗時的計算丰滑,并將函數(shù)信息和耗時存儲到dtp_records中顾犹。

after_objc_msgSend的實現(xiàn)簡單描述如下:

after_objc_msgSend {
    找到緩存中當前堆棧信息
    讀取之前緩存的執(zhí)行時間
    計算出方法的耗時
    生成一條記錄記錄方法信息和耗時
    更新記錄到dtp_records中
}

具體實現(xiàn)代碼參考源碼:
static inline uintptr_t pop_call_record()

__asm volatile ("mov lr, x0\n");

恢復寄存器x0的內(nèi)容到lr中,還原h(huán)ook_objc_msgSend的lr寄存器褒墨。

這里需要講一下炫刷,為什么x0的值是lr寄存器的內(nèi)容?
首先我們要理解x0在函數(shù)結(jié)束時郁妈,會作為返回值寄存器浑玛,存儲函數(shù)的返回內(nèi)容, 而after_objc_msgSend的返回值為pRecord->lr,也就是hook_objc_msgSend原始的lr的內(nèi)容噩咪。

pRecord->lr為什么是原始的lr寄存器的內(nèi)容呢顾彰?
這里我們結(jié)合before_objc_msgSend和after_objc_msgSend的源碼來仔細分析:

void before_objc_msgSend(id self, SEL _cmd, uintptr_t lr)  {
    push_call_record(self, object_getClass(self), _cmd, lr);
}

static inline void push_call_record(id _self, Class _cls, SEL _cmd, uintptr_t lr) {
    thread_call_stack *cs = get_thread_call_stack();
    if (cs) {
        ...
        thread_call_record *newRecord = &cs->stack[nextIndex];
        ...

        newRecord->lr = lr; // 此時存儲了lr到thread_call_record中
        
        ...
    }
}


uintptr_t after_objc_msgSend() {
    return pop_call_record();
}


static inline uintptr_t pop_call_record() {
    thread_call_record *pRecord = &cs->stack[nextIndex]; 
    ...
    return pRecord->lr; // 返回before_objc_msgSend中存儲的lr
}
 

在before_objc_msgSend時, "lr參數(shù)"被存入了thread_call_record對象中失晴。

那么這個"lr參數(shù)"是什么呢?
我們發(fā)現(xiàn)在before_objc_msgSend執(zhí)行前拘央,有一行匯編語句:__asm volatile ("mov x2, lr\n");
它將lr寄存器的內(nèi)容移到了x2寄存器中书在,而x2寄存器接下來做了什么呢灰伟?在arm64架構(gòu)下,x2寄存器對應的是before_objc_msgSend方法的第三個入?yún)ⅲ╱intptr_t lr)儒旬。

因此寄存器lr的內(nèi)容被存入了thread_call_record對象中栏账。而在after_objc_msgSend時,pRecord->lr對應的即是thread_call_record所保存的lr寄存器的原始內(nèi)容栈源。

load()

寄存器還原到objc_msgSend剛執(zhí)行結(jié)束后的狀態(tài)挡爵。

ret()

源碼:#define ret() __asm volatile ("ret\n");
匯編里的return語句,代表當前方法內(nèi)容執(zhí)行完畢甚垦,跳出方法塊茶鹃,繼續(xù)執(zhí)行。

這時會將lr寄存器讀取到pc寄存器中艰亮,跳出hook_objc_msgSend函數(shù)繼續(xù)執(zhí)行闭翩,這也是為什么執(zhí)行ret前要還原lr寄存器內(nèi)容的原因。

整理的流程如圖所示:


流程圖

重要的方法

執(zhí)行過程中迄埃,有幾個重要的方法疗韵,這里著重分析下。

save()

源碼:

    __asm volatile ( \
        "stp x8, x9, [sp, #-16]!\n" \
        "stp x6, x7, [sp, #-16]!\n" \
        "stp x4, x5, [sp, #-16]!\n" \
        "stp x2, x3, [sp, #-16]!\n" \
        "stp x0, x1, [sp, #-16]!\n");

save主要用戶緩存當前方法的寄存器狀態(tài)侄非,所以將x0-x8的寄存器緩存到了棧上蕉汪。
之所以用到了x9寄存器,是因為arm棧是按照16字節(jié)對齊逞怨,而由于寄存器大小固定為8字節(jié)者疤,所以需要x9來進行字節(jié)補齊。
這里簡單介紹幾個知識:

  • sp:棧頂寄存器骇钦,寄存器移動到棧上都需要依賴sp寄存器宛渐。
  • stp x8, x9, [sp, #-16]!匯編語句屬于精簡的語句,省去了棧拉伸的代碼眯搭。實際上等同于以下指令:
sub sp,sp,#0x16
stp x8,x9,[sp]

load()

源碼:

#define load() \
    __asm volatile ( \
        "ldp x0, x1, [sp], #16\n" \
        "ldp x2, x3, [sp], #16\n" \
        "ldp x4, x5, [sp], #16\n" \
        "ldp x6, x7, [sp], #16\n" \
        "ldp x8, x9, [sp], #16\n");

load和save是對應關(guān)系窥翩,用于恢復save存儲的寄存器信息。

call()

源碼:

#define call(b, value) \
    __asm volatile ("stp x8, x9, [sp, #-16]!\n"); \
    __asm volatile ("mov x12, %0\n" :: "r"(value)); \
    __asm volatile ("ldp x8, x9, [sp], #16\n"); \
    __asm volatile (#b " x12\n");

call方法的參數(shù)b,在當前環(huán)境下鳞仙,都是用了blr跳轉(zhuǎn)指令進行方法執(zhí)行寇蚊。
第一行和第三行是對x8寄存器進行暫存和恢復,之所以這么做棍好,是因為__asm volatile ("mov x12, %0\n" :: "r"(value));執(zhí)行過程中仗岸,會通過x8來保存函數(shù)地址允耿,再進行跳轉(zhuǎn)。

總結(jié)

想不到簡簡單的函數(shù)耗時統(tǒng)計扒怖,卻包含了好幾個知識點:

  • obj_msgSend()如何hook
  • fishhook原理
  • 匯編基本指令

雖然大部分都有相關(guān)的文章可以輔助理解较锡,但當自己去做的時候,還是會發(fā)現(xiàn)很多新的理解盗痒,一點點的積累一點點的進步蚂蕴。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市俯邓,隨后出現(xiàn)的幾起案子骡楼,更是在濱河造成了極大的恐慌,老刑警劉巖稽鞭,帶你破解...
    沈念sama閱讀 216,744評論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件鸟整,死亡現(xiàn)場離奇詭異,居然都是意外死亡朦蕴,警方通過查閱死者的電腦和手機篮条,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,505評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來吩抓,“玉大人兑燥,你說我怎么就攤上這事∏倥。” “怎么了降瞳?”我有些...
    開封第一講書人閱讀 163,105評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長蚓胸。 經(jīng)常有香客問我挣饥,道長,這世上最難降的妖魔是什么沛膳? 我笑而不...
    開封第一講書人閱讀 58,242評論 1 292
  • 正文 為了忘掉前任扔枫,我火速辦了婚禮,結(jié)果婚禮上锹安,老公的妹妹穿的比我還像新娘短荐。我一直安慰自己,他們只是感情好叹哭,可當我...
    茶點故事閱讀 67,269評論 6 389
  • 文/花漫 我一把揭開白布忍宋。 她就那樣靜靜地躺著,像睡著了一般风罩。 火紅的嫁衣襯著肌膚如雪糠排。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,215評論 1 299
  • 那天超升,我揣著相機與錄音入宦,去河邊找鬼哺徊。 笑死,一個胖子當著我的面吹牛乾闰,可吹牛的內(nèi)容都是我干的落追。 我是一名探鬼主播,決...
    沈念sama閱讀 40,096評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼涯肩,長吁一口氣:“原來是場噩夢啊……” “哼淋硝!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起宽菜,我...
    開封第一講書人閱讀 38,939評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎竿报,沒想到半個月后铅乡,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,354評論 1 311
  • 正文 獨居荒郊野嶺守林人離奇死亡烈菌,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,573評論 2 333
  • 正文 我和宋清朗相戀三年阵幸,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片芽世。...
    茶點故事閱讀 39,745評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡挚赊,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出济瓢,到底是詐尸還是另有隱情荠割,我是刑警寧澤,帶...
    沈念sama閱讀 35,448評論 5 344
  • 正文 年R本政府宣布旺矾,位于F島的核電站蔑鹦,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏箕宙。R本人自食惡果不足惜嚎朽,卻給世界環(huán)境...
    茶點故事閱讀 41,048評論 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望柬帕。 院中可真熱鬧哟忍,春花似錦、人聲如沸陷寝。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,683評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽凤跑。三九已至粗蔚,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間饶火,已是汗流浹背鹏控。 一陣腳步聲響...
    開封第一講書人閱讀 32,838評論 1 269
  • 我被黑心中介騙來泰國打工致扯, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人当辐。 一個月前我還...
    沈念sama閱讀 47,776評論 2 369
  • 正文 我出身青樓抖僵,卻偏偏與公主長得像,于是被迫代替她去往敵國和親缘揪。 傳聞我的和親對象是個殘疾皇子耍群,可洞房花燭夜當晚...
    茶點故事閱讀 44,652評論 2 354

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