iOS 開發的最后一步就是進行 App 的打包和分發,這里分為兩個步驟:
-
Archive:對
Target
進行編譯、歸檔,生成.xcarchive
文件。 -
Export:對 .xcarchive 歸檔文件進一步處理,生成不同渠道的
.ipa
包,進行分發。
作為最終會在用戶手機上安裝的 ipa 包,一個重要的屬性就是它的占用體積,通過一些實踐,我們可以有效縮減最終安裝包的大小,節省下載流量,提高使用體驗,有利于產品的推廣。
下面就簡單介紹下 archive 文件、ipa 文件的組成和分析方法,以及一些常見的 App 包瘦身思路。
了解 .xcarchive 歸檔
當我們在 Xcode 菜單中選擇 Product -> Archive
后,編譯系統就會對當前的 Xcode 工程進行分析、編譯和打包,最終生成目標 Target
的一個 Archive(歸檔)
,我們可以在 Window -> Organizer -> Archives
頁面查看到所有緩存的歷史歸檔信息:
所謂的”歸檔“,就是對源碼進行編譯后,將此次編譯生成的各種文件、資源、記錄統一封裝到一個地方,方便進行管理和回溯。
右鍵選擇一個 archive,然后點擊 Show in Finder
,可以看到它在 Finder 中表示為一個 .xcarchive
后綴的文件。
這個 .xcarchive 文件包含了我們的應用和它的符號表信息(symbol information)以其它的相關的資源,右鍵選擇 顯示包內容
,我們可以查看一個 Archive 歸檔中具體的文件結構:
其中每個文件夾的含義:
BCSymbolMaps
Xcode 對 BitCode 符號表進行混淆(Symbol Hiding)后生成的對照表,和 dSYM 文件會一一對應。
dSYMs
存儲此次編譯的符號表(debug symbols),用來符號化解析崩潰堆棧。
Products
存儲此次編譯生成的的 App 包(.app)。
要注意的是這個包雖然包括了 App 運行需要的可執行文件以及其它資源,但是和最終用戶下載的版本會有所不同。后續的
export
操作會對其進行進一步處理。
SCMBlueprint
如果 Xcode 打開了版本管理(Preferences -> Source Control -> Enable Source Control),SCMBlueprint 文件夾會存儲此次編譯的版本控制信息,包括使用的 git 版本、倉庫、分支等。
如果未來想要回溯此次編譯的源碼版本,可以從這個 SCMBlueprint 中找到必要的信息。
SwiftSupport
如果你在 Target 的 Build Settings
中打開了 ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES
,此次編譯使用的 Swift 版本對應的標準庫文件(.dylib)會被放到這個文件夾中。
發布 App 時,這些標準庫也會被復制到 ipa bundle 中。
不過現在 Swift 的 ABI 已經穩定了,Xcode 10.2 及以后的版本打出來的包,在 iOS 12.2 及以后的系統的 app bundle 中不用再自帶鏈接庫了,節省了一定的體積。
了解 ipa 文件
.ipa(iOS App Store Package) 文件是最終被安裝到 iPhone 上的應用格式,包含了運行 App 所必需的的簽名、二進制包、資源等內容。
在 Organizer
中無論用什么方式 export
應用的安裝包,最終生成的都是一個 ipa 文件。
如果要查看 ipa 中的內容,我們可以簡單地把后綴名改為 .zip
然后解壓,也可以用命令行進行解壓:
zip -0 -y -r myAppName.ipa Payload/
觀察解壓以后的包,主要包含以下內容:
可執行文件
可執行文件是 ipa 的核心,占用體積也最大。
我們可以用 lipo
命令來查看可執行文件支持的指令集:
簽名文件
App 的簽名信息會被放到 _CodeSignature
文件夾中。
info.plist
存儲 App 主要信息的 plist 文件也會被一并打包到 ipa 中。
entitlements
entitlement 直譯成中文是“權益”、“權限”的意思。
當你在 Capabilities
中開啟一些特定的權限時,Xcode 會自動給你生成一個 .entitlements
文件,在這個文件中通過 xml 的格式將這些授權記錄下來。
常見的權限包括:
- iCloud 存儲
- Push notifications 推送通知
- Apple Pay 和 PassKit 蘋果支付
- App Group
除了在 CodeSign 階段被使用外,這個 entitlements 文件最終也會被打包到 ipa 中,在運行時供操作系統檢測 App 的授權情況是否合法。
App Plugins
如果你的 App 實現了應用擴展(App Extension),擴展的包會以 .appex
的后綴存儲在 PlugIns
文件夾中:
也就是說,App Extension 會跟隨主 App 一起被安裝到用戶手機上,當然卸載的時候也是會被一起卸載。
鏈接庫
App 運行所需要的各種鏈接庫會被放入 Frameworks
文件夾。
資源文件
App 運行需要的各種資源文件也是 ipa 體積的大頭,常見的有:
- 各種多媒體資源:圖片、音視頻
- xib 文件:
.nib
.storyboardc
- 各種打包的資源
.bundle
- 其它類型的資源:字體、數據庫、證書等等
App 瘦身
要對 App 安裝包體積進行壓縮,我們首先要知道安裝包占用的多少空間,這些空間由哪些部分組成,然后再進行針對性的優化。
查看最終用戶安裝包大小
實際上在 Xcode 本地 archive 出來的 app 包或者 export 出來的 ipa 包和最終用戶下載的版本會有所不同(通常體積會大很多)。因為蘋果可能會對 App 進行重新編譯(如果上傳了 BitCode),也會針對不同的設備型號、iOS 版本分發不同的資源(比如 2x、3x 的圖片),最后還會對整個 .ipa 進行壓縮,以減少從 App Store 下載時耗費的流量。
那么如何估算用戶最終下載版本的包體積大小呢?其實在 iTunes Connect 頁面我們可以直接查詢到。
打開 iTunes Connect,選擇 我的App
-> 活動
-> 所有構建版本
,然后選擇一個要查看的版本:
找到 App Store 文件大小
按鈕:
在彈出的列表中,可以看到在最新版本的 iOS 系統下,不同設備下載的包體積大小:
列表中的兩列:
- 下載大小:表示通過無線下載的壓縮 App 大小
- 安裝大小:安裝后此 App 將在用戶設備上占用的磁盤空間大小
分析 App 包 Size
為了更直觀地查看哪些資源占用了 App 安裝包的體積,我們可以借助一些文件工具來分析解壓后的 ipa 包,比如說 derlien
可以很直觀地看到各種不同類型文件所占的比例。
檢查未使用資源
隨著 App 的不斷迭代,我們往往會無意間引入很多用不到的資源,或者一些資源的引用已經從代碼中去除了,但是沒有及時從 bundle 中刪除,造成 App 包體積的浪費。
為了查找這些不再使用的資源,我們可以借助開源工具 LSUnusedResources 來檢測整個工程。
[圖片上傳失敗...(image-519b2e-1569495361068)]
針對一些特殊情況,比如代碼中使用例如
[UIImage imageNamed:[NSString stringWithFormat:@"icon_tag_%d", index]]
的方式引用資源,LSUnusedResources 也支持使用正則表達式來模糊匹配。
壓縮圖片
圖片文件是安裝包中最常見的資源了,常常會占有相當一部分比例,未壓縮的圖片體積往往相當大,通過一些工具壓縮圖片資源,節省空間:
- 無損壓縮:ImageOptim
- 有損壓縮:tinypng
使用 Asset Catalogs 存儲資源
相比于直接將圖片拖入工程目錄的方式,使用 Asset Catalogs
會更節省體積。Asset Catalogs 會用一個高度優化的特殊格式來存所有圖片,對 png 圖片也會進行最大化的壓縮。
Xcode 工程模板會自動生成一個 Assets.xcassets
文件,我們也可以按需創建另外的 .xcassets
,最終在 ipa 包中,這些 xcassets 都會被壓縮到 Assets.car 文件中,一定程度上也保證了安全性。
除了圖片資源外,Asset Catalogs 也可以存儲文本、Data 甚至 AR、apple TV 相關的資源,非常全能,所以比較好的實踐就是:
能用 Asset Catalogs 管理的資源,盡量使用 Asset Catalogs 來管理
分析 LinkMap 文件
上面提到,App 包占用空間中很大一部分比例是最終編譯生成的可執行文件(MACH-O),可執行文件的大小不僅和代碼體積有關,也受編譯器版本、編譯選項、鏈接庫、目標架構等影響。
我們可以通過分析編譯時產生的 LinkMap
來了解 MACH-O 文件的組成部分。
要找到對應的 LinkMap,首先在 Xcode Target -> Build Settings -> Write Link Map File
設置為 YES,然后在 Target -> Build Settings -> Path to Link Map File
選項中設置好 LinkMap 的生成地址(一般用 build 文件夾中的默認地址就好了),archive 成功后,我們就可以在對應地址找到該次編譯的 LinkMap 了:
LinkMap 記錄了編譯時的鏈接信息,用來描述可執行文件的構造成分,包括代碼段 __TEXT
和數據段 __DATA
的分布情況:
網上有很多腳本可以對 LinkMap 進行分析統計,比如:
獲取到分析結果后,我們可以精確了解各個模塊、鏈接庫、方法在可執行文件中的位置和占用空間:
對于一些占比特別大的模塊,常見的優化思路有:
- 尋找可替代的,小體積的依賴庫,或者自己實現
- 去掉靜態庫中不需要的指令集,比如 armv7s,x86等,只保留發布需要的 armv7,arm64
- 提高代碼重用性
- 進一步分析代碼中沒有被使用的方法、模塊,對代碼庫進行精簡。
- 砍需求
使用 bitcode
bitcode 是在 LLVM 體系中介于前端語言(OC、Swift、C)和后端語言(X86、ARM的機器碼)之間的中間語言。
一次完整的編譯(從源碼到.O目標文件)包含三個主要步驟:
- 前端(Frontend):負責把各種類型的源代碼編譯為 bitcode 中間表示。
- 優化(Optimizer):負責對 bitcode 進行各種類型的優化,將 bitcode 代碼進行一些邏輯等價的轉換,使得代碼的執行效率更高,體積更小。
- 后端(Backend):也叫 CodeGenerator,負責把優化后的 bitcode 編譯為指定目標架構的機器碼,比如 x86、arm64 等等。
我們可以在 Xcode Target -> Build Settings -> Enable Bitcode
中打開 bitcode 選項,這樣在 archive 時,會將中間生成的 bitcode 嵌入到鏈接后的二進制文件(.o)中,用于提交到 App Store。
上面提到,bitcode 作為 LLVM 的中間語言,是可以從它直接編譯出最終程序的,Apple 拿到我們上傳的 bitcode 后,會使用最新的技術、編譯器針對不同的終端設備重新編譯 App,而這些重新編譯的版本往往比我們本地 Xcode 編譯的版本體積更小、效率更高。
如果后續需要支持新的平臺或者有新的編譯技術革新,蘋果就不用依賴開發者重新上傳了,直接使用現成的 bitcode 編譯出船新的版本.
值得注意的是:在打包時,如果一些三方的依賴庫沒有開啟 bitcode,或者開啟了但是沒有在最終引用的鏈接庫中帶有 bitcode,那么整個工程就無法用 bitcode 來編譯了。
按需加載資源(On-Demand Resources)
iOS9 以后,蘋果提供了 On-Demand Resources
功能來減少安裝包的體積。我們可以將一些資源標記為 “按需加載”,在需要使用的時候請求操作系統從 App Store 中下載。這個功能非常適合一些大型游戲、帶有付費內容或者大量不常使用的多媒體資源的 App。
[圖片上傳失敗...(image-8b63f0-1569495361068)]
當然,按需加載只是針對 App 使用的資源文件,不包括二進制可執行文件或者源碼。
On-Demand Resources 的配置可以很輕松地在 Xcode 中完成。
首先在 Target -> Resource Tags
中創建資源 tag,一個 tag 表示一組可以被獨立下載的資源,后面我們就會使用這個 tag 在程序中請求操作系統下載對應的資源包到本地。
不同的 tag 包含的資源是可以重復的,App Store 會自己 differ,不會重復下載。
然后找到想要按需加載的資源文件,為它們分配一個或多個之前創建的 tag。
最后在代碼中,我們可以使用 NSBundleResourceRequest:
- 請求下載 on-demand 資源
- 將資源標記為已使用狀態(這樣下載的資源會被清理掉,節省本地空間)
- 管理資源下載過程,配置優先級、追蹤下載進度等等
- 檢測磁盤容量警告
下面的代碼是一個簡單的資源下載請求:
// 配置要下載的 tags
NSSet *tags = [NSSet setWithObjects: @"birds", @"bridge", @"city"];
// 創建 NSBundleResourceRequest 對象
resourceRequest = [[NSBundleResourceRequest alloc] initWithTags:tags];
// 請求資源,處理回調
[resourceRequest beginAccessingResourcesWithCompletionHandler: ^(NSError * __nullable error) {
if (error) {
// 處理錯誤
self.resourcesLoaded = NO;
return;
}
// 下載成功,可以直接使用這些資源了
self.resourcesAvailable = YES;
}
];
下圖總結了一個 on-demand 資源的生命周期:
總結
最近蘋果取消了移動網絡下載 150M 的限制,說明隨著手機容量的增加和移動網絡的普及,大家對 App 安裝包體積不再那么敏感了,只要我們遵循一些最佳實踐,一般不會在這一塊有太大的問題。