自研 iOS 熱更新機制——OCPack技術(shù)方案總結(jié)

https://juejin.im/post/6884833291740905480?utm_source=gold_browser_extension

I. 方案簡介

OCPack是一種 iOS 平臺上 App 動態(tài)化技術(shù)方案箩帚,用戶可以使用 Objective-C 語言編寫待動態(tài)化的功能邏輯(生成.m文件),然后通過OCPack提供的工具鏈生成 patch 文件(.bin格式)。客戶端則內(nèi)置了一個基于 Native 環(huán)境的的虛擬棧機蛇摸,它可以動態(tài)加載并執(zhí)行存儲在客戶端的 patch 文件中的方法笋婿。Patch 文件可根據(jù)業(yè)務(wù)需要隨時下載、更新并由虛擬機重新加載勘天、運行觅赊。

此方案的主要優(yōu)點:

  • 開發(fā)效率

    可以使開發(fā)者像寫普通功能代碼一樣右蕊,使用熟悉的 Xcode IDE 和 Objective-C 語言進行開發(fā)、調(diào)試吮螺,在開發(fā)完成后使用工具鏈即可方便地生成 patch 文件饶囚,提高開發(fā) patch 的效率。

  • 語法覆蓋

    考慮到使用者的方便性和開發(fā)周期的平衡鸠补,目前OCPack的實現(xiàn)覆蓋了c語言的基本語法和 Objective-C 中比較常用的語法萝风,保證開發(fā)者在使用中大部分常用的寫法都能直接支持,而部分不能支持的語法也有相應(yīng)的替代實現(xiàn)方式紫岩。

  • 問題定位

    • 對于暫不支持的語法规惰,OCPack的工具鏈能夠明確地給出錯誤原因提示及錯誤代碼位置,方便定位開發(fā)中遇到的問題泉蝌。

    • 上線后也可以調(diào)用相應(yīng)接口獲取虛擬機在各線程的調(diào)用棧信息歇万,結(jié)合編譯過程中生成的符號文件揩晴,開發(fā)就能夠準(zhǔn)確定位到當(dāng)時虛擬機調(diào)用到的源文件代碼行數(shù),方便定位贪磺、解決線上問題硫兰。符號解析工具也包含在工具鏈中。

  • 性能

    由于是基于 Native 環(huán)境寒锚,且自定義的棧機指令集意義明確瞄崇、設(shè)計精簡,并且大部分與 Native 環(huán)境之間的交互都是直接操作內(nèi)存地址壕曼,省去了像 JSPatch 一樣頻繁字符串解析及 Box/Unbox 的開銷。其中 OC 橋接調(diào)用效率近似原生等浊。

  • 內(nèi)存及穩(wěn)定性

    • JavaScript 的 GC 的內(nèi)存管理機制會導(dǎo)致內(nèi)存不能及時釋放腮郊,而如果強行釋放掉 JSContext 會導(dǎo)致線上出現(xiàn)一些詭異的崩潰,難以定位和解決筹燕。

    • 此方案支持 ARC 內(nèi)存管理轧飞,能夠與客戶端的 ARC/MRC 內(nèi)存管理機制正確配合,消除了GC時機不可控的問題撒踪。

    • 而且結(jié)合項目本身的特點过咬,將主要使用到的內(nèi)存都放到文件映射(mmap)中,盡量不占用應(yīng)用程序的內(nèi)存配額制妄,提高應(yīng)用的穩(wěn)定性掸绞。

II. 技術(shù)方案

OCPack復(fù)用 clang 前端來分析目標(biāo) Objective-C 代碼的語法樹,通過自定義 ASTFrontendAction 來遍歷語法樹耕捞,生成自定義指令集的匯編程序衔掸。在客戶端,由自研的虛擬棧機來解釋執(zhí)行匯編程序中的二進制指令俺抽。

生成 patch 文件的基本數(shù)據(jù)流程是:

  • Objective-C 源碼會首先通過OCPack編譯模塊轉(zhuǎn)換為自定義指令集的匯編程序(.s)敞映。此過程主要是通過解析 LLVM 生成的 Objective-C 代碼語法樹(AST)實現(xiàn)的。

  • 匯編程序(.s)隨后會通過一個自研匯編工具(smc)被轉(zhuǎn)換為二進制的 patch 文件(.bin)磷斧。

注:OCPack自定義的棧機匯編指令主要有67條振愿,除基本指令以外,主要是依據(jù)語法樹結(jié)點類型設(shè)計弛饭。

在運行時冕末,客戶端內(nèi)置的虛擬棧機能夠根據(jù)用戶需求加載指定的 patch 文件,然后就可以執(zhí)行其中任意方法了侣颂。

以下分模塊來介紹主要技術(shù)點:

編譯模塊:

功能:Objective-C程序(.m) -> 語法樹 -> 匯編程序(.s)

1. 獨立的編譯程序

