1. 程式人生 > >iOS 安裝包瘦身 (上篇)

iOS 安裝包瘦身 (上篇)

本文來自網易雲社群

作者:饒夢雲

1. 安裝包組成

談到 App 瘦身,最直接的想法莫過於分析一個安裝包內部結構,瞭解其每一部分的來源。解壓一個 ipa 包,拿到其 payload 中 app 檔案的資料,整理歸類後其大致如下:

  • Exectutable: 可執行檔案

  • Resources:資原始檔

    • 圖片資源:Assets.car/bundle/png/jpg 等

    • 視訊/音訊資源:mp4/mp3 等

    • 靜態網頁資源:html/css/js 等

    • 檢視資源:xib/storyboard 等

    • 其他:文字/字型/證書 等

  • Framework:專案中使用的動態庫

    • SwiftSupport: libSwiftxxx 等一系列 Swift 庫

    • 其他依賴庫:Embeded Framework

  • Pulgins:Application Extensions

    • appex:其組成大致與 ipa 包組成一致

從以上結構中可以看出一個 ipa 包大致由 Executable, Resources, Framework,Plugins 四大模組組成,接下來我們就從這四個方向來探討 App 瘦身的具體方案。

2. 可執行檔案瘦身

可執行檔案就是我們原始碼(.m/.h/.swift ...)的編譯結果。在 iOS 或者 macOS 中稱之為 Mach-O executable,它是程式的入口。

2.1. 編譯器優化

Xcode 支援編譯器層面的一些優化優化選項,可以讓我們介於更快的編譯速度和更小的二進位制大小並且更快的執行速度之間自由選擇想要進行的優化粒度。

2.1.1. Clang/LLVM 編譯器優化選項

我們都知道 Xcode 是使用 Clang 來編譯 Objective-C 語言的,Clang 的優化選項在其文件 clang - Code Generation Options 中可以查閱得到。我們的 IDE-Xcode 只提供給我們 6 個等級的編譯選項,在 Xcode -> Build Setting -> Apple LLVM 9.0 - Code Generation -> Optimization Level 中進行設定,每個等級的說明,可以參考官方文件

xcode-clang-optimization

  • None[-O0]: 編譯器不會優化程式碼,意味著更快的編譯速度和更多的除錯資訊,預設在 Debug 模式下開啟。

  • Fast[-O,O1]: 編譯器會優化程式碼效能並且最小限度影響編譯時間,此選項在編譯時會佔用更多的記憶體。

  • Faster[-O2]:編譯器會開啟不依賴空間/時間折衷所有優化選項。在此,編譯器不會展開迴圈或者函式內聯。此選項會增加編譯時間並且提高程式碼執行效率。

  • Fastest[-O3]:編譯器會開啟所有的優化選項來提升程式碼執行效率。此模式編譯器會執行函式內聯使得生成的可執行檔案會變得更大。一般不推薦使用此模式。

  • Fastest Smallest[-Os]:編譯器會開啟除了會明顯增加包大小以外的所有優化選項。預設在 Release 模式下開啟。

  • Fastest, Aggressive Optimization[-Ofast]:啟動 -O3 中的所有優化,可能會開啟一些違反語言標準的一些優化選項。一般不推薦使用此模式。

Fastest Smallest[-Os] 極小限度會影響到包大小,而且也保證了程式碼的執行效率,是最佳的釋出選項,一般 Xcode 會在 Release 下預設選擇 Fastest Smallest[-Os] 選項,較老的專案可能沒有自動勾選。

XCode 中設定的選項最終會反應在 Clang 命令上面,開啟 build log 可以看到此選項最終的表現形式:

clang/llvm-optimization-level-command

如果你還需要 clang 的其他選項來編譯你的專案,可以在 Other C Flag 中直接新增其引數。舉例來說,在 Optimization Level 中設定 Fastest Smallest[-Os] 和在 Other C Flags 中新增 -Os 效果是一樣的。

2.1.2. Swift Complier/LLVM 編譯優化選項

Swift 語言的編譯器是 swiftlang,同時也是基於 LLVM 後端的。Xcode 9.3 版本之後 Swift 編譯器會提供新的選項來幫助減少 Swift 可執行檔案的大小:

xcode-swift-llvm-optimization

  • No optimization[-Onone]:不進行優化,能保證較快的編譯速度。

  • Optimize for Speed[-O]:編譯器將會對程式碼的執行效率進行優化,一定程度上會增加包大小。

  • Optimize for Size[-Osize]:編譯器會盡可能減少包的大小並且最小限度影響程式碼的執行效率。

Xcode 9.3 以前和優化選項混雜在一起的編譯模式可以獨立設定了:

xcode-swift-compilation

  • Single File:單個檔案優化,可以減少增量編譯的時間,並且可以充分利用多核 CPU,並行優化多個檔案,提高編譯速度。但是對於交叉引用無能為力。

  • Whole Module:模組優化,最大限度優化整個模組,能處理交叉引用。缺點不能利用多核 CPU 的優勢,每次編譯都會重新編譯整個 Module。

在 Relese 模式下 -Osize 和 Whole Module 同時開啟效果會發揮的最好,從現有的案例中可以看到它會減少 5%~30% 的可執行檔案大小,並且對效能的影響也微乎其微(大約 5%)。參考官方文件SwiftCafe

此選項雖然是 Xcode 9.3 支援的,但是我們發現 Xcode 9.2 對應的 Swift Compiler 也是支援 Osize 的。所以 Xcode 9.2 版本中可以在 Build Settings -> Other Swift Flags 中新增 -Osize 提前獲取編譯器優化的好處。

xcode9.2-swiftc-Osize

雖然 Xcode 9.3/Swift4.1 已經發布,但是其編譯器不是特別穩定,特別是開啟 Osize 選項之後,編譯器很多情況下會莫名其妙的崩潰(Segmentation fault),我們在 bugs.swift.org 上面也找到了很多同類的崩潰。所以假如你使開啟 Osize 之後遇到了同類的崩潰,你可以選擇放棄 Osize,或者想辦法修改程式碼繞開編譯器崩潰。

2.2. 去除符號資訊

可執行檔案中的符號)是指程式中的所有的變數、類、函式、列舉、變數和地址對映關係,以及一些在除錯的時候使用到的用於定位程式碼在原始碼中的位置的除錯符號,符號和斷點定位以及堆疊符號化有很重要的關係。

2.2.1. Strip Style

Strip Style 表示的是我們需要去除的符號的型別的選項,其分為三個選擇項:

  • All Symbols: 去除所有符號,一般是在主工程中開啟。

  • Non-Global Symbols: 去除一些非全域性的 Symbol(保留全域性符號,Debug Symbols 同樣會被去除),連結時會被重定向的那些符號不會被去除,此選項是靜態庫/動態庫的建議選項。

  • Debug Symbols: 去除除錯符號,去除之後將無法斷點除錯。

iOS 的除錯符號是 DWARF 格式的,相關概念如下:

  • Mach-O: 可執行檔案,原始檔編譯連結的結果。包含對映除錯資訊(物件檔案)具體儲存位置的 Debug Map。

  • DWARF:一種通用的除錯檔案格式,支援原始碼級別的除錯,除錯資訊存在於 物件檔案 中,一般都比較大。Xcode 除錯模式下一般都是使用 DWARF 來進行符號化的。

  • dSYM:獨立的符號表檔案,主要用來做釋出產品的崩潰符號化。dSYM 是一個壓縮包,裡面包含了 DWARF 檔案。

dwarf_dsym_executable

使用 Xcode 編譯打包的時候會先通過可執行檔案的 Debug Map 獲取到所有物件檔案的位置,然後使用 dsymutil 來將物件檔案中的 DWARF 提取出來生成 dSYM 檔案。

2.2.2. Strip Linked Product

If enabled, the linked product of the build will be stripped of symbols when performing deployment postprocessing.

並不是所有的符號都是必須的,比如 Debug Map,所以 Xcode 提供給我們 Strip Linked Product 來去除不需要的符號資訊(Strip Style 中選擇的選項相應的符號),去除了符號資訊之後我們就只能使用 dSYM 來進行符號化了,所以需要將 Debug Information Format 修改為 DWARF with dSYM file。

我之前一直疑惑沒有 DWARF 除錯資訊之後 Xcode 是靠什麼來生成 dSYM 的,答案其實還是 DWARF,因為 Xcode 編譯實際的操作步驟是:生成帶有 DWARF 除錯資訊的可執行檔案 -> 提取可執行檔案中的除錯資訊打包成 dSYM -> 去除符號化資訊。去除符號是單獨的步驟,使用的是 strip 命令。

另外一個問題是,去除符號化資訊之後我們只能使用 dSYM 來進行符號化,那我們使用 Xcode 來進行除錯的時候會不會太麻煩了?其實我們完全不用擔心這個問題:Strip Linked Product 選項在 Deployment Postprocessing 設定為 YES 的時候才生效,而在 Archive 的時候 Xcode 總是會把 Deployment Postprocessing 設定為 YES 。所以我們可以開啟 Strip Linked Product 並且把 Deployment Postprocessing 設定為 NO,而不用擔心除錯的時候會影響斷點和符號化,同時打包的時候又會自動去除符號資訊。這個選項也是預設開啟的,較老的專案可以選擇手動開啟。

2.2.3. Strip Debug Symbols During Copy

