1. 程式人生 > >ios 安裝包縮身

ios 安裝包縮身

安裝包組成:

談到 App 瘦身,最直接的想法莫過於分析一個安裝包內部結構,瞭解其每一部分的來源。解壓一個 ipa 包,拿到其 payload 中 app 檔案的資料,整理歸類後其大致如下:
Exectutable: 可執行檔案
Resources:資原始檔
2.1 圖片資源:Assets.car/bundle/png/jpg 等
2.2 視訊/音訊資源:mp4/mp3 等
2.3 靜態網頁資源:html/css/js 等
2.4 檢視資源:xib/storyboard 等
2.5 其他:文字/字型/證書 等
3. Framework:專案中使用的動態庫
3.1 SwiftSupport: libSwiftxxx 等一系列 Swift 庫
3.2 其他依賴庫:Embeded Framework
4. Pulgins:Application Extensions
5. CocosPods
appex:其組成大致與 ipa 包組成一致

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

一、資源瘦身
資源瘦身主要是去除無用資源或者壓縮資源。
資源主要包括圖片、音訊、視訊、多語言包、配置檔案等。
無用資源指的是,專案中沒有被引用的資源,找到的辦法就是,去專案中搜索該檔名,圖片資源去掉@2x,@3x,沒有搜尋到的就是無用資源。
當然,資源名在專案中另外拼接的,特殊處理,所以一個APP最好有一個統一的拼接格式。
壓縮資源,一般指圖片的壓縮,圖片資源控制在80k左右(@3x的全屏圖片);資源壓縮主要對png進行無失真壓縮,用的是ImageOptim工具和compress命令(需要安裝XQuartz-2.7.5.dm外掛)。不建議對資源做有失真壓縮,有失真壓縮需要設計一個個檢查,通常壓縮後效果不盡人意。

還有就是配置檔案的壓縮,比如內建的離線資源等。

1.1 審查安裝包中的每個檔案
審查安裝包中的每個檔案是最為簡單有效的挖掘優化點的方式,在包大小優化過程中也應被反覆執行。
1.2 Jimu app原生圖片資源縮身
在本次包大小優化期間,需要支援iOS7-iOS11之間的系統。與蘋果建議的方式一致,使用asset catalog來管理圖片。絕大部分圖片均為png格式。每種圖片都加入了2x圖和3x圖。以下的優化和討論也將基於這個前提。
對於圖片資原始檔的優化,我們主要採用了3個思路:
(1)圖片壓縮
(2)將圖片放置到雲端
(3)排查和清除冗餘圖片
3.1 圖片壓縮
想要優化資原始檔,可能大家想到的第一個優化方式就是壓縮。而更進一步,我們也考慮了是否能用webP等空間佔用更小的格式來替換png圖片。
3.1.1 png圖片壓縮
我們嘗試了一個小有名氣的png壓縮工具:ImageOptim。這個工具能夠在不改變圖片質量的情況下壓縮圖片的大小。開啟設定,我們能看到和選擇它使用的壓縮演算法。

ImageOptim會對每張圖片分別應用以上幾種壓縮演算法,然後對比每種壓縮演算法產出的圖片,選取最小的那張作為輸出結果。
我們使用ImageOptim對工程中幾乎所有的圖片做了一次壓縮。整個過程持續了若干小時。在壓縮過程中,我們發現,大部分圖片都能被壓縮到原來的70%左右,個別圖片能獲得更高的壓縮比。
ImageOptim的表現無疑是可觀的。然而當我們滿懷期望的提交修改、打包後,得到的結果卻有點出乎意料。雖然工程中的圖片都經過了ImageOptim壓縮,但我們的ipa大小並沒有什麼變化。
在查閱了一些文件後,我們瞭解到,Xcode在構建的過程中,有一個步驟叫做compile asset catalog。在這個步驟中,Xcode會自行對png圖片作壓縮,並且會壓縮成能夠快速讀取渲染的格式。如果我們對工程中的圖片進行了ImageOptim的壓縮,在compile asset catalog的過程中,Xcode會用自己的演算法重新壓縮,而這個”重新壓縮“的過程,相當於將ImageOptim的壓縮“回滾“了,很可能反而增大了圖片。
這也就表明了,無論我們怎麼壓縮工程中的png圖片,對包大小優化來說都是徒勞的。(但用ImageOptim工具壓縮jpg圖片還是有效的。)
使用8-bit的PNG圖片,比32-bit的圖片能減少4倍的壓縮率。由於8-bit的圖片支援最多256種不同的顏色,所以8-bit的圖片一般只應該用於一小部分的顏色圖片。例如灰度圖片最好使用8-bit。

尋求Xcode中與圖片相關的配置項
我們對png格式沒有做深入研究,所以這裡不能清楚解釋這兩個壓縮過程究竟做了什麼。但是Xcode讓png圖片“增大”的行為還是讓我們感到不甘,於是我們開始尋求是否有一些配置項能夠關閉Xcode的壓縮過程。
Xcode 提供的給我們兩個編譯選項來幫助壓縮 PNG 資源:
Compress PNG Files:打包的時候自動對圖片進行無失真壓縮,使用的工具為 pngcrush,壓縮比還是相當高的,比較流行的壓縮軟體 ImageOptim 也是使用 pngcrush 進行壓縮 PNG 的。
Remove Text Medadata From PNG Files:能幫助我們移除 PNG 資源的文字字元,比如影象名稱、作者、版權、創作時間、註釋等資訊。
專案引進的 PNG 資源都是自動被 Xcode 進行壓縮了,所以完全不需要自己再去用工具壓縮一遍。當除非你是使用 bundle 管理的資源,因為 bundle 是直接拷貝進專案,並不會被 Xcode 進行壓縮;JPG 或者其他型別的圖片資源可以使用 ImageOptim 進行無失真壓縮然後匯入到 Xcode 中,為了提高效率建議還是提供 PNG 格式的圖片。
iOS 9 中引入的 App Thinning 中提到過 Slicing 的技術,當我們把一個完整的安裝包提交給 App Store 後,App Store 會為不同的裝置準備不同的變體(Variant),裝置的在下載 App 的時候它能幫助我們自動選擇合適的 Variant 進行下載。
可執行檔案的 Slicing 技術就是上面所說的 BitCode,同樣資原始檔也是支援 Slicing 的。比如 iPhone 6 下載的安裝包中就只會包含 2x 圖,iPhone 6 Plus 下載的安裝包就只會包含 3x 圖,但是隻有使用 asset catelogs(也就是 XCAssets) 管理的資源才支援 Slicing,所以儘量還是使用 XCAsset 來管理資源圖片。同時 XCAsset 也支援 PDFs 向量圖,在上傳到 App Store 之後,會根據向量圖自動生成 1x, 2x, 3x 圖,然後進行 Slicing。
當然 XCAsset 也有它的存在的問題:
使用 XCAsset 管理的資源會被壓縮並打包成一個 Asset.car 檔案,我們無法獲取相應圖片的物理路徑,因此我們無法使用 [UIImage imageWithContentsOfFile:] 的方式來獲取圖片。對於那些需要使用物理路徑的方式來訪問的圖片,建議還是直接拖拽到 App 中進行管理。
iOS 10.3 推出的更換 App Icon 的資原始檔只能放在 App 根目錄下進行管理。
使用 XCAsset 管理圖片後,Xib/Storyboard 中設定的帶字尾 .png 圖片在 Interface Builder 是不可見的,都是顯示的問號,但是執行起來是沒有問題的。最好的做法是全域性搜尋並去掉字尾保證更好的開發體驗。

