iOS開發(fā):Archive、ipa 和 App 包瘦身

iOS 開發(fā)的最后一步就是進(jìn)行 App 的打包和分發(fā),這里分為兩個(gè)步驟:

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

作為最終會(huì)在用戶手機(jī)上安裝的 ipa 包,一個(gè)重要的屬性就是它的占用體積,通過一些實(shí)踐,我們可以有效縮減最終安裝包的大小,節(jié)省下載流量,提高使用體驗(yàn),有利于產(chǎn)品的推廣。

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

了解 .xcarchive 歸檔

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

Archives

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

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

這個(gè) .xcarchive 文件包含了我們的應(yīng)用和它的符號(hào)表信息(symbol information)以其它的相關(guān)的資源,右鍵選擇 顯示包內(nèi)容,我們可以查看一個(gè) Archive 歸檔中具體的文件結(jié)構(gòu):

.xcarchive 文件

其中每個(gè)文件夾的含義:

BCSymbolMaps

Xcode 對 BitCode 符號(hào)表進(jìn)行混淆(Symbol Hiding)后生成的對照表,和 dSYM 文件會(huì)一一對應(yīng)。

dSYMs

存儲(chǔ)此次編譯的符號(hào)表(debug symbols),用來符號(hào)化解析崩潰堆棧。

Products

存儲(chǔ)此次編譯生成的的 App 包(.app)。

要注意的是這個(gè)包雖然包括了 App 運(yùn)行需要的可執(zhí)行文件以及其它資源,但是和最終用戶下載的版本會(huì)有所不同。后續(xù)的 export 操作會(huì)對其進(jìn)行進(jìn)一步處理。

SCMBlueprint

如果 Xcode 打開了版本管理(Preferences -> Source Control -> Enable Source Control),SCMBlueprint 文件夾會(huì)存儲(chǔ)此次編譯的版本控制信息,包括使用的 git 版本、倉庫、分支等。

如果未來想要回溯此次編譯的源碼版本,可以從這個(gè) SCMBlueprint 中找到必要的信息。

SwiftSupport

如果你在 Target 的 Build Settings 中打開了 ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES,此次編譯使用的 Swift 版本對應(yīng)的標(biāo)準(zhǔn)庫文件(.dylib)會(huì)被放到這個(gè)文件夾中。

發(fā)布 App 時(shí),這些標(biāo)準(zhǔn)庫也會(huì)被復(fù)制到 ipa bundle 中。

不過現(xiàn)在 Swift 的 ABI 已經(jīng)穩(wěn)定了,Xcode 10.2 及以后的版本打出來的包,在 iOS 12.2 及以后的系統(tǒng)的 app bundle 中不用再自帶鏈接庫了,節(jié)省了一定的體積。

了解 ipa 文件

.ipa(iOS App Store Package) 文件是最終被安裝到 iPhone 上的應(yīng)用格式,包含了運(yùn)行 App 所必需的的簽名、二進(jìn)制包、資源等內(nèi)容。

Organizer 中無論用什么方式 export 應(yīng)用的安裝包,最終生成的都是一個(gè) ipa 文件。

如果要查看 ipa 中的內(nèi)容,我們可以簡單地把后綴名改為 .zip 然后解壓,也可以用命令行進(jìn)行解壓:

zip -0 -y -r myAppName.ipa Payload/

觀察解壓以后的包,主要包含以下內(nèi)容:

可執(zhí)行文件

可執(zhí)行文件是 ipa 的核心,占用體積也最大。

可執(zhí)行文件

我們可以用 lipo 命令來查看可執(zhí)行文件支持的指令集:

查看可執(zhí)行文件的指令集

簽名文件

App 的簽名信息會(huì)被放到 _CodeSignature 文件夾中。

info.plist

存儲(chǔ) App 主要信息的 plist 文件也會(huì)被一并打包到 ipa 中。

entitlements

entitlement 直譯成中文是“權(quán)益”、“權(quán)限”的意思。

當(dāng)你在 Capabilities 中開啟一些特定的權(quán)限時(shí),Xcode 會(huì)自動(dòng)給你生成一個(gè) .entitlements 文件,在這個(gè)文件中通過 xml 的格式將這些授權(quán)記錄下來。

常見的權(quán)限包括:

  • iCloud 存儲(chǔ)
  • Push notifications 推送通知
  • Apple Pay 和 PassKit 蘋果支付
  • App Group