主要使用 clang 的 libTooling 接口栓霜,實現(xiàn)了 AST FrontendAction,通過實現(xiàn)自定義的 ASTConsumer 遞歸遍歷語法樹横蜒,對不同的節(jié)點類型作相應(yīng)處理胳蛮,生成可執(zhí)行的匯編指令程序销凑。

  • 編譯選項

    要調(diào)用 clang 的模塊為目標(biāo) Objective-C 源文件(.m)生成語法樹,需要先提供編譯此 .m 所需的編譯選項仅炊。對于集成方來說斗幼,目標(biāo)文件可能需要依賴很多相關(guān)的.h或有其他編譯開關(guān),為了方便集成方得到完整的編譯選項列表抚垄,我們制作了相應(yīng)的工具蜕窿,可以方便地從集成方工程的編譯日志中得到目標(biāo)文件所需的編譯選項的完整列表。

  • 編譯錯誤和警告

    OCPack支持 Objective-C 語言中常用的語法類型呆馁,對于不支持的語法桐经,在編譯期間會生成相應(yīng)的日志文件,具體標(biāo)明了錯誤類型和錯誤位置 浙滤,方便開發(fā)定位問題阴挣。

:為了進一步提高開發(fā)效率,OCPack還實現(xiàn)了一個獨立的 clang plugin纺腊,可以通過給工程添加一個 .xcconfig 文件(替換默認(rèn)的 clang)畔咧,實現(xiàn)在 Xcode 中顯示相關(guān)的編譯錯誤,并能一鍵生成 .bin文件揖膜,省去了獲取編譯選項和手工查看錯誤日志的步驟誓沸,簡化了開發(fā)流程。

2. 棧機匯編指令集

為了連接包含有 Objective-C 代碼邏輯的語法樹和客戶端運行的虛擬機壹粟,OCPack需要定義一套比較完整的匯編指令集拜隧。該指令集應(yīng)該滿足以下兩個條件:

  • 提供足夠的功能支持,用以實現(xiàn)預(yù)定義的 Objective-C 語法范圍趁仙。具體地虹蓄,對于指定的語法樹節(jié)點類型集合,能夠通過編譯邏輯生成相應(yīng)的匯編指令組合幸撕,等價地完成原 Objective-C 代碼所要實現(xiàn)的邏輯功能薇组。

  • 盡量減少指令集的復(fù)雜性:一方面應(yīng)盡量減少指令的條數(shù),以降低虛擬機實現(xiàn)的復(fù)雜性坐儿;另一方面應(yīng)盡量降低單條指令本身的語義復(fù)雜性律胀,每條指令應(yīng)完成明確而有限的功能。

以下簡要介紹幾個比較典型的指令的設(shè)計方案:

2.1 push 和 pop 指令

棧機中最基本的部件是操作棧貌矿,用于存放正在進行中的操作數(shù)和操作結(jié)果炭菌。如:要計算 1 + 2,棧機需要執(zhí)行類似以下指令:

    push instant 1
    push instant 2
    add
復(fù)制代碼

先將兩個操作數(shù)12依次 push 進操作棧逛漫,再執(zhí)行 add 操作黑低。add 操作負(fù)責(zé)先 pop 對應(yīng)的操作數(shù),經(jīng)過加法計算后再將結(jié)果 push 進棧。以上指令執(zhí)行完后克握,操作棧頂存放的就是操作結(jié)果3蕾管。

但只有操作棧是不夠的,程序邏輯的復(fù)雜性要求像局部變量菩暗、方法參數(shù)等數(shù)據(jù)擁有確定不變的內(nèi)存地址掰曾,因此OCPack將局部變量、靜態(tài)變量停团、常量旷坦、指針、立即數(shù)等分別對應(yīng)一個段佑稠,每種類型的變量對應(yīng)于所屬段中的一個序號(index)秒梅。

  • 段:用于存放各種非臨時數(shù)據(jù)(可取到地址的數(shù)據(jù)),這些數(shù)據(jù)又分線程相關(guān)與線程無關(guān)舌胶,其中:
    線程相關(guān)的數(shù)據(jù)段主要包括:

            local    //局部變量
            arg      //方法實參
            this     //存放self(用于super的實現(xiàn))
            that     //用于實現(xiàn)struct的成員變量
            pointer  //用于輔助實現(xiàn)this, that 
            //注: 線程相關(guān)的段數(shù)據(jù)存放在thread_context(線程局部存儲)中捆蜀,只對本線程可見
    復(fù)制代碼
    

    線程無關(guān)的數(shù)據(jù)段主要包括:

            const    //常量字符串
            static   //靜態(tài)變量
            instant  //立即數(shù)
            //注:線程無關(guān)的段數(shù)據(jù)存放在machine中,各線程都可見
    復(fù)制代碼
    

在對語法樹進行遍歷的過程中辆琅,OCPack編譯器會維護一個符號表,對每個變量聲明(VarDecl)建立相應(yīng)的符號表項这刷,存放其段名和序號(index)婉烟。

對語法樹中的變量引用(VarDeclRef 結(jié)點),OCPack編譯器會找到其相應(yīng)的 VarDecl 的符號表項暇屋,生成相應(yīng)的 push似袁、pop 指令。