可能的配置項:
Compress PNG Files (COMPRESS_PNG_FILES)
Optimization (ASSETCATALOG_COMPILER_OPTIMIZATION)

經過試驗,Compress PNG Files選項對asset catalog中的資源無效,因為這個選項僅適用於零散資原始檔。
Optimization置為space也對包大小沒有任何影響,原因有兩點:
(1)app工程使用cocoapods管理,並且命中了cocoapods合併asset catalog的策略,asset catalog的編譯過程在cocoapods生成的指令碼中,故build settings中的設定無效;
(2)Optimization引數只對最低支援iOS8及以上的app起作用。具體的分析過程下文中將介紹。
尋求修改構建過程
由於無法用正當途徑跳過、改變Xcode對png的壓縮過程,我們還抱著希望想尋求是否有其他trick的方式能夠阻止Xcode壓縮圖片。
比如我們是否可以在編譯期間插入指令碼來干預compile asset catalog的過程?我們是否可以更改build rule來定製對asset catalog的處理方式?
在編譯過程中,我們能看到compile asset catalog這個過程使用的工具是actool。這是一個內建在Xcode裡的工具。我們可以在以下路徑中找到actool:

遺憾的是,actool並非一個指令碼,而是一個編譯完成的二進位制檔案。這就導致compile asset catalog的過程變成了完全的黑盒。 我們嘗試了直接將actool工具刪除,但這樣會直接導致無法構建成功。顯然Xcode的設計者不會希望使用者干預它的構建過程。
經過分析Jimu app工程的構建過程,我們發現,由於Jimu app的工程使用cocoapods進行了庫管理,並且Jimu app的工程滿足了一些條件,實際上真正有效執行asset catalog編譯的過程是在[CP] Copy Pods Resources這個指令碼中。這也是上文中設定Optimization引數無效的原因之一。
這個指令碼呼叫了actool工具完成了最後一步:

在這裡我們可以看到actool的一些引數,改動這些引數是否能改變actool的壓縮策略呢? 遺憾的是,我們測試了去掉–compress-pngs引數、增加–optimization time和增加–optimization space引數,發現這些改動對包大小都沒有任何影響。

為什麼增加–optimization time和增加–optimization space引數對包大小沒有影響呢?這個結果顯然非常不符合預期,而網路上關於ASSETCATALOG_COMPILER_OPTIMIZATION引數的文件也甚少,讓我們疑惑不已。
通過demo實驗,我們發現,當工程不依賴cocoapods時,build setting中修改optimization選項是有效的,那理論上,在指令碼呼叫actool時傳入–optimization space,應該也能起作用才對。
經過將Jimu app工程多次與demo對比,最終我們發現了問題的根源:Jimu app的工程最低支援iOS7,而optimization引數似乎在iOS8及以後才能起作用。在這一輪包大小優化期間,我們還無法放棄iOS7,所以optimization的思路只能就此終止。
如果考慮修改build rule呢?我們是不是可以嘗試使用自定義的工具編譯asset catalog?但經過嘗試,這個方法似乎也行不通。因為build rule是用來處理Xcode不認識的原始碼型別的,並不能改變已有型別的編譯方式。
至此,我們企圖壓縮asset catalog中png圖片的想法暫時就告終了。從這個過程中,我們能看出,Xcode對於png圖片的壓縮方式進行了很強的控制,它似乎不允許第三方開發者干預png圖片的壓縮過程。
3.1.2 使用webP替代png
壓縮實驗失敗後,我們仍然不甘心止步於此。由於開發者難以干預asset catalog內的圖片,一個自然的想法產生了:我們是否能廢棄asset catalog?
廢棄asset catalog可能能帶來以下兩個收益:
(1)可以考慮將png圖片切換到webP等其他格式
(2)廢棄Asset Catalog後,可以刪去2x的圖片,只保留3x的圖片。經過hook改造系統方法的實現,我們驗證了這個想法是可行的。
考慮到app從asset catalog中讀取圖片可能比從bundle中讀取圖片有更高的效能,所以在開發過程中,啟動階段的圖片依然被保留在了asset catalog中。最終10.5MB的asset.car檔案被優化成了3.6MB的asset.car+3.6MB的零散資原始檔,看起來減少了3.2MB,是一個比較可觀的數值。
app slicing
然而在這一系列優化過程中,我們僅僅關注了內部平臺構建出的安裝包的包大小,而忽視了app store中使用者看到的包大小值。實際上後者才是真正影響到轉換率等核心指標的關鍵。
經同事提醒,我們這樣的優化方法和蘋果提供的app slicing優化有衝突。實驗後我們發現,廢棄asset catalog事實上可能會導致包大小不減反增。
app slicing是iOS9增加的功能。當用戶從app store上下載app時,可以只下載適用於其裝置的app架構版本和所需資源,從而減少app所佔的空間。
如果開發者想要使用app slicing,只需要將資原始檔用Asset Catalog管理,不需要做額外的任何事情。 因此,app已經有了app slicing的效果,用3x的裝置檢視app store中的“Jimu app”,顯示的包大小比2x的裝置大3MB(而沒有使用asset catalog的騰訊新聞,兩個手機顯示的包大小是一致的)
使用Xcode的archive方式構建的安裝包,可以在匯出ipa時制定相應的裝置,來測試app slicing功能。 嘗試後我們發現,對於2x的裝置,廢棄asset catalog反而會導致安裝包增加1MB,而對於3x的裝置,廢棄asset catalog能優化安裝包大小1MB。然而顯然,這樣的優化是得不償失的。
3.2 將圖片放置到雲端
將部分圖片放置到雲端,等到使用者需要時再去下載,這看起來也是一個優化安裝包大小的方法。我們對蘋果提供的On Demand Resources功能進行了嘗試,也自行開發了資源包下載邏輯。
3.2.1 On Demand Resource
蘋果從iOS 9開始引入了On Demand Resource功能,即一部分圖片可以被放置在蘋果的伺服器上,不隨著app的下載而下載,直到使用者真正進入到某個頁面時才下載這些資原始檔。
我們考慮可以讓某些業務僅在iOS 9及以後版本中可用,然後應用On Demand Resource來優化這些業務的資源。
經過了一段時間的開發實驗,一切都如同預期,當我們以為On Demand Resource是一個可行的思路時,我們卻發現了一個Xcode巨坑的問題:當工程需要支援iOS9以下系統時,Xcode會在打包完成上傳app store時失敗。On Demand Resource的想法只能擱置。
3.2.2 資原始檔雲端下載
由於On Demand Resource實驗失敗,我們自行開發了一套雲端下載流程,並且對個別個別大圖(幾乎為全屏大小的圖片)進行了嘗試。首批圖片精簡後,安裝包大小減少了1.1MB。
雲端下載的策略為:
(1)在若干時機嘗試下載zip圖片包,對zip包進行版本判斷,若雲端有更新版本,則根據螢幕是3x還是2x,下載對應的zip包,解壓存入沙盒中;
(2)在讀取圖片時,首先從bundle中讀取,若失敗,則從沙盒中讀取,若依然失敗,則將該圖片當作一個網路圖片進行請求,確保圖片能被展示。
經過線上測試,大約95%的場景下,使用者可以從沙盒中成功讀取圖片,剩下約5%場景下使用者會將圖片當作網路圖片來請求。當然這個實驗結論會隨著圖片所在的頁面層級變化。
3.3 排查和清除冗餘圖片
資原始檔雲端下載雖然是一個優化安裝包大小的有效思路,但多少對使用者體驗有一些影響。所以我們又將優化的重點放到了排查和清理冗餘圖片上。最後的結果證明,排查和清理冗餘圖片的確能帶來客觀有效的優化。
出去正常的排查冗餘圖片的流程,我們還在不斷審視安裝包內容時收穫了意外的發現。整個排查和清除冗餘圖片可以分為三個方向:
(1)常規的冗餘圖片清理
(2)修復cocoapods帶來的圖片重複合併問題
(3)利用tint color精簡單色圖示
3.3.1 常規的冗餘圖片清理
隨著業務迭代,有不少圖片成為了永遠也不會使用到的殭屍圖片。這些圖片往往佔據著較大空間,對於冗餘圖片的排查和清理是包大小優化中便捷而有效的一項優化內容。 Jimu app iOS端在三輪包大小優化中都進行了冗餘圖片排查,每次都能清理出的圖片體現在ipa上的大小都在500KB以上,相對而言是比較可觀的數值。
使用一個開源的Mac app,LSUnusedResources,來進行冗餘圖片的排查: https://github.com/tinymind/LSUnusedResources

