美团 iOS 工程 zsource 命令背后的那些事儿
zsource 命令是什么?
美團 App 在 2015 年就已經基于 CocoaPods 完成了組件化的工作。在組件化的改造過程中,為了能夠加速整體工程的構建速度,我們對需要集成進美團 App 的組件進行了二進制化,同時提供一個叫做 cocoapods-binary 的 CocoaPods 插件來支持本地工程使用二進制。因此,美團 App 的開發者在集成開發時,除了自己正在開發的組件,其他的組件都以二進制的形式存在。
使用二進制,雖然會給工程帶來構建速度的提升,但是會帶來一個新的問題:在調試工程時,那些使用二進制的組件,無法像源碼調試那樣看到足夠豐富的調試信息。例如,如果程序在二進制組件的代碼中崩潰,我們只能看到該組件的堆棧信息和一些不明所以的匯編代碼:
和業界大多的組件化方案類似,美團 App 的組件化方案也提供了將一個組件從二進制切換到源碼的機制。美團工程的開發者能夠使用一系列配置和命令來切換組件的源碼和二進制狀態,但每次切換都需要重新執行 pod install。這種方式在組件化的初期是沒有什么問題的。但隨著美團 App 的組件數量不斷增長,即便是只切換一個組件的狀態,單次 pod install 的時間也增長到了分鐘級。而且這種方式每切換一次就必須重新編譯運行一次 App,在追查一些偶現崩潰問題時,開發體驗非常不友好,也不利于崩潰問題的快速定位分析。
為了解決以上提到的這些問題,我們利用 CocoaPods 的插件機制,為 CocoaPods 的 pod 命令增加了 zsource 子命令,開發者可以在使用二進制構建工程的同時,非常快速地將一個組件調出源碼進行調試,具體的使用效果可以看一下如下的屏幕錄制:
zsource 命令的開發始末
在推出 zsource 功能后,很多同學都對 zsource 背后的技術原理十分感興趣。其實 zsource 整個功能的開發流程也十分的有趣,就像小說一樣,分為幾個不同的時期:
- 原理猜想
- 查閱資料
- 簡單粗暴的嘗試
- 柳暗花明
- 工程化
原理猜想
如果讓我們猜想 Xcode 斷點調試功能的實現原理,可能大部分人都會猜這樣一種可能:Xcode 在編譯 Debug 版本的二進制過程中,在二進制中某個字段存儲了該二進制所對應的源碼的文件地址。當我們在 Xcode 中打斷點進行調試的時候,Xcode 會根據二進制中這個字段中存儲的源碼文件地址,打開對應的源碼文件,并在 UI 上展示該源碼文件。
道理好像沒有什么問題,但是事實是這樣嗎?在某次團建回國的航班上,我們組成威和志宇兩位同學在提出這種猜想后,拿出電腦,做了一個這樣的小實驗:
實驗中,他們分別創建了兩個 Xcode 工程 A 和 B,工程 A 會產出一個二進制 libA.a。工程 B 中會接將 A 的產出 libA.a 拖到工程中,然后設置 A 中代碼的符號斷點,然后編譯運行。結果發現,當斷點斷在 A 中的代碼時,Xcode 會直接跳轉到 A 的源文件中,并且可以繼續增加斷點以及正常的單步調試。
通過這個實驗,成威和志宇同學確定了猜想的正確性。那么接下來需要做的,就是確定二進制中,這個源文件地址信息具體藏在哪一個字段中。
查閱資料
我們都知道蘋果的 Mach-O 二進制文件使用的是 DWARF 這種格式來存放調試相關的數據的。但因為我們很難從這個問題中提煉幾個精確的關鍵詞在搜索引擎中檢索,所以很難通過簡單的幾次檢索就獲取到我們想要的答案:二進制這個字段的名稱,在初期甚至無法確定這個字段應該是從 Mach-O 的資料中檢索還是從 DWARF 的資料中檢索。
在沒有太好的搜索結果的情況下,我們一度曾經想嘗試去從頭去啃一啃找到的一些二進制相關的文檔:
- osx-abi-macho-file-format-reference
- Introduction to the DWARF Debugging Format
- DWARF 1.1.0 Reference
簡單粗暴的嘗試
然而,由于對二進制格式不是那么熟悉,也不太了解二進制相關的詞匯和概念,所以閱讀文檔的速度就非常緩慢。
不過,技術的有趣之處就在于,有時候你可以基于我們的猜想,任意去嘗試,跳過艱辛的文檔閱讀過程。在文檔閱讀遇到挫折后,我們猜想,二進制中很有可能也是用字符來存儲這些源碼信息的,那么如果我們就把二進制當做字符來看,是不是能搜到一些東西呢?
于是我們試著做了一個比較簡單的二進制文件,二進制文件中僅僅包含一個 ZSCViewController,然后用 xxd 這個命令嘗試讀取二進制中的內容,考慮到 xxd 的輸出會折行,我們選取了 ZSCViewController 字符串的子串進行過濾:
xxd ./libZSource.a | grep -C 5 'ZSCViewControlle'果真得到了一些結果:
通過這個實驗,我們確定了二進制中源碼文件的路徑確實是用普通的字符來存儲的;緊接著,我們用 MachOViewer 來查看二進制文件,以獲取到更友好的二進制信息。利用 MachOViewer,我們可以發現這些信息都存在了二進制的 “__debug_str” Section 中。
雖然還是不確定這個地址所對應的字段叫什么,但研究到這里,我們還是有所進展的,最起碼我們可以假定這個路徑一定是緊跟在 “Apple LLVM version 10.0.0 ” 字符后面的,然后利用一些讀取 Mach-O 的 Ruby 庫,比如 ruby-macho,基于這個假定來讀取這個路徑,為這個特性的工具化提供一絲可能性。
柳暗花明
簡單的嘗試沒有得到想要的答案,但透過 Section 的名字,可以確定源碼文件的路徑信息和 DWARF 有關。
長時間和 CI 打交道的經驗告訴我們,對于每一種二進制格式,蘋果公司都會提供一個可以專門用于解析的命令行工具,所以我們就嘗試找了找有沒有解析二進制中 DWARF 格式的命令行工具。
功夫不負有心人,我們找到了 dwarfdump,那么用它來看看之前的那個二進制文件:
dwarfdump ./libZSource.a | grep 'ZSCViewContro'果然有了更好的輸出:
這里我們注意到了 AT_name 這個字段名。拿著這個字段名,去前面給出的 DWARF 1.1.0 Reference 文檔中查閱,我們可以得知:
An AT_name attribute whose value is a null-terminated string containing the full or relative path name of the primary source file from which the compilation unit was derived.
進一步查詢,我們可以找到另一個和他類似的字段 —— AT_comp_dir:
An AT_comp_dir attribute whose value is a null-terminated string containing the current working directory of the compilation command that produced this compilation unit in whatever form makes sense Forelax the host system.
看起來,這兩個字段就是我們所苦苦追尋的答案了。
工程化
通過實驗,以及找到的這兩個字段的描述,我們基本可以確定,即便工程是使用二進制構建,只要二進制 AT_name 字段中的路徑存在對應的源碼文件,App 一樣可以使用源碼進行斷點調試。這種調試方式除了修改源碼再次構建不能生效以外,其他的調試場景都和直接使用源碼構建無異。考慮到我們日常的調試場景絕大多數都只需要查看其他組件的源碼,并不需要修改,把這個功能工程化還是非常有意義的。
那接下來的事情就比較簡單了:
幸運的是,查看完美團 App 的幾百個組件后,我們發現只有少數近一年內沒有制作過二進制的組件路徑比較不同,其他都相同,因此可以先忽略這一小部分組件。如果這部分組件需要支持該功能,只要再制作一次二進制即可。
確定方案以后,寫代碼就很簡單了,最終我們利用 CocoaPods,提供了 zsource 的三個命令:
總結
zsource 功能整體的開發過程基本上都是基于一個個的猜想和實驗來完成的,整體的開發上線過程實際上只花了兩個晚上。但如果在沒有基礎知識的情況下,選擇把上文中提到的參考資料都看懂后再動手,可能會花費更多的時間。這一個有趣的驗證過程也充分說明,有時候我們可以不拘泥于冗長的文檔以及資料,通過類似逆向工程的方式,非常快速地拿到我們需要的答案。此時我們再回過頭去看文檔,可能會獲得比直接看文檔更好的效果。
最后,非常感謝成威老師和志宇同學對技術的崇高追求,即便在飛機上,也愿意拿出電腦驗證自己的猜想,為 zsource 后續的工程化落地提供了更多的可能。
作者簡介
- 宇杰,美團 iOS 工程師,2016 年加入美團,先后參與美團 App 持續集成平臺建設、美團 App ReactNative 平臺化等工作。目前在參與美團 App 工程效率提升和 Flutter 應用的相關工作。
總結
以上是生活随笔為你收集整理的美团 iOS 工程 zsource 命令背后的那些事儿的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 顶会论文:基于神经网络StarNet的行
- 下一篇: Spring Cloud Hoxton正