push和pop指令的參數(shù)就是段名和序號(index):

  • push segment index —— 將 segment 段中的 index 處數(shù)據(jù) push 到操作棧頂
  • pop segment index —— 將操作棧頂?shù)臄?shù)據(jù) pop 到 segment 段中的 index 處
2.2 prolog 指令
  • prolog 指令是每個虛擬機中方法的第一條指令咐刨,它會根據(jù)其指令參數(shù)為當(dāng)前方法棧幀中的local段開辟相應(yīng)大小的段空間昙衅,并記錄當(dāng)前棧幀的返回地址,然后計算并記錄參數(shù)列表(arg 段)起始地址定鸟,再將調(diào)用者的棧頂指針指向參數(shù)表之前而涉,最后切換到被調(diào)用者的棧幀。

  • 格式:prolog arg_size local_size

    • 其中 arg_size 表示所有參數(shù)的總長度联予,用于計算參數(shù)列表的起始地址

    • local_size 表示局部變量段的長度

方法調(diào)用和傳參這塊的設(shè)計需要一些特別的考慮啼县,主要需要滿足幾個要求:

  • 調(diào)用者只需將參數(shù)和返回值按要求 push 到操作棧上,然后直接跳轉(zhuǎn)到被調(diào)方法的起始地址沸久,程序就可順利執(zhí)行季眷,調(diào)用方不應(yīng)承擔(dān)其他不必要的責(zé)任

  • 根據(jù)棧機的一般調(diào)用邏輯,被調(diào)函數(shù)返回時卷胯,剛才push進來的參數(shù)和返回值應(yīng)該已由被調(diào)者pop子刮,此時棧頂應(yīng)該只有返回值,棧頂以下應(yīng)該是跟這次調(diào)用無關(guān)的其他數(shù)據(jù)

  • 被調(diào)方需要知道參數(shù)和返回地址的具體位置窑睁,參數(shù)需要有固定地址挺峡,支持隨機訪問葵孤,不能是只靠 pop 得到的暫存值

  • 局部變量也需要隨機訪問,其大小需要在函數(shù)執(zhí)行一開始就分配好

為了滿足這些需求沙郭,OCPack中設(shè)計了 prolog 這一指令:

  • 在每個方法頭部加此指令佛呻,調(diào)用者一跳到當(dāng)前方法就執(zhí)行此指令,相關(guān)設(shè)置都在此指令中執(zhí)行病线,盡量減少對調(diào)用者的要求吓著。

  • 根據(jù)約定,執(zhí)行到 prolog 時送挑,棧頂存放的是返回地址绑莺,棧頂以下是倒排的參數(shù)表, prolog 指令先 pop 返回地址并保存下來惕耕,然后將調(diào)用者的棧楨的 sp 往回調(diào)整參數(shù)表的長度(此長度作為指令參數(shù)由編譯時確定纺裁,調(diào)用者無需通過棧來傳遞此信息),也即指向第一個參數(shù)的位置司澎。注意此時還沒有生成被調(diào)用者的棧楨欺缘,所有的操作都還在調(diào)用者的棧楨上下文內(nèi)。這樣就能夠保證被調(diào)者返回時調(diào)用者的 sp 是在合適的位置挤安,到時候直接 push 返回值就可以谚殊。

  • 此時 prolog 根據(jù)返回地址及調(diào)用者的棧楨信息生成新棧楨,新棧幀中建立的 arg 段直接指向參數(shù)表的起始位置蛤铜,之后訪問參數(shù)即可使用 push arg i 或 pop arg i 等指令嫩絮。

  • 同時,局部變量的 local 段也需要建立围肥,其大小也由編譯時確定剿干,即是 prolog 指令的 local_size 參數(shù),在建好棧楨并切換當(dāng)前棧幀后穆刻,即完成了方法調(diào)用的過渡階段置尔,程序流程便可繼續(xù)進行。

2.3 ret 指令
  • ret 指令是虛擬機中方法的最后一條指令氢伟,與 prolog 相對應(yīng)撰洗,用于回退棧幀(unwind frame),并將返回值數(shù)據(jù)由被調(diào)方的棧頂拷貝到 unwind 以后調(diào)用方的操作棧頂腐芍,以實現(xiàn)調(diào)用完成后返回值位于當(dāng)前棧頂?shù)恼{(diào)用約定差导。

  • 格式:ret retSize

此處有一次數(shù)據(jù)拷貝,拷貝大小即為返回值的大小猪勇。為盡量減少對調(diào)用者的影響设褐,在編譯期給 ret 方法增加 retSize 參數(shù),以便在執(zhí)行 ret 的時候就能完成數(shù)據(jù)拷貝,棧幀回退到調(diào)用者后助析,調(diào)用者可以預(yù)期返回值就在自己操作棧的棧頂犀被,后續(xù)邏輯不受當(dāng)前棧頂值是由方法調(diào)用返回還是自行 push 得到的影響,邏輯較清晰外冀。

2.4 跳轉(zhuǎn)指令

為了實現(xiàn)條件判斷 if/else 和 for 循環(huán)等流程控制語法寡键,OCPack指令集定義了jmpjmp_if指令,根據(jù)語法樹中對應(yīng)類型的節(jié)點具體情況雪隧,生成相應(yīng)的跳轉(zhuǎn)指令和跳轉(zhuǎn) label西轩。這些文本跳轉(zhuǎn) label 會被存儲在 .s 文件中,然后在下一階段(Assembler 將 .s 轉(zhuǎn)換為 .bin時)被替換成相應(yīng)的偏移地址脑沿。

2.5 switch指令
1) switch跳轉(zhuǎn)表