這個app的原理是,對某一檔案目錄下所有的原始碼檔案進行掃描,用正則表示式匹配出所有的@"xxx"字串(會根據不同型別的原始碼檔案使用不同的匹配規則),形成“使用到的圖片的集合”,然後掃描所有的圖片檔名,檢視哪些圖片檔名不在“使用到的圖片的集合”中,然後將這些圖片檔案呈現在結果中。
推薦使用 FengNiao 來自動刪除圖片,因為其相對比較新,是 2017 年開始開發的,並且是使用 swift 語言開發的,方便進行二次開發。FengNiao 的基本原理是查找出專案中所有使用到的字串和專案中所有的資原始檔。兩者進行匹配(完全匹配和模式匹配,模式匹配支援帶數字資源的字首/中綴/字尾匹配),計算差集就為未使用的資源。
相比於之前流行的 LSUnusedResources,FengNiao 支援模式匹配會更加強大:比如我們匯入 image_01 image_02 image_03 這樣的圖片資源作為幀動畫素材,使用的時候是 image_%d 或者 image_(index) 方式,FengNiao 會把這些圖片資源作為使用中的資源,不會出現誤刪的情況。當然如果你還是用了其他 Pattern,可以考慮擴充套件 FengNiao。
除了這些之外,FengNiao 是命令列工具,我們可以給 Xcode 新增 Run Script,在每次構建的時候自動檢測/清理未使用的資源。
由於基於原始碼的掃描工具結果不是百分百準確的,所以建議最好的做法是在專案編譯的時候提供出顯式的 Warning,然後再次確認之後再去刪除。同時也可以配合資源命名規範來優化工具,如果你們的命名規範和工具的檢測規範能夠保持一致的話,搜尋的結果無疑是最為準確的。
之所以要使用自動化工具來檢測重複資源的原因是因為資源是弱型別,我們在專案迭代過程中手動去維護是相當麻煩的一個過程。轉換一下思維,如果資源變成強型別了,那我們維護起來就相當容易了。目前就有這樣一個工具-R.swift,類似於 Android 開發中的 R 檔案,有興趣的可以去嘗試。

3.3.2 修復cocoapods帶來的圖片重複合併問題
Jimu app重度使用cocoapods進行庫管理。隨著平臺化的進行,越來越多的程式碼被封裝成了pod庫,以庫的形式整合進工程中。在排查安裝包內資原始檔的過程中,我們也發現了2個由cocoapod帶來的“圖片重複合入安裝包”的問題。這兩個問題的解決,也給安裝包大小優化帶來了700KB左右的優化。
png檔案和asset catalog重複合入安裝包
在排查安裝包內容時,我們發現.app檔案的最外層,有一些預期外的零散資原始檔。Jimu app的資原始檔絕大部分都是用asset catalog管理,僅有個別圖片以零散png的形式打入安裝包中。這些圖片的出現不符合預期。
經過排查,我們發現這些圖片來自於一個pod庫。而奇怪的是,這個pod庫的確是使用asset catalog進行資原始檔管理的,為什麼圖片還會以png的形式進入到安裝包中呢?
原來,這個pod庫在編寫podspec的時候,用了這樣的語句指定資原始檔:

我們使用demo進行了測試,發現podspec中這樣書寫,會導致asset catalog中的圖片,既作為asset catalog被合併到主工程的asset.car中,也會作為png被拷貝到安裝包中。導致其中一套圖片白白佔用了安裝包空間。
在這個例子中,使用萬用字元來指定pod庫中的資原始檔顯然是不合理的,會帶來不可預期的陷阱。應該以白名單的形式明確指定哪些資原始檔是pod庫中有效的資原始檔。
cocoapods暴力合併工程內asset catalog問題
在更新另一個業務方的pod庫的時候,我們還發現了一個資原始檔被重複合入安裝包的問題。
Pod庫在podspec中是這樣指定資原始檔的:

在業務方自己的獨立app和pod庫的樣例工程中,這樣指定資原始檔沒有任何異常。但是當這個pod庫接入到Jimu app中時,我們卻發現包大小的增長超過了預期。
簡單排查發現,這個Image.xcassets中的圖片,既作為了一個單獨的asset.car被放入了名為MyPod的bundle中,又被合併到了主工程的asset.car中,而後者是預期之外的。這導致這些圖片在安裝包中存在了兩份。

究其原因,我們發現原來這是cocoapod的一個缺陷導致的。
在工程構建的最後一步,會執行一個Copy Pods Resources的步驟,該步驟就是執行一個Pods-NewsInHouse-resources.sh指令碼,指令碼內容在pod install的時候生成。 這個指令碼的最末幾行有這樣的一個操作:

即如果工程符合某些條件,則找到工程目錄下所有的xcassets,使用xcode的actool工具將這些xcassets合併為一個assets.car檔案。
這裡合併的是“工程目錄下的所有xcassets”,也就是說,不管這個xcassets針對的是哪個target,是否被工程使用了,只要它在工程的某個子資料夾下,就會被打包進安裝包中。
顯然這樣的暴力合併可能導致安裝包莫名其妙增大、圖片資源莫名其妙衝突等問題。
暴力合併需要工程符合什麼樣的條件?第一行的if語句列出了三個條件:
(1) WRAPPER_EXTENSION是一個環境變數,構建iOS app是一般為app,所以我們工程肯定符合
(2) xcrun —find actool查詢xcode的環境中是否有actool工具,我們的工程肯定符合
(3) XCASSET_FILES是一個數組,其中有幾個元素取決於有多少個pod,將xcassets寫到了s.resoures中。這個條件目前Jimu app的工程符合,而業務方獨立app和樣例工程不符合,所以Jimu app的工程符合以上三個條件,該指令碼會執行暴力合併步驟。
為什麼cocoapods需要這樣暴力合併?
因為主工程的xcasset命名不規律,檔案儲存位置不規律,cocoapods的開發者也找不到更好的方法來準確合併所有需要的xcassets檔案,所以只能採取這種暴力的方式。
如何避免這樣的暴力合併?我們思考是否能通過制定pod庫接入規範來杜絕podspec中resource_bundles的指定方式,但顯然這樣的規範沒有什麼合理性,也難以得到業務方的認同。
於是我們轉而思考是否能通過技術手段來填補cocoapod的缺陷?工程中的xcasset的確有無法規範命名、儲存位置不規律的問題,但是它們都屬於某個target,可以通過target來檢索到所有應該合併的xcasset檔案。
目前我們執行的解決方案是:在build phase中,在執行copy pods resources之前,執行一個指令碼,替換Pods-NewsInHouse-resources.sh指令碼的某一行,用更合理的合併方式取代暴力合併。
替換掉的一行是:(這一行會找出工程根目錄下所有的xcassets)

替換為:(get_all_xcassets是我們寫的一個ruby指令碼,這一行的作用是利用xcodeproj工具找出當前target的build phase中的copy bundle resources中的所有xcassets)

也就是說,替換後我們不再暴力合併工程根目錄下所有的xcassets,而只是合併當前target需要的xcassets。
回過頭,我們再來分析一下指定resource_bundles和指定resources的區別。
Resource_bundles是cocoapods 0.23.0加入的一個屬性,比起resources,cocoapods官方更推薦使用resource_bundles:
We strongly recommend library developers to adopt resource bundles as there can be name collisions using the resources attribute. Moreover, resources specified with this attribute are copied directly to the client target and therefore they are not by Xcode.
Cocoapods的文件中提到了兩點:
(1)使用resource_bundles能大大減小命名衝突的概率
(2)使用resources資原始檔是直接拷貝到app中的(對於xib,xcassets等,cocoapods會用指令碼進行編譯),沒有經過Xcode的優化(此處指的應該是零散的png)
在構建過程中,使用resources的資原始檔,會在構建的最後一步Pods-NewsInHouse-resource.sh中被拷貝到app中。 使用resource_bundles的資原始檔,在構建pod時,就已經被合併到bundle中了,最後在Pods-NewsInHouse-resource.sh中這些bundle被拷貝到app中。 放在resouce_bundles中的資原始檔,整個構建過程更符合Xcode的構建方式,能應用Xcode的優化,跟進Xcode的版本,所以一般情況下更推薦將資原始檔放到resource_bundles中。
制定pod庫資原始檔規範
經歷了兩次pod庫資原始檔合併帶來的陷阱,我們認為有必要制定一個資原始檔接入規範。
對於最低支援iOS7的pod:
我們推薦使用resource配合xcassets的方式來整合各個外掛中的資源 具體的做法有:
1、Pod中的資原始檔建議使用xcassets組織
(1)xcassets需要新增到podspec的resources中
(2)xcassets中的圖片名,必須使用字首;(xcassets間的命名衝突會導致讀取的圖片不可預期)
2、如果pod中有資原始檔沒有用xcassets組織
(1)這些資原始檔必須放入resource_bundles中,禁止放入resource中;(resource_bundle中的資源在構建期能經過Xcode的優化,而resource中的資源不能)
對於最低支援iOS8的pod:
我們推薦使用resource_bundles配合xcassets的方式來整合各個外掛中的資原始檔。 具體的做法有:
1、Pod中的資原始檔建議使用xcassets組織
(1)xcassets需要新增到podspec的resource_bundles中
(2)Pods中的程式碼,在讀取圖片資源時,使用imageNamed:inBundle:compatibleWithTraitCollection:讀取(該方法最低支援iOS8),無法使用imageNamed:讀取
2、如果pod中有資原始檔沒有用xcassets組織
(1)這些資原始檔必須放入resource_bundles中,禁止放入resource中;(resource_bundle中的資源在構建期能經過Xcode的優化,而resource中的資源不能)
如果該pod不需要支援iOS7,則更支援使用後者方式,這樣做的優點有:
(1)各個pod管理各自的資原始檔,不會有命名衝突的問題
(2)能利用蘋果的app slicing功能
(3)防止cocoapods暴力合併所有xcassets的引起其他潛在問題
3.3.3 利用tint color精簡單色圖示
在瀏覽了安裝包內所有的圖片檔案後,我們產生了一個直觀的感受:由於有日夜間模式,導致大部分圖示都切出了日間和夜間兩套圖示,而這兩套圖示的形狀是完全一致的,只是顏色有差異。
如果能結合tint color對單色圖示做一次精簡,對安裝包大小和圖示的管理都有正向的影響。
tint color是蘋果在iOS7推出的功能,我們可以讀取一個圖示,然後給它賦予一個color值,在手機螢幕上它就能顯示出相應的效果。tint color適用於對單色圖示進行著色,相比於其他精簡圖示的解決方案,tint color方便、可靠、擁有原生支援。
精簡圖示的工作需要各業務端共同參與,可以預計將消耗較大的人力成本。為了儘量減輕業務方的負擔,我們提前做了一些預備工作,包括篩選、色值抽取、色號匹配、分配到人等。這些工作均使用指令碼完成。
最終我們篩選出了大約3MB、1500+張形狀重複的圖片,理想情況下可以精簡掉其中的一半。
最後我們將候選圖片以這樣的檔名輸出:

image
檔名中包含了精簡所需要的全部資訊,便於業務方接入。