除了在 CodeSign 階段被使用外,這個(gè) entitlements 文件最終也會(huì)被打包到 ipa 中,在運(yùn)行時(shí)供操作系統(tǒng)檢測 App 的授權(quán)情況是否合法。

App Plugins

如果你的 App 實(shí)現(xiàn)了應(yīng)用擴(kuò)展(App Extension),擴(kuò)展的包會(huì)以 .appex 的后綴存儲(chǔ)在 PlugIns 文件夾中:

應(yīng)用擴(kuò)展

也就是說,App Extension 會(huì)跟隨主 App 一起被安裝到用戶手機(jī)上,當(dāng)然卸載的時(shí)候也是會(huì)被一起卸載。

鏈接庫

App 運(yùn)行所需要的各種鏈接庫會(huì)被放入 Frameworks 文件夾。

資源文件

App 運(yùn)行需要的各種資源文件也是 ipa 體積的大頭,常見的有:

  • 各種多媒體資源:圖片、音視頻
  • xib 文件:.nib .storyboardc
  • 各種打包的資源 .bundle
  • 其它類型的資源:字體、數(shù)據(jù)庫、證書等等

App 瘦身

要對 App 安裝包體積進(jìn)行壓縮,我們首先要知道安裝包占用的多少空間,這些空間由哪些部分組成,然后再進(jìn)行針對性的優(yōu)化。

查看最終用戶安裝包大小

實(shí)際上在 Xcode 本地 archive 出來的 app 包或者 export 出來的 ipa 包和最終用戶下載的版本會(huì)有所不同(通常體積會(huì)大很多)。因?yàn)樘O果可能會(huì)對 App 進(jìn)行重新編譯(如果上傳了 BitCode),也會(huì)針對不同的設(shè)備型號(hào)、iOS 版本分發(fā)不同的資源(比如 2x、3x 的圖片),最后還會(huì)對整個(gè) .ipa 進(jìn)行壓縮,以減少從 App Store 下載時(shí)耗費(fèi)的流量。

那么如何估算用戶最終下載版本的包體積大小呢?其實(shí)在 iTunes Connect 頁面我們可以直接查詢到。

打開 iTunes Connect,選擇 我的App -> 活動(dòng) -> 所有構(gòu)建版本,然后選擇一個(gè)要查看的版本:

選擇構(gòu)建版本

找到 App Store 文件大小 按鈕:

App Store 文件大小

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

查詢下載大小

列表中的兩列:

  • 下載大?。罕硎就ㄟ^無線下載的壓縮 App 大小
  • 安裝大?。喊惭b后此 App 將在用戶設(shè)備上占用的磁盤空間大小

分析 App 包 Size

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

derlien

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

檢查未使用資源

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

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

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

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

壓縮圖片

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

使用 Asset Catalogs 存儲(chǔ)資源

相比于直接將圖片拖入工程目錄的方式,使用 Asset Catalogs 會(huì)更節(jié)省體積。Asset Catalogs 會(huì)用一個(gè)高度優(yōu)化的特殊格式來存所有圖片,對 png 圖片也會(huì)進(jìn)行最大化的壓縮。

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

ipa 中的 Assets.car

除了圖片資源外,Asset Catalogs 也可以存儲(chǔ)文本、Data 甚至 AR、apple TV 相關(guān)的資源,非常全能,所以比較好的實(shí)踐就是:

能用 Asset Catalogs 管理的資源,盡量使用 Asset Catalogs 來管理

分析 LinkMap 文件

上面提到,App 包占用空間中很大一部分比例是最終編譯生成的可執(zhí)行文件(MACH-O),可執(zhí)行文件的大小不僅和代碼體積有關(guān),也受編譯器版本、編譯選項(xiàng)、鏈接庫、目標(biāo)架構(gòu)等影響。

我們可以通過分析編譯時(shí)產(chǎn)生的 LinkMap 來了解 MACH-O 文件的組成部分。

要找到對應(yīng)的 LinkMap,首先在 Xcode Target -> Build Settings -> Write Link Map File 設(shè)置為 YES,然后在 Target -> Build Settings -> Path to Link Map File 選項(xiàng)中設(shè)置好 LinkMap 的生成地址(一般用 build 文件夾中的默認(rèn)地址就好了),archive 成功后,我們就可以在對應(yīng)地址找到該次編譯的 LinkMap 了:

生成的LinkMap文件

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

LinkMap

網(wǎng)上有很多腳本可以對 LinkMap 進(jìn)行分析統(tǒng)計(jì),比如:

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