switch需要運行時決定跳到哪個 case label 對應(yīng)的地址藕畔,只用jmp_if需要在 case列表代碼尾部插入多條比較語句,而棧機又需要每次比較前都push相應(yīng)的參數(shù)庄拇,實現(xiàn)比較繁瑣而且性能較差注服,因此OCPack在指令集中增加了cmp_nresolve_labeljmp_tos指令措近。

  • 首先溶弟,OCPack編譯器在生成指令時會先將 switch 要比較的目標(biāo) push 進操作棧,然后再將各個 case 的值 push 進棧瞭郑,然后添加cmp_n n指令辜御。在運行時,cmp_n n指令會從棧上 pop 出n個數(shù)據(jù)凰浮,與棧頂?shù)臄?shù)據(jù)(即 switch 的目標(biāo))進行比較我抠,如果與第i個相等苇本,則將ipush到棧頂袜茧。

  • 后面再添加指令resolve_label label_prefix。此指令在運行時會將 label_prefix 與棧頂?shù)?code>i進行字符串拼接瓣窄,生成目標(biāo) label 名笛厦,并在 machine 中進行查找,找到對應(yīng)的 label 地址俺夕,push 到棧頂裳凸。其中 label_prefix 是每個 switch 語句唯一的,可以支持 switch 嵌套劝贸。

  • 然后再添加指令jmp_tos姨谷。此指令在運行時會跳轉(zhuǎn)到棧頂?shù)牡刂罚瑥亩鴮崿F(xiàn) switch 的功能映九。

2) continue和break的支持:

分別維護一個 break和 continue 的 label 棧梦湘,棧頂元素為當(dāng)前 break 或 continue 調(diào)用時應(yīng)該 jmp 到的目標(biāo) label,在目標(biāo)表達式開始和結(jié)束時進行入棧和出棧操作。在遇到語法樹上結(jié)點為 break 或 continue 時捌议,取出當(dāng)前棧頂?shù)哪繕?biāo) label哼拔,生成jmp 目標(biāo)label指令。

2.6 call指令
  • 使用 libffi 實現(xiàn)動態(tài) c 方法調(diào)用瓣颅。對于每個被調(diào)用的 c 方法倦逐,在 .s 會有一項DECL_C_FUNC 的聲明,聲明包含此方法的名稱宫补、簽名(包括參數(shù)個數(shù)和類型)等信息檬姥。

  • .s 中的參數(shù)類型是OCPack自定義的字符串,一一對應(yīng)到 libffi 的類型守谓。對于 struct 來說穿铆,生成指令時需要遞歸找到 struct 所有成員的類型,拼成相應(yīng)的字符串斋荞,然后在運行期反解字符串荞雏,構(gòu)造出 libffi 所需的數(shù)據(jù)類型。

  • 對于變參的方法平酿,方法名相同而參數(shù)個數(shù)或類型不同的凤优,在 DECL_C_FUNC 時會對應(yīng)不同的條目,虛擬機在運行時會根據(jù)對應(yīng)的條目去構(gòu)造相應(yīng)的 libffi 參數(shù)數(shù)據(jù)蜈彼。

2.7 基本一元筑辨、二元運算符指令
  • 指令集中對算術(shù)、邏輯幸逆、移位等等基本運算符都有對應(yīng)的指令棍辕,指令參數(shù)包括返回值類型、操作數(shù)類型等还绘。

  • 在虛擬機的實現(xiàn)代碼中將各種運算楚昭、數(shù)據(jù)類型的組合分配到相應(yīng)的c語言實現(xiàn),運行時就根據(jù)傳入的指令和參數(shù)調(diào)用其相應(yīng)實現(xiàn)拍顷。

注: 此指令只支持整型抚太、浮點型等基本數(shù)據(jù)類型的運算,不支持自定義類型重載的運算符

2.8 左右值轉(zhuǎn)換
  • 指令集有左值轉(zhuǎn)右值的指令昔案,其參數(shù)為右值的 size尿贫。此指令的作用為:先 pop 操作棧頂存儲的地址(addr),然后取地址為 addr 的大小為 size 的內(nèi)存數(shù)據(jù)踏揣,push 到操作棧頂庆亡。

  • 在 clang 生成的 AST 中,所有 VarDeclRef 其實對應(yīng)的是變量的地址捞稿,對于訪問變量內(nèi)容(變量右值)的代碼又谋,AST 中 VarDeclRef 的父節(jié)點都是左右值轉(zhuǎn)換節(jié)點钝尸。因此OCPack中 push 指令,類似push seg index都是將 seg 段 index 處的地址 push 進操作棧搂根,而取對應(yīng)地址處的具體內(nèi)容由左右值轉(zhuǎn)換指令來完成珍促。