image
為了將圖片中的有效資訊抽取出來放在檔名中,我們提前做了以下這些工作:
(1)獲取ipa內的全部圖片
使用工具 iOS Images Extractor可以幫助解壓asset.car檔案,獲得ipa內全部圖片。
(2)篩選“形狀一致”的圖片
由於我們對影象處理並沒有做深入研究,所以使用了一個拍腦袋想出來的樸素方法:獲取一張圖片所有畫素點的alpha值,alpha值完全一致的兩張圖片,就是“形狀一樣”的圖片。
我們使用了ImageMagick這個工具抽取影象的每個畫素點值,然後對所有alpha值做md5計算。經過目測,使用這個方式來篩選形狀一致的圖片還是比較有效的。
(3)獲取單色圖片的色值
使用ImageMagick工具,抽取影象每個畫素點值,排除掉全透明的點,然後找到色值的眾數,則可以認為是該單色圖片的色值。
(4)獲取圖片的色號
擁有了色值之後,有些app可能就可以直接用色值來做後續開發了。但是Jimu app中不允許使用色值,必須使用UI規範中的“色號”,比如“面1”、“字1”之類的。
同時我們希望矯正那些“有一點偏差”的色值。
下圖底色為標準色,而icon的顏色其實並不是標準色,有一點差,但是肉眼基本看不出來,可能是設計師在作圖時手抖了。這種情況下我們就需要做“矯正”。

image
這個問題也可以表述為:如何將一個色值匹配到與它最接近的標準色上?
對影象沒有研究,經過一番google,我知道了這個命題的關鍵字叫做“color distance”,於是又一番google,得知了一些公式,比如:http://colormine.org/delta-e-calculator/
最後找了一個開源的工具:http://chir.ag/projects/ntc/
這是一個js的工具,能將一個色值匹配到與它最接近的某個顏色名稱上。
於是我直接將Jimu app的標準色色值給複製到了原碼裡。於是這個指令碼可以完成的工作是:輸入隨便一個色值,輸出與之最接近的Jimu app標準色色值。
最後,如何將標準色值再對映到“字x”、“面x”呢,這就需要找到一張圖的日間模式和夜間模式,然後用兩個標準色值去找色號名字。
(5)將圖片分配到人
精簡圖示的工作需要各業務方來推動,所以在做準備工作時,我們需要將每個圖示分配到各業務方。由於僅憑肉眼很難判斷一個圖示是屬於哪個業務的,所以我們使用了git log作為分發依據,以誰新增誰負責為原則。
對於指定的圖片名,我們首先使用mdfind命令找出它所在的路徑,然後讀取git log,查詢到該圖片的新增者,完成分類。
使用tint color著色,不僅能精簡掉形狀相同的夜間模式圖示,可能對日間模式圖示還能帶來優化空間。
在使用tint color著色後,單色圖片自身的顏色(RGB色值)便失去了意義。圖片提供的全部資訊實際就只有alpha通道的資訊。在這種情況下,考慮將圖片轉為灰度圖可以進一步縮減圖片體積。
整個tint color的接入工作還在進展過程中。

1.3 Jimu app原生音訊資源縮身

  1. 參考WWDC中的Audio Development for Games,裡面介紹瞭如何有效的處理音訊。常規來說,我們要使用AAC或MP3來壓縮音訊,並且可以嘗試降低一下音訊的位元率。有時候44.1khz的取樣是沒有必要的,稍微低一點的位元率也不會降低音訊的質量。

1.4 Jimu app原生視訊資源縮身
視訊/音訊等圖片資源相對圖片來說會大很多,所以建議把視訊/音訊放在服務端,客戶端在使用的時候進行下載或者使用流播放。

1.5 Jimu app Unitiy資源縮身

1.6 Jimu app Blockly資源縮身
1.7 html資源縮身
H5 資源也是建議放在服務端,如果對 H5 載入和離線訪問有要求的話,可以使用離線快取的方式來快取網頁資源到本地。

1.8 檢視資源
這裡所說的檢視資源是指 xib/storyboard。xib 在打包時會被壓縮為 nib 檔案,storyboard 檔案會被壓縮為 storyboardc 檔案,storyboardc 是個壓縮包,內部包含了相應的 nib 和 一個 plist 檔案。一般的 nib 檔案壓縮後在幾 KB 到幾十 KB 大小,這部分包大小的影響相對於 xib 能提高開發效率來說影響是微乎及微的,網易漫畫 App 中使用到了 257 個 xib 檔案,但是其在 payload 中的資料僅僅只有 1.7M 大小。

1.8 Framework 和 Framework中的資源
Framework 資料夾存放的是 Embedded Framework,它在打包的時候最終會被拷貝進 Target App Bundle 中的 Framework 資料夾中,在 App 啟動的時候才會被連結和載入。Embedded Framework 主要分類兩類:
SwiftSupport:Framework 資料夾中字首是 libSwift 的一些 framework。由於目前 Swift ABI 還未穩定,我們釋出應用的時候還需要帶上一份自己應用中使用到的 Swift 標準庫程式碼,這部分佔用最終 ipa 的大小可能在 10M 左右。
其他依賴庫:使用 Cocoapods 管理依賴並且設定了 user_framework! 時三方庫原始碼都會打包成 framework,然後匯入到工程當中。
Framework中的資源
這裡所說的 Framework 表示的是: 靜態庫(.a) Framework(Static Library)
目前絕大部分的 Framework 的做法是直接將資源放進 bundle 中進行管理的,在主工程打包的時候,Xcode 會將這部分資源直接拷貝進 App Target Bundle 中,這樣做就存在2個問題:
使用 bundle 管理的資源是不會被 Xcode 優化的(圖片壓縮等)
使用 bundle 管理的資源不享受 App Thinning/Slicing。
所以儘量還是選擇 XCAsset 進行 Framework 的資源管理,靜態庫和動態庫的管理方式有所不同:
靜態庫(.a)/Framework(Static Library): 靜態庫的目標檔案(.a/.framework) 中是不能包含資原始檔的,所以這部分只能使用 bundle 來管理。但是由於 bundle 直拷貝的特性,我們需要把 xib/storyboard/asset catalog 編譯後的產物(nib/storyboardc/Asset.car)放進 bundle 裡。比較普遍的一個做法是藉助 Bundle Target 來編譯我們的資原始檔,具體做法看這篇文章。
動態庫: 動態庫相對來說要簡單一點,因為動態庫本身就是一個 bundle。所以我們直接把資原始檔放在目標檔案(.framework)中就可以了。
如果你是使用 Cocoapods 管理你的原始碼,也可以使用 XCAsset 來管理資源,參考 在 Cocoapods 中使用 XCAsset。