Specifies whether binary files that are copied during the build, such as in a Copy Bundle Resources or Copy Files build phase, should be stripped of debugging symbols. It does not cause the linked product of a target to be stripped—use Strip Linked Product (STRIP_INSTALLED_PRODUCT) for that.

與 Strip Linked Product 類似,但是這個是將那些拷貝進專案包的三方庫、資源或者 Extension 的  Debug Symbol 去除掉,同樣也是使用的 strip 命令。這個選項沒有前置條件,所以我們只需要在 Release 模式下開啟,不然就不能對三方庫進行斷點除錯和符號化了。

如果依賴的 Target 是獨立簽名的(比如 App Extension),strip 操作就會失效,並伴隨著 Warning:warning: skipping copy phase strip, binary is code signed: xxxx。此情況將依賴的 Target 中的 Strip Linked Product 修改為 YES,保證依賴的 Target 是已經去除了符號即可,Waning 忽略掉就可以了。

Cocoapods 管理的動態庫(use_framework!)的情況就相對要特殊一點,因為 Cocoapods 中的的動態庫是使用自己實現的指令碼 Pods-xxx-frameworks.sh 來實現拷貝的,所以並不會走 Xcode 的流程,當然也就不受 Strip Debug Symbols During Copy 的影響。當然 Cocoapods 是原始碼管理的,所以只需要將原始碼 Target 中的 Strip Linked Product 設定為 YES 即可。

2.2.4. Strip Swift Symbols

Adjust the level of symbol stripping specified by the STRIP_STYLE setting so that when the linked product of the build is stripped, all Swift symbols will be removed.

開啟 Strip Swift Symbols 能幫助我們移除相應 Target 中的所有的 Swift 符號,這個選項也是預設開啟的。

補充一點:Swift ABI 穩定之前,Swift 標準庫是會打進目標檔案的,想要同時移除 Swift 標準庫裡面的符號的話需要在釋出選項中勾選 Strip Swift symbols,如下圖所示:

distribution_strip_swift_symbols

2.3. BitCode

BitCode 是 iOS 9 引入的新特性,官方文件解釋 BitCode 是一種程式中間碼,其實就是 LLVM IR 的一種編碼形式 - BitCodeFormart

bitcode-architecture

上圖表示了 IR 和 BitCode 在編譯器架構中所在的位置,需要說明的是 BitCode 是以 section 形式儲存在可執行檔案中。當我們把攜帶 BitCode 的 App 提交到 AppStore 後,蘋果會提取出可執行檔案中的 BitCode 段,然後針對不同的 CPU 架構編譯和連結成不同的可執行檔案變體(Variant),不同 CPU 架構的裝置會自動選擇合適的架構的變體進行下載。而在 BitCode 之前沒我們都是把所有需要的 CPU 架構集合打包成一個 Fat Binary,結果就是使用者最終下載的安裝包之中有很多冗餘的 CPU 架構支援程式碼。

從以上編譯器架構中我們也可以得出一個結論:開啟 BitCode 之後編譯器後端(Backend)的工作都由 Apple 接管了。所以假如以後蘋果推出了新的 CPU 架構或者以後 LLVM 推出了一系列優化,我們也不再需要為其釋出新的安裝包了。

2.3.1. BitCode 一致性要求

一致性要求意味著工程開啟 BitCode 之後必須要求所有打進 Bundle 的 Binary 都需要支援 BitCode,也就是說我們依賴的靜態庫和動態庫都是含有 BitCode 的,不然就會打包失敗。對於 Cocoapods 等原始碼管理工具來管理的依賴庫來說操作會比較簡單,我們只需要開啟 Pods 工程中的 BitCode 就行。但是對於一些三方的閉源庫,我們就無能為力了。

2.3.2. BitCode 的崩潰定位

開啟 BitCode 之後需要特別注意崩潰定位的問題:由於最終的可執行檔案是 Apple 自動生成的,同時產生新的符號表檔案,所以我們使用原本打包生成的 dSYM 符號化檔案是無法完成符號化的。所以我們需要在上傳至 App Store 時需要勾選 Include app symbols for your application to receive symboilcated crash logs from Apple:

Include app symbols

勾選之後 Apple 會給我們生成 dSYM,然後就可以在 Xcode -> Organizer 或者 iTunes Connect 中下載對應的 dSYM 來進行符號化了。

2.3.3. BitCode 的編譯選項優化

上面所說的編譯器優化是在編譯器前端完成的,所以提交的 BitCode 應該是經過優化的。但是 去除符號資訊,是在編譯生成可執行檔案之後完成的, 蘋果在生成可執行檔案之後是否給我們去除了符號也不得而知。

本文來自網易雲社群,經作者饒夢雲授權釋出

網易雲免費體驗館,0成本體驗20+款雲產品!

更多網易研發、產品、運營經驗分享請訪問網易雲社群