注: 在實現(xiàn)初期,OCPack的 push 指令是直接將 seg 段 index 處變量的右值 push 進操作棧(即這種情況下忽略左右值轉(zhuǎn)換的結(jié)點)剩愧,但后來發(fā)現(xiàn)在類似賦值操作中的左值變量的情況下猪叙,AST 中沒有左右值轉(zhuǎn)換結(jié)點,如果對這些情況特殊處理仁卷,邏輯會變得較為復(fù)雜且難以保證覆蓋完全穴翩,后來決定完全依照 AST 中結(jié)點的排布邏輯,將 push 操作的對象改成了對應(yīng)變量的左值锦积,犧牲部分性能換取程序的可靠性芒帕。

2.9 Objective-C 方法調(diào)用指令
  • 指令集中有專用于調(diào)用 Objective-C 方法的指令 OBJC_MSG_CLASS/OBJC_MSG_INST。

  • 虛擬機在解釋執(zhí)行此指令時丰介,先取得存儲在棧上的所有參數(shù)背蟆,然后構(gòu)造相應(yīng)的 NSInvocation,通過 invoke 來實現(xiàn)對 Objective-C runtime 的調(diào)用哮幢。

  • 指令實現(xiàn)中带膀,對于 target 和參數(shù)都采用 __unsafe_unretained 方式進行引用,即不改變其生命周期橙垢。返回值則一律使用 autorelease 方式垛叨,確保返回值在返回給直接調(diào)用者時是有效的。

注: 實現(xiàn)過程中柜某,Objective-C 調(diào)用指令所需的輸入數(shù)據(jù)的內(nèi)存排布順序也經(jīng)歷了一番修改嗽元。因為對于 Objective-C 方法來說,只有拿到 selector 才能知道具體有多少個參數(shù)喂击,所以之前設(shè)計是參數(shù)表倒著放剂癌,即第一個參數(shù)放棧頂,第二個參數(shù)依次往下排惭等。這樣可以穩(wěn)定地 pop 兩次就得到 selector 的聲明珍手,然后再根據(jù) selector 中指明的參數(shù)個數(shù)及大小 pop 所有的參數(shù)办铡。但這種方法在參數(shù)大小大于 64 bit 的情況下(如 struct)就比較難處理了辞做,因為要得到正確的 struct 數(shù)據(jù),程序需要 pop 對應(yīng)個數(shù)的64 bit寡具,然后做拼接秤茅,煩瑣而且容易出錯。經(jīng)權(quán)衡童叠,還是在指令參數(shù)中增加了參數(shù)表長度(編譯期得到)框喳,在調(diào)用 OBJC_MSG_CLASS/OBJC_MSG_INST 指令前课幕,參數(shù)還是按順序push(即第一個參數(shù)先 push,棧頂是最后一個參數(shù))五垮,在指令的實現(xiàn)中乍惊,根據(jù)指令參數(shù)中提供的參數(shù)表長度,直接從 sp 算出第一個參數(shù)的起始位置放仗,這樣所有的參數(shù)都可以用指針訪問润绎,而不用關(guān)心其大小了。原先需要的多次 pop 指令诞挨,變成只需在指令退出前莉撇,將 sp 回退參數(shù)表長度即可。

匯編模塊

功能:匯編程序(.s) -> 二進制補丁程序(.bin)

解析整個 .s 文本惶傻,將文本 token 轉(zhuǎn)換為對應(yīng)的二進制數(shù)據(jù)棍郎,主要包括:

  • 立即數(shù)從文本到二進制數(shù)據(jù)的轉(zhuǎn)換

  • 跳轉(zhuǎn) label 到地址的轉(zhuǎn)換

  • 常量字符串的轉(zhuǎn)換,此處一開始直接在 .s 中存儲了字符串银室,后來遇到 '\n\t' 等情況不能很好地支持涂佃,就改成了直接存儲字節(jié)碼

  • 生成導(dǎo)出函數(shù)表,表中記錄了虛擬機中定義的方法名和地址的對應(yīng)關(guān)系

  • 生成導(dǎo)入函數(shù)表蜈敢,包含了所有調(diào)用到的c方法聲明及其 index巡李,代碼段中調(diào)用 c 方法時直接調(diào)用此處的 index

  • static 數(shù)據(jù)段大小和全局區(qū)的總大小,因為虛擬機初始化時需要將全局區(qū)放到一段 shared & anonymous mmap 內(nèi)存上扶认,故需要此 size

  • 存儲 GUID 值

  • 存儲 Target arch侨拦,此值用于驗證 32bit 和 64bit,確保平臺和 .bin 文件的匹配

  • 文本指令轉(zhuǎn)化為二進制指令