Link Map 分析結(jié)果

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

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

使用 bitcode

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

bitcode

一次完整的編譯(從源碼到.O目標(biāo)文件)包含三個(gè)主要步驟:

  • 前端(Frontend):負(fù)責(zé)把各種類型的源代碼編譯為 bitcode 中間表示。
  • 優(yōu)化(Optimizer):負(fù)責(zé)對 bitcode 進(jìn)行各種類型的優(yōu)化,將 bitcode 代碼進(jìn)行一些邏輯等價(jià)的轉(zhuǎn)換,使得代碼的執(zhí)行效率更高,體積更小。
  • 后端(Backend):也叫 CodeGenerator,負(fù)責(zé)把優(yōu)化后的 bitcode 編譯為指定目標(biāo)架構(gòu)的機(jī)器碼,比如 x86、arm64 等等。

我們可以在 Xcode Target -> Build Settings -> Enable Bitcode 中打開 bitcode 選項(xiàng),這樣在 archive 時(shí),會(huì)將中間生成的 bitcode 嵌入到鏈接后的二進(jìn)制文件(.o)中,用于提交到 App Store。

上面提到,bitcode 作為 LLVM 的中間語言,是可以從它直接編譯出最終程序的,Apple 拿到我們上傳的 bitcode 后,會(huì)使用最新的技術(shù)、編譯器針對不同的終端設(shè)備重新編譯 App,而這些重新編譯的版本往往比我們本地 Xcode 編譯的版本體積更小、效率更高。

如果后續(xù)需要支持新的平臺(tái)或者有新的編譯技術(shù)革新,蘋果就不用依賴開發(fā)者重新上傳了,直接使用現(xiàn)成的 bitcode 編譯出船新的版本.

值得注意的是:在打包時(shí),如果一些三方的依賴庫沒有開啟 bitcode,或者開啟了但是沒有在最終引用的鏈接庫中帶有 bitcode,那么整個(gè)工程就無法用 bitcode 來編譯了。

按需加載資源(On-Demand Resources)

iOS9 以后,蘋果提供了 On-Demand Resources 功能來減少安裝包的體積。我們可以將一些資源標(biāo)記為 “按需加載”,在需要使用的時(shí)候請求操作系統(tǒng)從 App Store 中下載。這個(gè)功能非常適合一些大型游戲、帶有付費(fèi)內(nèi)容或者大量不常使用的多媒體資源的 App。

[圖片上傳失敗...(image-8b63f0-1569495361068)]

當(dāng)然,按需加載只是針對 App 使用的資源文件,不包括二進(jìn)制可執(zhí)行文件或者源碼。

On-Demand Resources 的配置可以很輕松地在 Xcode 中完成。

首先在 Target -> Resource Tags 中創(chuàng)建資源 tag,一個(gè) tag 表示一組可以被獨(dú)立下載的資源,后面我們就會(huì)使用這個(gè) tag 在程序中請求操作系統(tǒng)下載對應(yīng)的資源包到本地。

在 Resource Tags 管理資源 tags

不同的 tag 包含的資源是可以重復(fù)的,App Store 會(huì)自己 differ,不會(huì)重復(fù)下載。

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

為資源分配 tag

最后在代碼中,我們可以使用 NSBundleResourceRequest

  • 請求下載 on-demand 資源
  • 將資源標(biāo)記為已使用狀態(tài)(這樣下載的資源會(huì)被清理掉,節(jié)省本地空間)
  • 管理資源下載過程,配置優(yōu)先級(jí)、追蹤下載進(jìn)度等等
  • 檢測磁盤容量警告

下面的代碼是一個(gè)簡單的資源下載請求:

// 配置要下載的 tags
NSSet *tags = [NSSet setWithObjects: @"birds", @"bridge", @"city"];
 
// 創(chuàng)建 NSBundleResourceRequest 對象
resourceRequest = [[NSBundleResourceRequest alloc] initWithTags:tags];

// 請求資源,處理回調(diào)
[resourceRequest beginAccessingResourcesWithCompletionHandler: ^(NSError * __nullable error) {

    if (error) {

        // 處理錯(cuò)誤
        self.resourcesLoaded = NO;
        return;
    }

    // 下載成功,可以直接使用這些資源了
    self.resourcesAvailable = YES;
    }
];

下圖總結(jié)了一個(gè) on-demand 資源的生命周期:

On-demand resource life cycle

總結(jié)

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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容