1.8 Plusins
Plugin 內部主要存放的就是 App Extension,App Extension 是獨立打包簽名,然後再拷貝進 Target App Bundle 的。
Plugin中的靜態庫: 靜態庫最終會打包進可執行檔案內部,所以如果 App Extension 依賴了三方靜態庫,同時主工程也引用了相同的靜態庫的話,最終 App 包中可能會包含兩份三方靜態庫的體積。
Plugin中的動態庫: 動態庫是在執行的時候才進行載入連結的,所以 Plugin 的動態庫是可以和主工程共享的,把動態庫的載入路徑 Runpath Search Paths 修改為跟主工程一致就可以共享主工程引入的動態庫。
Plugin中的Swift Standard Library:
在 Swift ABI 穩定之前,Swift 標準庫會被拷貝進 App 當中。Swift 標準庫是動態連結庫,也是可以在主工程和其他的 App Extensions 之間共享的,前提當然是所有 Target 使用的 Swift 版本是一致的,否則就會出現意料之外的 bug。 設定共享分為兩步:(1)設定 Extension 中的 Always Embed Swift Standard Libraries 為 NO,讓編譯器不再為 Extension 生成 Swift 標準庫。(2)設定 Extension 中的動態庫的查詢路徑為主工程的 Framework 資料夾。

二、可執行檔案瘦身

  1. 在講可執行檔案瘦身之前先介紹Xcode的LinkMap檔案。LinkMap檔案是Xcode產生可執行檔案的同時生成的連結資訊,用來描述可執行檔案的構造成分,包括程式碼段(__TEXT)和資料段(__DATA)的分佈情況。只要設定Project->Build Settings->Write Link Map File為YES,並設定Path to Link Map File,build完後就可以在設定的路徑看到LinkMap檔案了。

link map是編譯連結時可以生成的一個txt檔案,它生成目的就是幫助程式設計師分析包大小。link map記錄了每個方法在當前的二進位制架構下佔據的空間。通過分析link map,我們可以瞭解每個類甚至每個方法佔據了多少安裝包空間。
在編譯時開啟Xcode build setting中的Write Link Map File開關,Xcode就會生成一份link map檔案。
目前已經有不少開源的分析link map的工具,可以輸出每個類、每個靜態庫佔用的空間,並進行排序。通過檢視link map,我們可以對二進位制程式碼佔據的包大小空間有個直觀瞭解,同時在引入第三方庫時也可以使用link map作出評估。


第一部分列舉可執行檔案裡所有.obj檔案,以及每個檔案的編號。
2. Sections:

第二部分是可執行檔案的段表,描述各個段在可執行檔案中的偏移位置和大小。第一列是段的偏移量,第二列是段佔用大小,Address(n)=Address(n-1)+Size(n-1);第三列是段型別,程式碼段和資料段;第四列是段名字,如__text是可執行機器碼,__cstring是字串常量。有關段的概念可參考蘋果官方文件《OS X ABI Mach-O File Format Reference》
3. Symbols:
Address Size File Name
0x100005A50 0x00000074 [ 1] +[WCPayInfoItem initialize]

0x10231C120 0x00000018 [ 1] literal string: [email protected][email protected]"WCPayInfoItem"8

0x10252A41A 0x0000000E [ 1] literal string: WCPayInfoItem

第三部分詳細描述每個obj檔案在每個段的分佈情況,按第二部分Sections順序展示。例如序號1的WCPayInfoItem.o檔案,+[WCPayInfoItem initialize]方法在__TEXT.__text地址是0x100005A50,佔用大小是116位元組。根據序號累加每個obj檔案在每個段的佔用大小,從而計算出每個obj檔案在可執行檔案的佔用大小,進而算出每個靜態庫、每個功能模組程式碼佔用大小。這裡要注意的地方是,由於__DATA.__bbs是代表未初始化的靜態變數,Size表示應用執行時佔用的堆大小,並不佔用可執行檔案,所以計算obj佔用大小時,要排除這個段的Size。

回到我們的可執行檔案瘦身問題,LinkMap檔案可以幫助我們尋找優化點。

如何進行二進位制檔案優化
通過審查Jimu app的安裝包,發現Jimu app的二進位制檔案佔了相當大的體積(100+MB)。想要優化二進位制檔案的大小,我們必須精簡程式碼。
在精簡程式碼層面上,我們主要從兩個思路著手:使用技術手段排查刪減冗餘程式碼、監控程式碼的增長情況和分佈。另外優化編譯選項也是行之有效的方法。
2.1 技術手段排查冗餘程式碼
沒有被引用的類和方法是可以通過技術手段被篩選出來的。
MachO檔案中有__DATA.__objc_classrefs和__DATA.__objc_selrefs段,分別近似於“被使用的類的集合”和“被使用的方法的集合”。通過取差集的方式可以篩選出未被使用的類和方法。
去掉冗餘字串
程式碼上定義的所有靜態字串都會記錄在在可執行檔案的__cstring段,如果專案裡Log非常多,這個空間佔用也是可觀的,也有幾百K的大小,可以考慮清理所有冗餘的字串。另外如果有特別長的字串,建議抽離儲存成靜態檔案,因為AppStore對可執行檔案加密導致壓縮率低,特別長的字串抽離成靜態資原始檔後壓縮率會比在可執行檔案裡高很多。另外如果有特別長的字串,建議抽離儲存成靜態檔案,因為AppStore對可執行檔案加密導致壓縮率低,特別長的字串抽離成靜態資原始檔後壓縮率會比在可執行檔案裡高很多。

通過掃描查詢無用程式碼
掃描無用程式碼的基本思路都是查詢已經使用的方法/類和所有的類/方法,然後從所有的類/方法當中剔除已經使用的方法/類剩下的基本都是無用的類/方法,但是由於 Objective-C 是動態語言,可以使用字串來呼叫類和方法,所以檢查結果一般都不是特別準確,需要二次確認。目前市面上的掃描的思路大致可以分為 3 種:

基於 Clang 掃描,
基本思路是基於 clang AST。追溯到函式的呼叫層級,記錄所有定義的方法/類和所有呼叫的方法/類,再取差集。具體原理參考 如何使用 Clang Plugin 找到專案中的無用程式碼,目前只有思路沒有現成的工具。

基於可執行檔案掃描
Mach-O 檔案中的 (__DATA,__objc_classlist) 段表示所有定義的類, (__DATA.__objc_classrefs) 段表示所有引用的類(繼承關係是在 __DATA.__objc_superrefs 中);使用的方法和引用的方法也是類似原理。因此我們使用 otool 等命令逆向可執行檔案中引用到的類/方法和所有定義的類/方法,然後計算差集。
基於原始碼掃描
一般都是對原始碼檔案進行字串匹配。例如將 A *a、[A xxx]、NSStringFromClass(“A”)、objc_getClass(“A”) 等歸類為使用的類,@interface A : B 歸類為定義的類,然後計算差集。
基於原始碼掃描 有個已經實現的工具 - fui,但是它的實現原理是查詢所有 #import “A” 和所有的檔案進行比對,所以結果相對於上面的思路來說可能更不準確。

