iOS 安裝包瘦身
一. 安裝包組成
談到 App 瘦身,最直接的想法莫過于分析一個安裝包內(nèi)部結(jié)構(gòu)淮捆,了解其每一部分的來源砚哆。解壓一個ipa
包,拿到其payload
中app
文件的數(shù)據(jù)混狠,整理歸類后其大致如下:
- Exectutable: 可執(zhí)行文件
- Resources:資源文件
- 圖片資源:
Assets.car/bundle/png/jpg
等 - 視頻/音頻資源:
mp4/mp3
等 - 靜態(tài)網(wǎng)頁資源:
html/css/js
等 - 視圖資源:
xib/storyboard
等 - 其他:文本/字體/證書 等
- 圖片資源:
- Framework:項目中使用的動態(tài)庫
-
SwiftSupport: libSwiftxxx
等一系列 Swift 庫 - 其他依賴庫:
Embeded Framework
-
- Pulgins:Application Extensions
- appex:其組成大致與 ipa 包組成一致
從以上結(jié)構(gòu)中可以看出一個 ipa 大致由 Executable
, Resources
, Framework
,Plugins
四大模塊組成岸霹,接下來我們就從這四個方向來探討 App 瘦身的具體方案。
二:可執(zhí)行文件瘦身
可執(zhí)行文件就是我們源代碼(.m/.h/.swift ...)
的編譯結(jié)果将饺。在 iOS 或者 macOS 中稱之為 Mach-O executable贡避,它是程序的入口。
2.1: 編譯器優(yōu)化
Xcode 支持編譯器層面的一些優(yōu)化優(yōu)化選項予弧,可以讓我們介于更快的編譯速度和更小的二進制大小并且更快的執(zhí)行速度之間自由選擇想要進行的優(yōu)化粒度刮吧。
2.1.1. Clang/LLVM
編譯器優(yōu)化選項
我們都知道 Xcode 是使用 Clang來編譯 Objective-C 語言的,Clang 的優(yōu)化選項在其文檔 clang - Code Generation Options 中可以查閱得到掖蛤。我們的 IDE-Xcode 只提供給我們 6 個等級的編譯選項杀捻,在 Xcode -> Build Setting -> Apple LLVM 9.0 - Code Generation -> Optimization Level
中進行設(shè)置,每個等級的說明蚓庭,可以參考官方文檔:
- None[-O0]: 編譯器不會優(yōu)化代碼致讥,意味著更快的編譯速度和更多的調(diào)試信息仅仆,默認在 Debug 模式下開啟。
- Fast[-O,O1]: 編譯器會優(yōu)化代碼性能并且最小限度影響編譯時間拄踪,此選項在編譯時會占用更多的內(nèi)存蝇恶。
- Faster[-O2]:編譯器會開啟不依賴空間/時間折衷所有優(yōu)化選項拳魁。在此惶桐,編譯器不會展開循環(huán)或者函數(shù)內(nèi)聯(lián)。此選項會增加編譯時間并且提高代碼執(zhí)行效率潘懊。
- Fastest[-O3]:編譯器會開啟所有的優(yōu)化選項來提升代碼執(zhí)行效率姚糊。此模式編譯器會執(zhí)行函數(shù)內(nèi)聯(lián)使得生成的可執(zhí)行文件會變得更大。一般不推薦使用此模式授舟。
- Fastest Smallest[-Os]:編譯器會開啟除了會明顯增加包大小以外的所有優(yōu)化選項救恨。默認在 Release 模式下開啟。
- Fastest, Aggressive Optimization[-Ofast]:啟動 -O3 中的所有優(yōu)化释树,可能會開啟一些違反語言標準的一些優(yōu)化選項肠槽。一般不推薦使用此模式。
Fastest Smallest[-Os]
極小限度會影響到包大小奢啥,而且也保證了代碼的執(zhí)行效率秸仙,是最佳的發(fā)布選項,一般 Xcode 會在 Release 下默認選擇 Fastest Smallest[-Os] 選項桩盲,較老的項目可能沒有自動勾選寂纪。
XCode 中設(shè)置的選項最終會反應在 Clang 命令上面,打開 build log
可以看到此選項最終的表現(xiàn)形式:
如果你還需要 clang 的其他選項來編譯你的項目赌结,可以在 Other C Flag 中直接添加其參數(shù)捞蛋。舉例來說,在 Optimization Level 中設(shè)置 Fastest Smallest[-Os] 和在 Other C Flags 中添加 -Os 效果是一樣的柬姚。
2.1.2. Swift Complier/LLVM
編譯優(yōu)化選項
Swift 語言的編譯器是 swiftlang
拟杉,同時也是基于 LLVM
后端的析既。Xcode 9.3 版本之后 Swift 編譯器會提供新的選項來幫助減少 Swift 可執(zhí)行文件的大斜液荨:
- No optimization[-Onone]:不進行優(yōu)化鹉胖,能保證較快的編譯速度融涣。
- Optimize for Speed[-O]:編譯器將會對代碼的執(zhí)行效率進行優(yōu)化雄家,一定程度上會增加包大小瞎饲。
- Optimize for Size[-Osize]:編譯器會盡可能減少包的大小并且最小限度影響代碼的執(zhí)行效率输吏。
Xcode 9.3
以前和優(yōu)化選項混雜在一起的編譯模式可以獨立設(shè)置了:
Single File:單個文件優(yōu)化担孔,可以減少增量編譯的時間卦洽,并且可以充分利用多核 CPU贞言,并行優(yōu)化多個文件,提高編譯速度阀蒂。但是對于交叉引用無能為力该窗。
Whole Module:模塊優(yōu)化弟蚀,最大限度優(yōu)化整個模塊,能處理交叉引用酗失。缺點不能利用多核 CPU 的優(yōu)勢义钉,每次編譯都會重新編譯整個 Module。
在 Relese 模式下 -Osize
和 Whole Module
同時開啟效果會發(fā)揮的最好规肴,從現(xiàn)有的案例中可以看到它會減少 5%~30%
的可執(zhí)行文件大小捶闸,并且對性能的影響也微乎其微(大約 5%)。參考官方文檔 和 SwiftCafe拖刃。
此選項雖然是 Xcode 9.3
支持的删壮,但是我們發(fā)現(xiàn) Xcode 9.2 對應的 Swift Compiler
也是支持 Osize
的。所以 Xcode 9.2
版本中可以在 Build Settings -> Other Swift Flags
中添加 -Osize
提前獲取編譯器優(yōu)化的好處兑牡。
雖然 Xcode 9.3/Swift4.1 已經(jīng)發(fā)布央碟,但是其編譯器不是特別穩(wěn)定,特別是開啟 Osize 選項之后均函,編譯器很多情況下會莫名其妙的崩潰(Segmentation fault)亿虽,我們在 bugs.swift.org 上面也找到了很多同類的崩潰。所以假如你使開啟 Osize 之后遇到了同類的崩潰苞也,你可以選擇放棄 Osize洛勉,或者想辦法修改代碼繞開編譯器崩潰。
2.2: 去除符號信息
可執(zhí)行文件中的符號)是指程序中的所有的變量墩朦、類坯认、函數(shù)、枚舉氓涣、變量和地址映射關(guān)系牛哺,以及一些在調(diào)試的時候使用到的用于定位代碼在源碼中的位置的調(diào)試符號,符號和斷點定位以及堆棧符號化有很重要的關(guān)系劳吠。
2.2.1. Strip Style
Strip Style
表示的是我們需要去除的符號的類型的選項引润,其分為三個選擇項:
- All Symbols: 去除所有符號,一般是在主工程中開啟痒玩。
- Non-Global Symbols: 去除一些非全局的 Symbol(保留全局符號淳附,Debug Symbols 同樣會被去除),鏈接時會被重定向的那些符號不會被去除蠢古,此選項是靜態(tài)庫/動態(tài)庫的建議選項奴曙。
- Debug Symbols: 去除調(diào)試符號,去除之后將無法斷點調(diào)試草讶。
iOS 的調(diào)試符號是 DWARF
格式的洽糟,相關(guān)概念如下:
- Mach-O: 可執(zhí)行文件,源文件編譯鏈接的結(jié)果。包含映射調(diào)試信息(對象文件)具體存儲位置的
Debug Map
- DWARF:一種通用的調(diào)試文件格式坤溃,支持源碼級別的調(diào)試拍霜,調(diào)試信息存在于 對象文件 中,一般都比較大薪介。Xcode 調(diào)試模式下一般都是使用 DWARF 來進行符號化的祠饺。
- dSYM:獨立的符號表文件,主要用來做發(fā)布產(chǎn)品的崩潰符號化汁政。dSYM 是一個壓縮包道偷,里面包含了 DWARF 文件。
使用 Xcode 編譯打包的時候會先通過可執(zhí)行文件的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 調(diào)試信息之后 Xcode 是靠什么來生成 dSYM 的,答案其實還是 DWARF距贷,因為 Xcode 編譯實際的操作步驟是:生成帶有 DWARF 調(diào)試信息的可執(zhí)行文件 -> 提取可執(zhí)行文件中的調(diào)試信息打包成 dSYM -> 去除符號化信息
柄冲。去除符號是單獨的步驟,使用的是 strip
命令忠蝗。
另外一個問題是现横,去除符號化信息之后我們只能使用 dSYM 來進行符號化,那我們使用 Xcode 來進行調(diào)試的時候會不會太麻煩了阁最?其實我們完全不用擔心這個問題:Strip Linked Product
選項在 Deployment Postprocessing
設(shè)置為 YES 的時候才生效戒祠,而在 Archive
的時候 Xcode
總是會把 Deployment Postprocessing
設(shè)置為 YES
。所以我們可以打開 Strip Linked Product
并且把 Deployment Postprocessing
設(shè)置為 NO
速种,而不用擔心調(diào)試的時候會影響斷點和符號化姜盈,同時打包的時候又會自動去除符號信息。這個選項也是默認打開的配阵,較老的項目可以選擇手動開啟馏颂。
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
模式下開啟亿絮,不然就不能對三方庫進行斷點調(diào)試和符號化了。
如果依賴的 Target
是獨立簽名的(比如 App Extension
),strip 操作就會失效壹无,并伴隨著Warning:warning: skipping copy phase strip, binary is code signed: xxxx
葱绒。此情況將依賴的 Target 中的 Strip Linked Product
修改為 YES
,保證依賴的 Target
是已經(jīng)去除了符號即可斗锭,Waning 忽略掉就可以了地淀。
Cocoapods 管理的動態(tài)庫(use_framework!)的情況就相對要特殊一點,因為 Cocoapods 中的的動態(tài)庫是使用自己實現(xiàn)的腳本 Pods-xxx-frameworks.sh 來實現(xiàn)拷貝的岖是,所以并不會走 Xcode 的流程帮毁,當然也就不受 Strip Debug Symbols During Copy 的影響。當然 Cocoapods 是源碼管理的豺撑,所以只需要將源碼 Target 中的 Strip Linked Product 設(shè)置為 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
穩(wěn)定之前爷肝,Swift 標準庫是會打進目標文件的,想要同時移除 Swift 標準庫里面的符號的話需要在發(fā)布選項中勾選 Strip Swift symbols陆错,如下圖所示:
####### 2.3. BitCode
BitCode
是 iOS 9 引入的新特性灯抛,官方文檔解釋 BitCode
是一種程序中間碼,其實就是 LLVM IR
的一種編碼形式 - BitCodeFormart音瓷。
上圖表示了 IR 和 BitCode 在編譯器架構(gòu)中所在的位置对嚼,需要說明的是 BitCode 是以 section 形式保存在可執(zhí)行文件中。當我們把攜帶 BitCode 的 App 提交到 AppStore 后绳慎,蘋果會提取出可執(zhí)行文件中的 BitCode 段纵竖,然后針對不同的 CPU 架構(gòu)編譯和鏈接成不同的可執(zhí)行文件變體(Variant),不同 CPU 架構(gòu)的設(shè)備會自動選擇合適的架構(gòu)的變體進行下載杏愤。而在 BitCode 之前沒我們都是把所有需要的 CPU 架構(gòu)集合打包成一個 Fat Binary靡砌,結(jié)果就是用戶最終下載的安裝包之中有很多冗余的 CPU 架構(gòu)支持代碼。
從以上編譯器架構(gòu)中我們也可以得出一個結(jié)論:開啟 BitCode 之后編譯器后端(Backend)的工作都由 Apple 接管了声邦。所以假如以后蘋果推出了新的 CPU 架構(gòu)或者以后 LLVM 推出了一系列優(yōu)化乏奥,我們也不再需要為其發(fā)布新的安裝包了。
2.3.1. BitCode 一致性要求
一致性要求意味著工程開啟 BitCode 之后必須要求所有打進 Bundle 的 Binary 都需要支持 BitCode亥曹,也就是說我們依賴的靜態(tài)庫和動態(tài)庫都是含有 BitCode 的邓了,不然就會打包失敗。對于 Cocoapods 等源碼管理工具來管理的依賴庫來說操作會比較簡單媳瞪,我們只需要開啟 Pods 工程中的 BitCode 就行骗炉。但是對于一些三方的閉源庫,我們就無能為力了蛇受。
2.3.2. BitCode 的崩潰定位
開啟 BitCode 之后需要特別注意崩潰定位的問題:由于最終的可執(zhí)行文件是 Apple 自動生成的句葵,同時產(chǎn)生新的符號表文件,所以我們使用原本打包生成的 dSYM 符號化文件是無法完成符號化的。所以我們需要在上傳至 App Store 時需要勾選 Include app symbols for your application to receive symboilcated crash logs from Apple:
勾選之后 Apple 會給我們生成 dSYM乍丈,然后就可以在 Xcode -> Organizer
或者 iTunes Connect
中下載對應的 dSYM 來進行符號化了剂碴。
2.3.3. BitCode 的編譯選項優(yōu)化
上面所說的編譯器優(yōu)化是在編譯器前端完成的,所以提交的 BitCode 應該是經(jīng)過優(yōu)化的轻专。但是 去除符號信息忆矛,是在編譯生成可執(zhí)行文件之后完成的, 蘋果在生成可執(zhí)行文件之后是否給我們?nèi)コ朔栆膊坏枚?/p>
2.4. 清理無用代碼
2.4.1. Dead Code Stripping
Activating this setting causes the -dead_strip flag to be passed to ld(1) via cc(1) to turn on dead code stripping. Remove functions and data that are unreachable by the entry point or exported symbols
Xcode 默認會開啟此選項请垛,C/C++/Swift 等靜態(tài)語言編譯器會在 link 的時候移除未使用的代碼催训,但是對于 Objective-C 等動態(tài)語言是無效的。因為 Objective-C 是建立在運行時上面的宗收,底層暴露給編譯器的都是 Runtime 源碼編譯結(jié)果漫拭,所有的部分應該都是會被判別為有效代碼。
2.4.2. 通過掃描查找無用代碼
掃描無用代碼的基本思路都是查找已經(jīng)使用的方法/類和所有的類/方法混稽,然后從所有的類/方法當中剔除已經(jīng)使用的方法/類剩下的基本都是無用的類/方法采驻,但是由于 Objective-C 是動態(tài)語言,可以使用字符串來調(diào)用類和方法荚坞,所以檢查結(jié)果一般都不是特別準確挑宠,需要二次確認。目前市面上的掃描的思路大致可以分為 3 種:
- 基于 Clang 掃描
- 基于可執(zhí)行文件掃描
- 基于源碼掃描
2.4.2.1. 基于 clang 掃描
基本思路是基于 clang AST颓影。追溯到函數(shù)的調(diào)用層級,記錄所有定義的方法/類和所有調(diào)用的方法/類懒鉴,再取差集诡挂。具體原理參考 如何使用 Clang Plugin 找到項目中的無用代碼,目前只有思路沒有現(xiàn)成的工具临谱。
2.4.2.2. 基于可執(zhí)行文件掃描
Mach-O 文件中的 (__DATA,__objc_classlist)
段表示所有定義的類璃俗, (__DATA.__objc_classrefs)
段表示所有引用的類(繼承關(guān)系是在__DATA.__objc_superrefs
中);使用的方法和引用的方法也是類似原理悉默。因此我們使用 otool
等命令逆向可執(zhí)行文件中引用到的類/方法和所有定義的類/方法城豁,然后計算差集。具體參考iOS微信安裝包瘦身抄课,目前只有思路沒有現(xiàn)成的工具唱星。
2.4.2.3. 基于源碼掃描
一般都是對源碼文件進行字符串匹配。例如將 A *a
跟磨、[A xxx]
间聊、NSStringFromClass("A")
、objc_getClass("A")
等歸類為使用的類抵拘,@interface A : B
歸類為定義的類哎榴,然后計算差集。
基于源碼掃描 有個已經(jīng)實現(xiàn)的工具 - fui,但是它的實現(xiàn)原理是查找所有 #import "A" 和所有的文件進行比對尚蝌,所以結(jié)果相對于上面的思路來說可能更不準確迎变。
2.4.2.4. 通過 AppCode 查找無用代碼
AppCode 提供了 Inspect Code 來診斷代碼,其中含有查找無用代碼的功能飘言。
它可以幫助我們查找出 AppCode 中無用的類衣形、無用的方法甚至是無用的 import
,但是無法掃描通過字符串拼接方式來創(chuàng)建的類和調(diào)用的方法热凹,所以說還是上面所說的 基于源碼掃描 更加準確和安全泵喘。
通過掃描的方式去檢查無用代碼有個痛點就是 類的方法調(diào)用是一種引用關(guān)系,以上所說的四種思路都是查找到引用末端的未使用的代碼般妙,我們很難通過一次掃描就定位到所有未使用的類纪铺,自動化實現(xiàn)起來也較難。舉個例子來說碟渺,假如 A 是一個未使用到的類鲜锚,但是 A 引用了 B,所以首次檢查結(jié)果是 A 未被引用苫拍,B 被無用類 A 引用了芜繁,我們需要把 A 刪除了之后我們才能了解到 B 是否是無用的類。當然如果你重新去實現(xiàn)一個引用樹的話就另當別論了绒极。
由于掃描無用類實現(xiàn)起來較為麻煩骏令,并且其檢查結(jié)果也不是特別準確。所以建議還是讓開發(fā)者養(yǎng)成一個良好的習慣垄提,在迭代或者重構(gòu)代碼的時候把老的代碼刪除榔袋,不要等到量變引起質(zhì)變的時候才回頭去優(yōu)化。
2.5. 重構(gòu)重復代碼
重復代碼堆積太多铡俐,不僅意味著 Bad Code Smell
凰兑,我們的包大小也會受到影響,我們可以使用 PMD 來檢查項目中的重復代碼审丘,并且做選擇性的重構(gòu)吏够。
2.6. 補充:Cocoapods 中的優(yōu)化選項配置
Cocoapods
的 project
文件在每次 pod install
或者 pod update
會重置,所以需要 hook pod install
來設(shè)置 Pods 中每個 Target 的編譯選項:
post_install do |installer|
installer.pods_project.targets.each do |target|
target.build_configurations.each do |config|
config.build_settings['ENABLE_BITCODE'] = 'NO'
config.build_settings['STRIP_INSTALLED_PRODUCT'] = 'YES'
config.build_settings['SWIFT_COMPILATION_MODE'] = 'wholemodule'
if config.name == 'Debug'
config.build_settings['SWIFT_OPTIMIZATION_LEVEL'] = '-Onone'
config.build_settings['GCC_OPTIMIZATION_LEVEL'] = '0'
else
config.build_settings['SWIFT_OPTIMIZATION_LEVEL'] = '-Osize'
config.build_settings['GCC_OPTIMIZATION_LEVEL'] = 's'
end
end
end
end
三: 資源瘦身
本文中的資源分為圖片資源滩报、音頻資源锅知、視頻資源、視圖資源露泊、字體資源喉镰、網(wǎng)頁資源等等。資源都是弱類型的惭笑,會隨著項目工程的增長而持續(xù)增長侣姆,維護起來費時費力生真,所以一般都是希望能有工具化、自動化的解決方案捺宗。
3.1. 圖片資源瘦身
3.1.1. Xcode 支持
3.1.1.1. Compress PNG Files
Xcode 提供的給我們兩個編譯選項來幫助壓縮 PNG 資源:
-
Compress PNG Files
:打包的時候自動對圖片進行無損壓縮柱蟀,使用的工具為 pngcrush,壓縮比還是相當高的蚜厉,比較流行的壓縮軟件 ImageOptim 也是使用 pngcrush 進行壓縮 PNG 的长已。 -
Remove Text Medadata From PNG Files
:能幫助我們移除 PNG 資源的文本字符,比如圖像名稱昼牛、作者术瓮、版權(quán)、創(chuàng)作時間贰健、注釋等信息胞四。
項目引進的 PNG 資源都是自動被 Xcode 進行壓縮了,所以完全不需要自己再去用工具壓縮一遍伶椿。當除非你是使用 bundle 管理的資源辜伟,因為 bundle 是直接拷貝進項目,并不會被 Xcode 進行壓縮脊另;JPG 或者其他類型的圖片資源可以使用 ImageOptim進行無損壓縮然后導入到 Xcode 中导狡,為了提高效率建議還是提供 PNG 格式的圖片。
3.1.1.2. App Thinning/XCAssets
iOS 9 中引入的 App Thinning 中提到過 Slicing
的技術(shù)偎痛,當我們把一個完整的安裝包提交給 App Store 后旱捧,App Store 會為不同的設(shè)備準備不同的變體(Variant),設(shè)備的在下載 App 的時候它能幫助我們自動選擇合適的 Variant 進行下載踩麦。
可執(zhí)行文件的 Slicing
技術(shù)就是上面所說的 BitCode
廊佩,同樣資源文件也是支持 Slicing
的。比如 iPhone 6 下載的安裝包中就只會包含 2x 圖靖榕,iPhone 6 Plus 下載的安裝包就只會包含 3x 圖,但是只有使用 asset catelogs
(也就是 XCAssets) 管理的資源才支持 Slicing顽铸,所以盡量還是使用 XCAsset
來管理資源圖片茁计。同時 XCAsset
也支持 PDFs
矢量圖,在上傳到 App Store
之后谓松,會根據(jù)矢量圖自動生成 1x, 2x, 3x 圖星压,然后進行 Slicing
。
當然 XCAsset
也有它的存在的問題:
- 使用 XCAsset 管理的資源會被壓縮并打包成一個 Asset.car 文件鬼譬,我們無法獲取相應圖片的物理路徑娜膘,因此我們無法使用
[UIImage imageWithContentsOfFile:]
的方式來獲取圖片。對于那些需要使用物理路徑的方式來訪問的圖片优质,建議還是直接拖拽到 App 中進行管理竣贪。 - iOS 10.3 推出的更換
App Icon
的資源文件只能放在 App 根目錄下進行管理军洼。 - 使用 XCAsset 管理圖片后,Xib/Storyboard 中設(shè)置的帶后綴 .png 圖片在 Interface Builder 是不可見的演怎,都是顯示的問號匕争,但是運行起來是沒有問題的。最好的做法是全局搜索并去掉后綴保證更好的開發(fā)體驗爷耀。
3.1.2. 去除冗余圖片資源
3.1.2.1. 去除無用資源
未使用的資源可以使用腳本來進行刪除甘桑。強烈推薦使用 FengNiao 來自動刪除圖片,因為其相對比較新歹叮,是 2017 年開始開發(fā)的跑杭,并且是使用 swift 語言開發(fā)的,方便進行二次開發(fā)咆耿。FengNiao 的基本原理是查找出項目中所有使用到的字符串和項目中所有的資源文件德谅。兩者進行匹配(完全匹配和模式匹配,模式匹配支持帶數(shù)字資源的前綴/中綴/后綴匹配)票灰,計算差集就為未使用的資源女阀。
相比于之前流行的 LSUnusedResources,F(xiàn)engNiao 支持模式匹配會更加強大:比如我們導入 image_01 image_02 image_03 這樣的圖片資源作為幀動畫素材屑迂,使用的時候是 image_%d 或者 image_(index) 方式浸策,F(xiàn)engNiao 會把這些圖片資源作為使用中的資源,不會出現(xiàn)誤刪的情況惹盼。當然如果你還是用了其他 Pattern庸汗,可以考慮擴展 FengNiao。
除了這些之外手报,F(xiàn)engNiao 是命令行工具蚯舱,我們可以給 Xcode 添加 Run Script
,在每次構(gòu)建的時候自動檢測/清理未使用的資源掩蛤。
由于基于源碼的掃描工具結(jié)果不是百分百準確的枉昏,所以建議最好的做法是在項目編譯的時候提供出顯式的 Warning,然后再次確認之后再去刪除揍鸟。同時也可以配合資源命名規(guī)范來優(yōu)化工具兄裂,如果你們的命名規(guī)范和工具的檢測規(guī)范能夠保持一致的話,搜索的結(jié)果無疑是最為準確的阳藻。
之所以要使用自動化工具來檢測重復資源的原因是因為資源是弱類型晰奖,我們在項目迭代過程中手動去維護是相當麻煩的一個過程。轉(zhuǎn)換一下思維腥泥,如果資源變成強類型了匾南,那我們維護起來就相當容易了。目前就有這樣一個工具-R.swift蛔外,類似于 Android 開發(fā)中的 R 文件蛆楞,有興趣的可以去嘗試溯乒。
3.1.2.2. 去除重復資源
這里所說的重復資源是資源內(nèi)容相同但是命名不相同的一些資源,對于此類資源臊岸,我們可以使用 fdupes 來進行掃描并去除橙数,fdupes 的原理是對比不同文件的簽名,簽名相同的文件就會判定為重復資源帅戒。
然后我們就可以在 Xcode 中添加 Run Script
灯帮,對于掃描到的相同的資源,我們可以顯式的報出 Warning逻住,然后我們在開發(fā)階段解決資源重復的問題钟哥。
3.1.3. On-Demand Resources
On-Demand Resources 是 Apple 在 iOS 9 跟 App Thinning
一起引進的一項減少安裝包體積技術(shù),大致的概念是蘋果幫你把所有 App 中的資源管理在 App Store 云端上瞎访,然后你需要把資源標記為不同的 Tag腻贰,需要的時候才去下載相應 Tag 的圖片。引用蘋果文檔中的一張圖表示扒秸。
這種機制對于許多圖片資源都放在本地的 App 就會比較有用播演,比如游戲中的不同關(guān)卡可以分為不同的 tag,在用戶通關(guān)了一關(guān)之后才下載下一關(guān)資源伴奥。
3.2. 視頻/音頻圖片資源遠端化
視頻/音頻等圖片資源相對圖片來說會大很多写烤,所以建議把視頻/音頻放在服務(wù)端,客戶端在使用的時候進行下載或者使用流播放拾徙。
3.3. HTML5 遠端化
H5 資源也是建議放在服務(wù)端洲炊,如果對 H5 加載和離線訪問有要求的話,可以使用離線緩存的方式來緩存網(wǎng)頁資源到本地尼啡。
3.4. 視圖資源
這里所說的視圖資源是指 xib/storyboard
暂衡。xib
在打包時會被壓縮為 nib 文件,storyboard
文件會被壓縮為 storyboardc
文件崖瞭,storyboardc
是個壓縮包狂巢,內(nèi)部包含了相應的 nib 和 一個 plist 文件。一般的 nib 文件壓縮后在幾 KB 到幾十 KB 大小书聚,這部分包大小的影響相對于 xib 能提高開發(fā)效率來說影響是微乎及微的隧膘,網(wǎng)易漫畫 App 中使用到了 257 個 xib 文件,但是其在 payload 中的數(shù)據(jù)僅僅只有 1.7M 大小寺惫。
四: Framework
Framework 文件夾存放的是 Embedded Framework,它在打包的時候最終會被拷貝進 Target App Bundle
中的 Framework
文件夾中蹦疑,在 App 啟動的時候才會被鏈接和加載西雀。Embedded Framework
主要分類兩類:
- SwiftSupport:Framework 文件夾中前綴是 libSwift 的一些 framework。由于目前 Swift ABI 還未穩(wěn)定歉摧,我們發(fā)布應用的時候還需要帶上一份自己應用中使用到的 Swift 標準庫代碼艇肴,這部分占用最終 ipa 的大小可能在 10M 左右腔呜。
- 其他依賴庫:使用 Cocoapods 管理依賴并且設(shè)置了 user_framework! 時三方庫源碼都會打包成 framework,然后導入到工程當中
4.1. Framework 中的資源
這里所說的 Framework 表示的是: 靜態(tài)庫(.a) Framework(Static Library)
目前絕大部分的 Framework 的做法是直接將資源放進 bundle 中進行管理的再悼,在主工程打包的時候核畴,Xcode 會將這部分資源直接拷貝進 App Target Bundle
中,這樣做就存在2個問題:
- 使用 bundle 管理的資源是不會被
Xcode
優(yōu)化的(圖片壓縮等) - 使用 bundle 管理的資源不享受
App Thinning/Slicing
冲九。
所以盡量還是選擇 XCAsset
進行 Framework
的資源管理谤草,靜態(tài)庫和動態(tài)庫的管理方式有所不同:
-
靜態(tài)庫(.a)/Framework(Static Library)
: 靜態(tài)庫的目標文件(.a/.framework) 中是不能包含資源文件的,所以這部分只能使用 bundle 來管理莺奸。但是由于 bundle 直拷貝的特性丑孩,我們需要把 xib/storyboard/asset catalog 編譯后的產(chǎn)物(nib/storyboardc/Asset.car)放進 bundle 里。比較普遍的一個做法是借助 Bundle Target 來編譯我們的資源文件灭贷,具體做法看這篇文章 - 動態(tài)庫: 動態(tài)庫相對來說要簡單一點温学,因為動態(tài)庫本身就是一個 bundle。所以我們直接把資源文件放在目標文件(.framework)中就可以了甚疟。
如果你是使用 Cocoapods 管理你的源碼仗岖,也可以使用 XCAsset 來管理資源,參考 在 Cocoapods 中使用 XCAsset览妖。
4.2. Framework 中的可執(zhí)行文件
這部分可以參考以上的可執(zhí)行文件瘦身轧拄。
五: Plugins
Plugin 內(nèi)部主要存放的就是 App Extension,App Extension 是獨立打包簽名黄痪,然后再拷貝進 Target App Bundle 的紧帕。
5.1. Plugin 中的靜態(tài)庫
靜態(tài)庫最終會打包進可執(zhí)行文件內(nèi)部,所以如果 App Extension 依賴了三方靜態(tài)庫桅打,同時主工程也引用了相同的靜態(tài)庫的話是嗜,最終 App 包中可能會包含兩份三方靜態(tài)庫的體積。
5.2. Plugin 中的動態(tài)庫
動態(tài)庫是在運行的時候才進行加載鏈接的挺尾,所以 Plugin 的動態(tài)庫是可以和主工程共享的鹅搪,把動態(tài)庫的加載路徑 Runpath Search Paths
修改為跟主工程一致就可以共享主工程引入的動態(tài)庫。
5.3. Plugin 中的 Swift Standard Library
在 Swift ABI 穩(wěn)定之前遭铺,Swift 標準庫會被拷貝進 App 當中丽柿。Swift 標準庫是動態(tài)鏈接庫,也是可以在主工程和其他的 App Extensions
之間共享的魂挂,前提當然是所有 Target 使用的 Swift 版本是一致的甫题,否則就會出現(xiàn)意料之外的 bug。 設(shè)置共享分為兩步:
-
設(shè)置 Extension 中的 Always Embed Swift Standard Libraries 為 NO涂召,讓編譯器不再為 Extension 生成 Swift 標準庫
- 設(shè)置 Extension 中的動態(tài)庫的查找路徑為主工程的 Framework 文件夾
六:在網(wǎng)易漫畫中的實踐
我們在網(wǎng)易漫畫 App 中逐漸進行了實踐坠非,此次主要進行的是可執(zhí)行文件的瘦身:編譯優(yōu)化以及去除符號;資源瘦身:冗余資源清除以及主工程圖片分片(App Thinning)果正。下面是優(yōu)化前后的部分數(shù)據(jù):
優(yōu)化前 | 優(yōu)化后 | 效果 | |
---|---|---|---|
Executable | 35.4M | 17.1M | 52.1% |
Embedded Framework(除去 Swift STL) | 27.8M | 19M | 31.7% |
Images(FengNiao). | 13.3M | 11.7M | 12% |
Executable 的數(shù)據(jù)顯示的比較夸張的主要原因是我們在做瘦身的時候同時去除了之前使用到的直播 SDK炎码,實際可執(zhí)行文件的提升效果應該也在 30% 左右盟迟。IPA 文件從最初的 70+M 到現(xiàn)在 39.4M,總的來說效果還是相當明顯潦闲。
當然以上并非此次瘦身的全部內(nèi)容攒菠,F(xiàn)ramework 瘦身,冗余代碼清除等比較難的點后續(xù)也會陸續(xù)展開實踐歉闰,同時也會在本文中進行更新辖众。
七: 結(jié)論
- 將 Build Settings -> Clang/LLVM Generate Code -> Optimize Level 設(shè)置為 Fastest, Smallest(-Os)。
- 將 Build Settings -> Swift/LLVMGenerate Code -> Optimize Level 設(shè)置為 Optimize for Size(-Osize)新娜。
- 將 Build Settings -> Strip Linked Product 和 Strip Swift Symbols 設(shè)置為 YES赵辕,Deployment Postprocessing 設(shè)置為 NO,發(fā)布代碼的時候也需要勾選 Strip Swift Symbols概龄。
- Strip Debug Symbols During Copy 在 Release 模式下設(shè)置為 YES还惠。
- 有條件的話,適配 BitCode私杜。
- 無論在主工程或者 Framework 中都使用 XCAsset 來管理資源蚕键。
- 使用工具掃描刪除無用資源,推薦選擇 FengNiao衰粹,并添加 Run Scripts锣光。
- 使用工具掃描重復資源,推薦選擇 fdupes铝耻,并添加 Run Scripts誊爹。
- 如果你大量資源都放在本地,推薦使用 On-Demand Resources 方式來管理資源瓢捉。
- 在 Swift ABI 穩(wěn)定之前 Extension 和主 App 之間共享 Swift Standard Libraries频丘。
- 開啟 Compress PNG Files/Remove Text Metadata From PNG Files。
- 將 Dead Code Stripping 設(shè)置為 YES泡态。
- 使用工具掃描和清理無用代碼搂漠,同時養(yǎng)成良好習慣,在迭代或者重構(gòu)的時候刪除舊的代碼某弦。
- 使用工具掃描重復代碼并重構(gòu)它桐汤。
- 視頻/音頻/H5 等資源遠端化。
- 使用 xib/storyboard 來開發(fā)視圖界面會一定程序增加安裝包的大小靶壮。
- 使用 Swift 來開發(fā)程序會一定程序增加安裝包的大小怔毛,對包大小有嚴格要求的話也可以衡量是否使用 Swift。
- 如果你對包大小有嚴格要求的話腾降,選擇合適大小的三方庫來進行開發(fā)馆截。