轉(zhuǎn)換完成后將各數(shù)據(jù)存入內(nèi)存中相應(yīng)的數(shù)據(jù)段辐宾,再將整個內(nèi)存 dump 成一個二進制文件狱从。

注: 二進程文件在運行時所需的大部分?jǐn)?shù)據(jù)其排布都與 .bin 文件里的排布完全相同,這樣能方便地使用內(nèi)存映射來實現(xiàn) .bin 文件的加載叠纹,從而可以減少私有內(nèi)存的占用量季研。

加載模塊

功能:二進制補丁程序(.bin)加載

在調(diào)用 load_image 時,虛擬機會先將 .bin 文件 mmap 到一段內(nèi)存中誉察,檢測 magic number, bin version 及 arch 是否匹配与涡。

然后按全局區(qū)的大小申請一段 shared anonymous mmap 內(nèi)存。

然后分別加載各個數(shù)據(jù)段持偏,建立必要的運行時內(nèi)存數(shù)據(jù)驼卖,主要的數(shù)據(jù)段包括:

  • 常量字符串段,將全局區(qū)對應(yīng)大小的內(nèi)存分配給常量段鸿秆,并將對應(yīng)的 index 指向?qū)?yīng)的字符串起始地址

  • 靜態(tài)數(shù)據(jù)段酌畜,將全局區(qū)對應(yīng)大小的內(nèi)存分配給靜態(tài)段

  • 導(dǎo)出符號表

  • 導(dǎo)入符號表

  • 代碼段

  • GUID數(shù)據(jù)

注:bin文件具體格式如下: [圖片上傳中...(image-1b9c27-1603349931409-1)]

執(zhí)行模塊

功能:二進制補丁程序(.bin)的執(zhí)行

1) 虛擬機基本信息

  • 棧和各個段都以 64bit 為單位

  • 調(diào)用方法前,需要將對應(yīng)的參數(shù) push 到操作棧上

  • 調(diào)用完成后卿叽,棧頂放的就是返回值

  • 運行時上下文(thread_context)和棧幀

    • thread_context 維護一個棧幀的鏈表桥胞,用以存放調(diào)用關(guān)系

    • 棧幀用于存放當(dāng)前方法的運行時信息恳守,主要包括:

      • 棧幀基地址
      • 返回地址
      • 指向調(diào)用方棧幀基地址的指針
      • 段表基地址
      • 操作棧基址
      • 操作棧指針 sp
      • 程序計數(shù)器 ip

注:VM 函數(shù)棧幀具體內(nèi)存布局如下:

[圖片上傳中...(image-370312-1603349931409-0)]

2) Objective-C 調(diào)用虛擬機方法

  • Objective-C 代碼通過向虛擬機 的 callFunctionWithArgs: 方法傳入要調(diào)用函數(shù)名及其參數(shù)(此處的參數(shù)為真正參數(shù)的地址)并得到返回值來與虛擬機進行交互贩虾。

  • callFunctionWithArgs: 方法內(nèi)部會通過函數(shù)名查找導(dǎo)出表催烘,找到其方法簽名和地址。然后根據(jù)方法簽名中指定的參數(shù)大小缎罢,將傳入的參數(shù)地址處對應(yīng)大小的數(shù)據(jù) push 到操作棧上颗圣,然后再跳轉(zhuǎn)到被調(diào)方法的開始地址處,開始執(zhí)行匯編指令屁使。

  • 虛擬機執(zhí)行完所有匯編指令后返回到 callFunctionWithArgs: 中在岂,該方法再負(fù)責(zé)把棧頂?shù)姆祷刂禂?shù)據(jù)拷貝到調(diào)用方傳入的返回值地址處。

  • 調(diào)用完成后蛮寂,虛擬機的棧頂?shù)刂窇?yīng)該保持與調(diào)用前完全一致蔽午。

  • 如果虛擬機方法的返回值是 NSObject* 類型,OCPack會根據(jù)存儲返回值的變量是否是強引用而決定是否需要對返回的對象做__brige_retained操作酬蹋,用以中和調(diào)用方對 strong 類型變量的 release 操作及老。其他情況下因為返回的都是 autorelease 的對象,返回時不做特殊處理(詳見編譯模塊小節(jié)中 2.9 段Objective-C方法調(diào)用指令)范抓。

3) 虛擬機調(diào)用 OC 方法(f1)骄恶,f1 又調(diào)用到了虛擬機的方法(f2)

要支持此流程,需保證 f2 調(diào)用完成后虛擬機當(dāng)前棧幀的 sp 與調(diào)用前完全一致匕垫,以保證 f1 的執(zhí)行不受影響僧鲁。

4) 虛擬機方法間互相調(diào)用

在調(diào)用OC方法時,會先檢測對應(yīng)的方法是否在導(dǎo)出函數(shù)表中象泵,如果在寞秃,則走此流程。這也要求調(diào)用虛擬機方法時的參數(shù)表應(yīng)該與直接調(diào)用 OC 方法是一致的偶惠,否則還需要重新拷貝參數(shù)做適配春寿,降低虛擬機性能。