2.1.1 排查無用類
1.可以先查詢無用的OC類,查詢無用oc類有兩種方式,一種是類似於查詢無用資源,通過搜尋"[ClassName alloc/new"、"*ClassName “、”[ClassName class]"等關鍵字在程式碼裡是否出現。另一種是通過otool命令逆向__DATA.__objc_classlist段和__DATA.__objc_classrefs段來獲取當前所有oc類和被引用的oc類,兩個集合相減就是無用oc類。

此外,使用otool命令可檢視__DATA.__objc_classrefs段和__DATA.__objc_classlist段,兩者的差集可以認為是定義了但未使用的類。
不過__DATA.__objc_classrefs段和__DATA.__objc_classlist段中都只提供了類在二進位制檔案中的位置地址,而沒有提供類名等可讀資訊。所以在獲取到差集後,還需要結合

命令的輸出,將地址轉換成可讀的類名。
使用指令碼篩選出差集對應的類後,還需要進行一遍人工選擇。因為動態使用的類、從nib或storyboard初始化的類以及在同一個檔案中定義的多個類會被誤判為未使用的類。這需要結合業務進行一次梳理。
2.1.2 排查無用方法
所有已經被實現的方法可以通過linkmap來獲取,對linkmap做grep操作即可獲得結果:

而所有已經被使用的方法可以通過對二進位制檔案逆向獲得。使用otool工具逆向二進位制檔案的__DATA.__objc_selrefs 段,提取可執行檔案裡引用到的方法名:

使用這種方法取到的差集,還需要排除掉系統API中的protocol,accessor方法等。
另外以往C++在連結時,沒有被用到的類和方法是不會編進可執行檔案裡。但Objctive-C不同,由於它的動態性,它可以通過類名和方法名獲取這個類和方法進行呼叫,所以編譯器會把專案裡所有OC原始檔編進可執行檔案裡,哪怕該類和方法沒有被使用到。
結合LinkMap檔案的__TEXT.__text,通過正則表示式([+|-][.+\s(.+)]),我們可以提取當前可執行檔案裡所有objc類方法和例項方法(SelectorsAll)。再使用otool命令otool -v -s __DATA __objc_selrefs逆向__DATA.__objc_selrefs段,提取可執行檔案裡引用到的方法名(UsedSelectorsAll),我們可以大致分析出SelectorsAll裡哪些方法是沒有被引用的(SelectorsAll-UsedSelectorsAll)。注意,系統API的Protocol可能被列入無用方法名單裡,如UITableViewDelegate的方法,我們只需要對這些Protocol裡的方法加入白名單過濾即可。
另外第三方庫的無用selector也可以這樣掃出來的。

2.1.3 extension程式碼精簡
由於Jimu appiOS端最低需要支援iOS7,所以Jimu app中的庫都以靜態庫形式整合。這種整合方式會導致,一旦extension依賴了一些基礎庫,這些基礎庫的佔用的體積將會全部算入extension的體積中。
在審查安裝包內每個檔案時,我們發現Jimu app的today extension佔用了2MB左右,而extension本身的功能非常簡單,顯然存在著精簡的空間。我們對extension進行了重構,使它儘可能的少依賴基礎庫,儘可能所有功能都用系統自帶的框架完成。最終today extension的大小從2MB縮減為了300KB。

  1. 掃描重複程式碼
    可以利用第三方工具simian掃描。南非支付copy程式碼就是這樣被發現的。但除此成果之外,掃描出來的結果過多,重構起來也不方便,不如砍功能需求效果好。
  2. protobuf精簡改造
    protobuf是Google推出的一種輕量高效的結構化資料儲存格式,在微信用於網路協議和本地檔案序列化。但google預設工具生成的程式碼比較冗餘,像序列化、反序列化、計算序列化大小等方法都生成在具體的pb類裡,每個類的實現大同小異。通過程式碼分析以及結合protobuf原理,要想把這些方法抽象到基類,派生類提供每個欄位相關資訊就夠了:
    field number
    field label, optional, required or repeated
    wire type, double, float, int, etc
    是否packed
    repeated的資料型別
    另外通過無用selector列表,發現不少pb類屬性的getter或setter沒有被使用。原先的pb類屬性是用@synthesize修飾,編譯器會自動生成getter和setter。如果不想編譯器生成,則要用@dynamic。甚至我們可以把pb類的成員變數去掉。做法如下:
    基類增加id型別陣列ivarValues(參考了objc_class結構體ivars做法),用於存放物件的屬性值。物件屬性值統一用oc物件表示,如果型別是基礎型別(primitive,如int、float等),則用NSValue存
    過載methodSignatureForSelector:方法,返回屬性getter、setter的方法簽名
    過載forwardInvocation:方法,分析invocation.selector型別。如果是getter,從ivarValues獲取屬性值並設定為invocation的returnValue;如果是setter,從invocation第二個argument獲取屬性值,並存放到ivarValues裡
    過載setValue:forUndefinedKey:、valueForUndefinedKey:,防止通過KVO訪問屬性Crash
    做下效能優化,如pb類在initialize做一次初始化,快取屬性名的hash值,屬性的getter、setter方法的objcType等;屬性值不用std::map(屬性名->屬性值),而是改用陣列;MRC代替ARC(有些時候ARC自動新增的retain/release挺影響效能的);等等

如何進行編譯選項優化
Xcode 支援編譯器層面的一些優化優化選項,可以讓我們介於更快的編譯速度和更小的二進位制大小並且更快的執行速度之間自由選擇想要進行的優化粒度。
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 中進行設定,每個等級的說明,可以參考官方文件
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 可以看到此選項最終的表現形式

Swift Complier/LLVM 編譯優化選項
Swift 語言的編譯器是 swiftlang,同時也是基於 LLVM 後端的。Xcode 9.3 版本之後 Swift 編譯器會提供新的選項來幫助減少 Swift 可執行檔案的大小:
No optimization[-Onone]:不進行優化,能保證較快的編譯速度。
Optimize for Speed[-O]:編譯器將會對程式碼的執行效率進行優化,一定程度上會增加包大小。
Optimize for Size[-Osize]:編譯器會盡可能減少包的大小並且最小限度影響程式碼的執行效率。
Xcode 9.3 以前和優化選項混雜在一起的編譯模式可以獨立設定了:
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 提前獲取編譯器優化的好處。

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

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

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,而不用擔心除錯的時候會影響斷點和符號化,同時打包的時候又會自動去除符號資訊。這個選項也是預設開啟的,較老的專案可以選擇手動開啟。

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 即可。

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,如下圖所示:

BitCode
BitCode 是 iOS 9 引入的新特性,官方文件解釋 BitCode 是一種程式中間碼,其實就是 LLVM IR 的一種編碼形式 - BitCodeFormart。
當我們把攜帶 BitCode 的 App 提交到 AppStore 後,蘋果會提取出可執行檔案中的 BitCode 段,然後針對不同的 CPU 架構編譯和連結成不同的可執行檔案變體(Variant),不同 CPU 架構的裝置會自動選擇合適的架構的變體進行下載。而在 BitCode 之前沒我們都是把所有需要的 CPU 架構集合打包成一個 Fat Binary,結果就是使用者最終下載的安裝包之中有很多冗餘的 CPU 架構支援程式碼。

