iOS開發:Archive、ipa 和 App 包瘦身

iOS 開發的最后一步就是進行 App 的打包和分發,這里分為兩個步驟:

  1. Archive:對 Target 進行編譯、歸檔,生成 .xcarchive 文件。
  2. Export:對 .xcarchive 歸檔文件進一步處理,生成不同渠道的 .ipa 包,進行分發。

作為最終會在用戶手機上安裝的 ipa 包,一個重要的屬性就是它的占用體積,通過一些實踐,我們可以有效縮減最終安裝包的大小,節省下載流量,提高使用體驗,有利于產品的推廣。

下面就簡單介紹下 archive 文件、ipa 文件的組成和分析方法,以及一些常見的 App 包瘦身思路。

了解 .xcarchive 歸檔

當我們在 Xcode 菜單中選擇 Product -> Archive 后,編譯系統就會對當前的 Xcode 工程進行分析、編譯和打包,最終生成目標 Target 的一個 Archive(歸檔),我們可以在 Window -> Organizer -> Archives 頁面查看到所有緩存的歷史歸檔信息:

Archives

所謂的”歸檔“,就是對源碼進行編譯后,將此次編譯生成的各種文件、資源、記錄統一封裝到一個地方,方便進行管理和回溯。

右鍵選擇一個 archive,然后點擊 Show in Finder,可以看到它在 Finder 中表示為一個 .xcarchive 后綴的文件。

這個 .xcarchive 文件包含了我們的應用和它的符號表信息(symbol information)以其它的相關的資源,右鍵選擇 顯示包內容,我們可以查看一個 Archive 歸檔中具體的文件結構:

.xcarchive 文件

其中每個文件夾的含義:

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 文件大小 按鈕:

App Store 文件大小

在彈出的列表中,可以看到在最新版本的 iOS 系統下,不同設備下載的包體積大小:

查詢下載大小

列表中的兩列:

  • 下載大小:表示通過無線下載的壓縮 App 大小
  • 安裝大小:安裝后此 App 將在用戶設備上占用的磁盤空間大小

分析 App 包 Size

為了更直觀地查看哪些資源占用了 App 安裝包的體積,我們可以借助一些文件工具來分析解壓后的 ipa 包,比如說 derlien

derlien

可以很直觀地看到各種不同類型文件所占的比例。

檢查未使用資源

隨著 App 的不斷迭代,我們往往會無意間引入很多用不到的資源,或者一些資源的引用已經從代碼中去除了,但是沒有及時從 bundle 中刪除,造成 App 包體積的浪費。

為了查找這些不再使用的資源,我們可以借助開源工具 LSUnusedResources 來檢測整個工程。

[圖片上傳失敗...(image-519b2e-1569495361068)]

針對一些特殊情況,比如代碼中使用例如 [UIImage imageNamed:[NSString stringWithFormat:@"icon_tag_%d", index]] 的方式引用資源,LSUnusedResources 也支持使用正則表達式來模糊匹配。

壓縮圖片

圖片文件是安裝包中最常見的資源了,常常會占有相當一部分比例,未壓縮的圖片體積往往相當大,通過一些工具壓縮圖片資源,節省空間:

使用 Asset Catalogs 存儲資源

相比于直接將圖片拖入工程目錄的方式,使用 Asset Catalogs 會更節省體積。Asset Catalogs 會用一個高度優化的特殊格式來存所有圖片,對 png 圖片也會進行最大化的壓縮。

Xcode 工程模板會自動生成一個 Assets.xcassets 文件,我們也可以按需創建另外的 .xcassets,最終在 ipa 包中,這些 xcassets 都會被壓縮到 Assets.car 文件中,一定程度上也保證了安全性。

ipa 中的 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文件

LinkMap 記錄了編譯時的鏈接信息,用來描述可執行文件的構造成分,包括代碼段 __TEXT 和數據段 __DATA 的分布情況:

LinkMap

網上有很多腳本可以對 LinkMap 進行分析統計,比如:

獲取到分析結果后,我們可以精確了解各個模塊、鏈接庫、方法在可執行文件中的位置和占用空間:

Link Map 分析結果

對于一些占比特別大的模塊,常見的優化思路有:

  • 尋找可替代的,小體積的依賴庫,或者自己實現
  • 去掉靜態庫中不需要的指令集,比如 armv7s,x86等,只保留發布需要的 armv7,arm64
  • 提高代碼重用性
  • 進一步分析代碼中沒有被使用的方法、模塊,對代碼庫進行精簡。
  • 砍需求

使用 bitcode

bitcode 是在 LLVM 體系中介于前端語言(OC、Swift、C)和后端語言(X86、ARM的機器碼)之間的中間語言。

bitcode

一次完整的編譯(從源碼到.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 在程序中請求操作系統下載對應的資源包到本地。

在 Resource Tags 管理資源 tags

不同的 tag 包含的資源是可以重復的,App Store 會自己 differ,不會重復下載。

然后找到想要按需加載的資源文件,為它們分配一個或多個之前創建的 tag。

為資源分配 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 資源的生命周期:

On-demand resource life cycle

總結

最近蘋果取消了移動網絡下載 150M 的限制,說明隨著手機容量的增加和移動網絡的普及,大家對 App 安裝包體積不再那么敏感了,只要我們遵循一些最佳實踐,一般不會在這一塊有太大的問題。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,406評論 6 538
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,034評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,413評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,449評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,165評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,559評論 1 325
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,606評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,781評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,327評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,084評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,278評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,849評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,495評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,927評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,172評論 1 291
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,010評論 3 396
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,241評論 2 375

推薦閱讀更多精彩內容