5) 多線程支持

  • 運行時上下文(thread_context)指針放在線程局部存儲中忽孽,每個線程在讀绑改、寫上下文中數(shù)據(jù)時都是操作當(dāng)前線程自己的數(shù)據(jù),這樣就能保證各個線程之間運行狀態(tài)相互隔離兄一,從而支持多線程的調(diào)用場景厘线。

  • OCPack注冊了線程退出的回調(diào)函數(shù),當(dāng)一個線程退出時OCPack會刪除所有虛擬機在此線程中的上下文相關(guān)數(shù)據(jù)瘾腰。

6) 內(nèi)存占用

  • 二進制文件加載使用 mmap皆的,全局?jǐn)?shù)據(jù)區(qū)使用shared & anonymous mmap覆履,常量字符串?dāng)?shù)據(jù)直接指向 .bin 中的地址蹋盆。

  • 運行時上下文(thread_context)是每個線程一份费薄,都使用shared & anonymous mmap。

  • 運行時的虛擬機類本身只維護導(dǎo)入函數(shù)和導(dǎo)出函數(shù)數(shù)據(jù)以及少量指針數(shù)據(jù)栖雾。

7) 崩潰時的椑懵眨回溯

  • 運行時上下文(thread_context)中維護有一個棧幀結(jié)構(gòu)(thread_frame)的鏈表,對應(yīng)的就是虛擬機中方法的調(diào)用關(guān)系析藕。

  • 崩潰時遍歷所有線程中所有虛擬機的 thread_context召廷,遍歷其棧幀結(jié)構(gòu)鏈表,取出每個棧幀中存儲的 ip 地址寫到崩潰日志中账胧。

  • 崩潰日志中按照線程組織崩潰棧竞慢,還會記錄每個虛擬機的地址和其加載的 .bin 文件的 GUID,用以正確區(qū)分線程治泥、虛擬機的實例和 bin 文件筹煮。

8) 崩潰符號解析

OCPack編譯器生成或指定一個 GUID,后續(xù)生成的所有相關(guān)文件(包括.s居夹、.sym败潦、.bin以及運行時生成的崩潰 log)中都存有此 GUID。線上的崩潰日志發(fā)送回來后准脂,崩潰解析服務(wù)器能夠根據(jù)日志中的 GUID 查找到相應(yīng)的符號文件進行符號解析劫扒。同時構(gòu)建服務(wù)器數(shù)據(jù)庫中存儲了對應(yīng) GUID 的 bin 文件打包時所有依賴項的源信息(包括對應(yīng)的 .s 文件、bin 代碼對應(yīng)的源代碼版本狸膏、OCPack工具鏈的版本等)沟饥,方便開發(fā)重現(xiàn)、定位相關(guān)問題湾戳。

9) Hook Objective-C 方法

  • 原理與 JSPatch 類似闷板,通過將目標(biāo)類的目標(biāo)方法替換為 objc_msgForward,同時將 forwardInvocation 替換為自定義的 forward 方法院塞,實現(xiàn)當(dāng)目標(biāo)方法被調(diào)用時遮晚,轉(zhuǎn)向 forwardInvocation 的自定義實現(xiàn)。在自定義的 forward 的實現(xiàn)中拦止,將 NSInvocation 傳給內(nèi)置的虛擬機县遣,虛擬機會取出其參數(shù)并調(diào)用相應(yīng)的虛擬機方法,最后將返回值設(shè)回給 NSInvocation汹族,即完成了 Hook 的功能萧求。

  • 與 JSPatch 相比,用OCPack的方法調(diào)用省去了大量的字符串解析操作顶瞒,參數(shù)大都可直接傳入虛擬機進行處理夸政,方法調(diào)用的整體開銷比 JSPatch 小。