從以上編譯器架構中我們也可以得出一個結論:開啟 BitCode 之後編譯器後端(Backend)的工作都由 Apple 接管了。所以假如以後蘋果推出了新的 CPU 架構或者以後 LLVM 推出了一系列優化,我們也不再需要為其釋出新的安裝包了。
BitCode 一致性要求
一致性要求意味著工程開啟 BitCode 之後必須要求所有打進 Bundle 的 Binary 都需要支援 BitCode,也就是說我們依賴的靜態庫和動態庫都是含有 BitCode 的,不然就會打包失敗。對於 Cocoapods 等原始碼管理工具來管理的依賴庫來說操作會比較簡單,我們只需要開啟 Pods 工程中的 BitCode 就行。但是對於一些三方的閉源庫,我們就無能為力了。
BitCode的崩潰定位
開啟 BitCode 之後需要特別注意崩潰定位的問題:由於最終的可執行檔案是 Apple 自動生成的,同時產生新的符號表檔案,所以我們使用原本打包生成的 dSYM 符號化檔案是無法完成符號化的。所以我們需要在上傳至 App Store 時需要勾選 Include app symbols for your application to receive symboilcated crash logs from Apple:
勾選之後 Apple 會給我們生成 dSYM,然後就可以在 Xcode -> Organizer 或者 iTunes Connect 中下載對應的 dSYM 來進行符號化了。

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

編譯選項配置

(1) 配置編譯選項 
(Levels選項內)Generate Debug Symbols  設定為NO,這個配置選項應該會讓你減去小半的體積。注意這個如果設定成NO就不會在斷點處停下

Strip Link Product設成YES,app可執行檔案減少0.3M

Make Strings Read-Only設為YES,如果專案從低版本Xcode升級過來,這個編譯選項之前一直為NO,設為YES後可執行檔案減少了3M
將 Build Settings -> Clang/LLVM Generate Code -> Optimize Level 設定為 Fastest, Smallest(-Os)。
將 Build Settings -> Swift/LLVMGenerate Code -> Optimize Level 設定為 Optimize for Size(-Osize)。
將 Build Settings -> Strip Linked Product 和 Strip Swift Symbols 設定為 YES,Deployment Postprocessing 設定為 NO,釋出程式碼的時候也需要勾選 Strip Swift Symbols。
Strip Debug Symbols During Copy 在 Release 模式下設定為 YES
有條件的話,適配 BitCode。
無論在主工程或者 Framework 中都使用 XCAsset 來管理資源。
使用工具掃描刪除無用資源,推薦選擇 FengNiao,並新增 Run Scripts。
使用工具掃描重複資源,推薦選擇 fdupes,並新增 Run Scripts。
如果你大量資源都放在本地,推薦使用 On-Demand Resources 方式來管理資源。
在 Swift ABI 穩定之前 Extension 和主 App 之間共享 Swift Standard Libraries。
開啟 Compress PNG Files/Remove Text Metadata From PNG Files。
將 Dead Code Stripping 設定為 YES。
使用工具掃描和清理無用程式碼,同時養成良好習慣,在迭代或者重構的時候刪除舊的程式碼。
使用工具掃描重複程式碼並重構它。
視訊/音訊/H5 等資源遠端化。
使用 xib/storyboard 來開發檢視介面會一定程式增加安裝包的大小。
使用 Swift 來開發程式會一定程式增加安裝包的大小,對包大小有嚴格要求的話也可以衡量是否使用 Swift。
如果你對包大小有嚴格要求的話,選擇合適大小的三方庫來進行開發。

(2) 捨棄架構armv7
armv7用於支援4s和4,4s是2011年11月正式上線,雖然還有小部分人在使用,但是追求包體大小的完全可以捨棄了。
(3) Build Settings->Optimization Level有幾個編譯優化選項,release版應該選擇Fastest, Smalllest[-Os],這個選項會開啟那些不增加程式碼大小的全部優化,並讓可執行檔案儘可能小。

(4). 去除符號資訊
Strip Debug Symbols During Copy 和 Symbols Hidden by Default 在release版本應該設為yes,可以去除不必要的除錯符號。Symbols Hidden by Default會把所有符號都定義成”private extern”,設了後會減小體積。

(5) Strip Linked Product:DEBUG下設為NO,RELEASE下設為YES,用於RELEASE模式下縮減app的大小;

(6) 編譯器優化,去掉異常支援。Enable C++ Exceptions、Enable Objective-C Exceptions設定為NO,Other C Flags新增-fno-exceptions, 去掉異常支援,Enable C++ Exceptions和Enable Objective-C Exceptions設為NO,並且Other C Flags新增-fno-exceptions,可執行檔案減少了27M,其中__gcc_except_tab段減少了17.3M,__text減少了9.7M,效果特別明顯。可以對某些檔案單獨支援異常,編譯選項加上-fexceptions即可。但有個問題,假如ABC三個檔案,AC檔案支援了異常,B不支援,如果C拋了異常,在模擬器下A還是能捕獲異常不至於Crash,但真機下捕獲不了(有知道原因可以在下面留言:)。去掉異常後,Appstore後續幾個版本Crash率沒有明顯上升。個人認為關鍵路徑支援異常處理就好,像啟動時NSCoder讀取setting配置檔案得要支援捕獲異常,等等

(7), 利用AppCode 檢測未使用的程式碼:選單欄 ->Code->InspectCode
它可以幫助我們查找出 AppCode 中無用的類、無用的方法甚至是無用的 import ,但是無法掃描通過字串拼接方式來建立的類和呼叫的方法,所以說還是上面所說的 基於原始碼掃描 更加準確和安全。
通過掃描的方式去檢查無用程式碼有個痛點就是 類的方法呼叫是一種引用關係,以上所說的四種思路都是查詢到引用末端的未使用的程式碼,我們很難通過一次掃描就定位到所有未使用的類,自動化實現起來也較難。舉個例子來說,假如 A 是一個未使用到的類,但是 A 引用了 B,所以首次檢查結果是 A 未被引用,B 被無用類 A 引用了,我們需要把 A 刪除了之後我們才能瞭解到 B 是否是無用的類。當然如果你重新去實現一個引用樹的話就另當別論了。
由於掃描無用類實現起來較為麻煩,並且其檢查結果也不是特別準確。所以建議還是讓開發者養成一個良好的習慣,在迭代或者重構程式碼的時候把老的程式碼刪除,不要等到量變引起質變的時候才回頭去優化。

(8). Cocoapods中的優化選項配置
Cocoapods 的 project 檔案在每次 pod install 或者 pod update 會重置,所以需要 hook pod install 來設定 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’] =&nbs