性能優(yōu)化

  1. 二進制程序的大小優(yōu)化
  • OCPack在實現(xiàn)初期榴徐,采用了模板類的方式實現(xiàn)一守问、二元操作符的對不同操作數(shù)和返回值類型的支持匀归,這樣調(diào)試起來比較方便。但后來發(fā)現(xiàn)這種方案會導(dǎo)致代碼體積會暴增耗帕。模板方法會根據(jù)輸入穆端、輸出參數(shù)的類型生成大量的方法,而其中大部分方法都只有很短的幾條指令仿便,從最后的二進制內(nèi)容分析看体啰,光方法名就占用了大量內(nèi)存。

    • 在功能基本穩(wěn)定后嗽仪,在單元測試的保證下荒勇,將模板改為了宏實現(xiàn),大幅地減小代碼段和數(shù)據(jù)段的體積闻坚,framework 文件的大小從3.5M減到了不到150k枕屉。
  1. 性能優(yōu)化
  • 匯編代碼優(yōu)化

    • 由語法樹直接生成的匯編代碼里面會有很多無用的 push 操作,主要原因是某些表達式的返回值沒有被用到鲤氢,此時 push 是多余的搀擂,而且在比較大的循環(huán)還可能會使棧長度暴增,影響性能和穩(wěn)定性

    • 優(yōu)化方法是盡量去掉無用的 push 指令卷玉。主要是通過定義一些規(guī)則來判別某指令的返回值是否被用到哨颂,對于無用的表達式返回值不進行 push 操作。

  • 虛擬機性能優(yōu)化

    • 將頻繁用到的數(shù)據(jù)緩存下來:取運行時上下文和讀寫棧幀是頻繁操作相种,涉及線程局部存儲數(shù)據(jù)的讀取威恼,運行時上下文結(jié)構(gòu)的指針只會在一個線程中訪問,通過對代碼進行重構(gòu)寝并,將它緩存到 executor 類中箫措,提高了運行時效率。

    • 盡量減少核心循環(huán)處代碼的內(nèi)存訪問次數(shù):將棧幀中的部分?jǐn)?shù)據(jù)(如:ip)提出來放到 executor 類中衬潦,減少頻繁讀寫ip時導(dǎo)致的不必要的內(nèi)存操作次數(shù)斤蔓。

    • 盡量提高核心循環(huán)處代碼效率:用數(shù)組代替 map 實現(xiàn)指令到指令處理器的映射關(guān)系,提高運行時查詢效率镀岛。

    • 優(yōu)化后比優(yōu)化前提高了將近一倍

III. 未來計劃

鏈接器

  • 支持多個 .m 對應(yīng)的多個 .bin 鏈接成一個 .bin 文件弦牡,且各個 .bin 文件之間能夠互相調(diào)用

其他語法支持

  • 支持聲明 block 的語法

性能優(yōu)化

  • 去掉虛函數(shù)調(diào)用
  • 指令長度對齊
  • 保證 sp、ip 等頻繁操作的數(shù)據(jù)都放入寄存器——匯編實現(xiàn)相關(guān)核心方法漂羊、或者改變代碼結(jié)構(gòu)
  • 生成匯編代碼的優(yōu)化(窺孔優(yōu)化等)
  • 其他

Debug工具

  • 因為虛擬機執(zhí)行時是按指令執(zhí)行的驾锰,不容易直接對應(yīng)到 Objective-C 代碼,調(diào)試起來有點麻煩走越,后續(xù)計劃做一些功能椭豫,調(diào)試時方便地顯示指令地址到 Objective-C 源代碼的對應(yīng)關(guān)系。

作者:zhangjiezhi_
鏈接:https://juejin.im/post/6884833291740905480
來源:掘金
著作權(quán)歸作者所有。商業(yè)轉(zhuǎn)載請聯(lián)系作者獲得授權(quán)赏酥,非商業(yè)轉(zhuǎn)載請注明出處喳整。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市今缚,隨后出現(xiàn)的幾起案子算柳,更是在濱河造成了極大的恐慌低淡,老刑警劉巖姓言,帶你破解...
    沈念sama閱讀 206,378評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異蔗蹋,居然都是意外死亡何荚,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評論 2 382
  • 文/潘曉璐 我一進店門猪杭,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事万细〉侔” “怎么了?”我有些...
    開封第一講書人閱讀 152,702評論 0 342
  • 文/不壞的土叔 我叫張陵蜂筹,是天一觀的道長需纳。 經(jīng)常有香客問我,道長艺挪,這世上最難降的妖魔是什么不翩? 我笑而不...
    開封第一講書人閱讀 55,259評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮麻裳,結(jié)果婚禮上口蝠,老公的妹妹穿的比我還像新娘。我一直安慰自己津坑,他們只是感情好妙蔗,可當(dāng)我...
    茶點故事閱讀 64,263評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著疆瑰,像睡著了一般灭必。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上乃摹,一...
    開封第一講書人閱讀 49,036評論 1 285
  • 那天禁漓,我揣著相機與錄音,去河邊找鬼孵睬。 笑死播歼,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播秘狞,決...
    沈念sama閱讀 38,349評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼叭莫,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了烁试?” 一聲冷哼從身側(cè)響起雇初,我...
    開封第一講書人閱讀 36,979評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎减响,沒想到半個月后靖诗,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,469評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡支示,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,938評論 2 323
  • 正文 我和宋清朗相戀三年刊橘,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片颂鸿。...
    茶點故事閱讀 38,059評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡促绵,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出嘴纺,到底是詐尸還是另有隱情败晴,我是刑警寧澤,帶...
    沈念sama閱讀 33,703評論 4 323
  • 正文 年R本政府宣布栽渴,位于F島的核電站尖坤,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏熔萧。R本人自食惡果不足惜糖驴,卻給世界環(huán)境...
    茶點故事閱讀 39,257評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望佛致。 院中可真熱鬧贮缕,春花似錦、人聲如沸俺榆。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽罐脊。三九已至定嗓,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間萍桌,已是汗流浹背宵溅。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留上炎,地道東北人恃逻。 一個月前我還...
    沈念sama閱讀 45,501評論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親寇损。 傳聞我的和親對象是個殘疾皇子凸郑,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,792評論 2 345