今日頭條 iOS 安裝包大小優化—— 新階段、新實踐

{"type":"doc","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"前言"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"今日頭條 iOS 端從 2016 年起就關注到了安裝包大小的問題,並啓動了包大小優化。2017 年,我們將當時的經驗發表爲技術文章 "},{"type":"link","attrs":{"href":"http:\/\/mp.weixin.qq.com\/s?__biz=MzI1MzYzMjE0MQ==&mid=2247484366&idx=1&sn=5a2d0e981c733e9eaec274b835600e67&chksm=e9d0c82cdea7413ada372bb936541c2a49de664b38daa6137c179ab6177231fa8b00ddf9acbc&scene=21#wechat_redirect","title":"","type":null},"content":[{"type":"text","text":"《乾貨|今日頭條iOS端安裝包大小優化—思路與實踐》"}]},{"type":"text","text":"[1]。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如今三年過去了。今日頭條在繼續探索包大小優化時實踐了更多思路,包括構建配置、圖片壓縮、__TEXT 段遷移、二進制段壓縮等。這些優化項在業務入侵較少的前提下給今日頭條帶來了顯著的包大小收益。同時,整個業界在包大小優化上也產出了更多方案。因此我們更新文章,期待與大家共同交流包大小優化這件事。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/55\/556c02e01f7d3dfb857a453236bfdb0d.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"表格:今日頭條落地的優化項和收益一覽"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"一、安裝包的構成"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當我們通過構建,獲得了一個經過了 App Slicing 後的 ipa 文件後,將其用 zip 解壓縮方式解壓,進入 .app 文件後,我們就可以直觀地看到安裝包中的內容。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/7e\/7e4c84b09b7760e1a884d576bfc922ee.webp","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一個安裝包,往往包含資源與 iOS 上的可執行文件 Mach-O 文件兩部分,資源又可以分爲 Asset Catalog 的構建產物 Assets.car 文件和其他資源。其中 Assets.car 文件和 Mach-O 文件,是我們投入較多精力優化的部分。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"1.1、Assets.car 文件"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Assets.car 文件是工程中 Asset Catalog 的構建產物。Xcode 工具鏈中的 actool 負責構建 Assets.car。在構建 Assets.car 的過程中,actool 會按照一定策略選取編碼算法,對其中的 png 圖片重新編碼。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/24\/2472e24da8164f8fbb1ff9a02791e47a.webp","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"圖:Asset Catalog"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"1.2、Mach-O 文件"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Mach-O 文件是 iOS 上的可執行文件,它是由代碼源文件經過編譯和靜態鏈接獲得。經過 App Slicing 之後的 Mach-O 文件往往僅包含單個架構。使用 MachOView 等工具,我們可以直觀瞭解 Mach-O 中包含的內容。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/da\/da1318b8fb4378fb6caa4d1a60a8e94f.webp","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"同時,Link Map 文件能更進一步幫助我們分析 Mach-O 文件的構成。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/4c\/4c077d1ec82d86c83eae64fcc36ed603.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在 Build Settings 中打開 "},{"type":"codeinline","content":[{"type":"text","text":"LD_GENERATE_MAP_FILE"}]},{"type":"text","text":" 開關,構建 App 的過程中就會生成一個名叫 Link Map 的 txt 文件,它能展示每個段、每個節、每個函數在 Mach-O 中的分佈和大小。這些信息是包大小優化中經常使用的。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/e7\/e75dd3d03700855b8f18d05121aec630.webp","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"二、資源大小優化"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"“壓縮資源”往往是最容易被聯想到的包大小優化方案,但實際操作起來,卻也包含技巧。今日頭條在資源優化上做了諸多嘗試。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"2.1、使用合適的資源壓縮配置"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"今日頭條目前最低支持的 iOS 系統版本爲 iOS 9。然而,大部分 Pod 庫的 Podspec 文件中指定的"},{"type":"codeinline","content":[{"type":"text","text":"deployment_target"}]},{"type":"text","text":"(最低支持版本)由於未及時修改,依然還是 iOS 8,這就導致了這些 Pod 庫中指定的 "},{"type":"codeinline","content":[{"type":"text","text":"resource_bundles"}]},{"type":"text","text":" 在構建出 Assets.car 時,是以 iOS 8 爲最低支持版本的。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們通過實驗發現:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"1、將 Pod 庫和主工程的最低支持版本從 iOS 8.0 提升成 iOS 9.0"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"2、開啓 Pod 庫和主工程 Xcode Build Settings 中的 "},{"type":"codeinline","content":[{"type":"text","text":"ASSETCATALOG_COMPILER_OPTIMIZATION space"}]},{"type":"text","text":" 選項"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/1e\/1e6e3f04fb195c3e4ee553a9c2015f02.webp","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這兩項設置可以改變 actool 構建 Assets.car 時選取的編碼壓縮算法,減小包大小。我們可以使用 "},{"type":"codeinline","content":[{"type":"text","text":"xcrun assetutil --info Assets.car"}]},{"type":"text","text":" 命令檢查 Assets.car 中每張圖片使用的編碼壓縮算法。在今日頭條環境下,整理的結果如下:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/3c\/3c6225887f06c0a8c46124392794a110.webp","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"由於 Assets.car 中 png 圖片的編碼壓縮算法得到了改變,這兩項配置在今日頭條落地時獲得了 2.31MB 的包大小收益。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"2.2、使用 RGB with palette 壓縮圖片"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在今日頭條投入包大小優化的早期,我們曾嘗試對 Asset Catalog 中的 png 圖片做無損壓縮,但實踐後發現,雖然放入 Asset Catalog 的圖片大小有了明顯減小,但是構建的產物的大小卻幾乎沒有變化。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"經過探究,我們發現,Xcode 中,構建 Asset Catalog 的工具 actool 會首先對 Asset Catalog 中的 png 圖片進行解碼,得到 Bitmap 數據,然後再運用 actool 的編碼壓縮算法進行編碼壓縮處理。無損壓縮通過變換圖片的編碼壓縮算法減少大小,但是不會改變 Bitmap 數據。對於 actool 來說,它接收的輸入沒有改變,所以無損壓縮無法優化 Assets.car 的大小。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"那是否有其他的壓縮方式能優化 Assets.car 的大小呢?我們猜測對圖片做合適的有損壓縮是一個思路。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"於是我們嘗試了 RGB with palette 編碼方式[2]。RGB with palette 編碼的得到的字節流首先維護了一個顏色數組。顏色數組每個成員用 RGBA 四個分量維護一個顏色。圖像中的每個像素點則存儲顏色數組的下標代表該點的顏色。顏色數組維護的顏色種類和數量由圖片決定,同時可以人爲的限制顏色數組維護顏色的種類的上限,默認爲最大值 256 種。這種編碼方式正如它的名字:palette(調色板)。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/14\/14dea1c78c1b7509b71cc6e549d630d5.webp","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"App 中大部分圖片雖然使用了很多種類的顏色,但這些顏色中大多數都非常接近,從視覺上很難分辨,比如大量扁平風格的 icon。這種類型的圖片非常適合用 palette 編碼且減少顏色數組大小的方式來進行有損壓縮,既能減少顏色數量實現有損壓縮,也能保證保留的顏色貼近原始圖片,使得經過有損壓縮後的也看起來質量無損。我們在今日頭條上落地,獲得了 3.15MB 包大小收益。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在具體執行中,我們使用了 ImageOptim 工具改變圖片的編碼方式爲 RGB with palette :"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"imageoptim -Q --no-imageoptim --imagealpha --number-of-colors 16 --quality 40-80 .\/1.png\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"其中 "},{"type":"codeinline","content":[{"type":"text","text":"--number-of-colors"}]},{"type":"text","text":" 控制顏色數組維護顏色的數量;"},{"type":"codeinline","content":[{"type":"text","text":"--quality"}]},{"type":"text","text":" 控制圖片的質量變爲原來的百分比。我們的經驗表明,當 "},{"type":"codeinline","content":[{"type":"text","text":"--number-of-colors"}]},{"type":"text","text":" 從 16 開始向上調整,"},{"type":"codeinline","content":[{"type":"text","text":"--quality"}]},{"type":"text","text":" 維持 40-80,能夠在顯著減少包大小的同時維持肉眼看不到的質量變化。經過 UI 同學的像素眼審查,確認優化前後的圖片看起來無差別。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/1f\/1f0db5a62314eb1b6a723d57b19a9358.webp","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"2.3、Assets.car 合併"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"今日頭條使用 CocoaPods 進行組件集成,各個組件攜帶的 Asset Catalog 文件以 Podspec 中 resource_bundles 的方式引入,最終會以 Bundle 下的 Assets.car 文件的形式體現在安裝包內。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"以 7.9.4 版本爲例,安裝包內有 106 個 Bundle 包含 Assets.car 文件:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/a3\/a307256c3da27136a8036ba7a98ec154.webp","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Assets.car 文件本質上是 BOM 文件,同時,Xcode 在使用 actool 構建 Assets.car 文件時,也會自帶一些優化操作,比如:將若干張小圖片自動合併爲一張 Packed Image。因此,將若干個 Assets.car 合併,可以減少重複的 BOM Block,也可以最大化享受到 actool 自帶的優化效果。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在構建的過程中,今日頭條通過在 Build Phases 中加入腳本,將多個庫中 Asset Catalog 中的圖片合併到一個 Asset Catalog 中,再經 actool 構建成 Assets.car 產物。這一優化產生了 2.1MB 的包大小收益。同時,從理論上分析,這一優化也可以減少運行時 Assets.car 的解析操作,對圖片讀取的響應耗時有正向收益。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"2.4、文本文件壓縮"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"除了佔比最大的圖片資源,今日頭條安裝包內還有不少文本文件資源,如 JSON 文件、HTML 文件等。這些文本文件的壓縮也能帶來包大小優化效果。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"今日頭條落地的文本文件壓縮方案由三部分組成:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"1、壓縮階段:在 Build Phase 中添加腳本,構建期間對白名單內的文本文件做 zip 壓縮;"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"2、解壓階段:在 App 啓動階段,在異步線程中進行解壓操作,將解壓產物存放到沙盒中;"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"3、讀取階段:在 App 運行時,hook 讀取這些文件的方法,將讀取路徑從 Bundle 改爲沙盒中的對應路徑;"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這一方案能在業務入侵較少的前提下完成壓縮優化。我們首先將這一方案應用在了 Lottie 動畫的 JSON 文件上,產生了 400KB 的包大小收益。後續這一方案也可以進一步拓展,應用在更多類型的文件上。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"三、Mach-O 文件優化"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在資源優化的同時,我們也關注到,Mach-O 文件始終佔據了今日頭條安裝包 80% 左右的體積。Mach-O 文件的優化必不可少。下面我們以時間順序,介紹我們落地的 Mach-O 文件優化項。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"3.1、使用 -Oz 編譯參數"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Oz 是 Xcode 11 新增的編譯優化選項。WWDC 2019 《What's New in Clang and LLVM》[3] 中對 Oz 有過介紹。Oz 的核心原理是對重複的連續機器指令外聯成函數進行復用,和“內聯函數”的原理正好相反。因此,開啓 Oz,能減小二進制的大小,但同時理論上會帶來執行效率的額外消耗。對性能(CPU)敏感的代碼使用需要評估。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"蘋果給的參考數據是 4.5% 的包體積收益。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們在評估了執行效率、堆棧解析、穩定性和編譯速度後,對大部分源代碼開啓了 Oz 編譯,包體積減小 4MB 以上。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"3.2、使用鏈接時優化 LTO"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/7e\/7ecd0ac913b3b267202100cada720389.webp","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Link-Time Optimization 鏈接時優化,是 Xcode 自帶的一個編譯\/鏈接參數。根據 WWDC 2016 《What's New in LLVM》[4]的介紹,LTO 對包大小和運行效率都有正向影響。今日頭條在編譯和鏈接中均開啓 Incremental LTO 後,包體積減小 6.5MB。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"3.3、修正 Exported Symbols 配置"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/cb\/cbdec87d887d1474efbfdad5f028b2cf.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Xcode Build Settings 中的 "},{"type":"codeinline","content":[{"type":"text","text":"EXPORTED_SYMBOLS_FILE"}]},{"type":"text","text":" 配置,控制着 Mach-O 中 "},{"type":"codeinline","content":[{"type":"text","text":"__LINKEDIT"}]},{"type":"text","text":" 段中 Export Info 的信息。動態鏈接器 dyld 在做符號綁定時,會讀取被綁定的動態庫或可執行文件的 Export Info 信息,得到一個符號對應的實際調用地址。如果正在被綁定的符號,在目標動態庫的 Export Info 中缺失,dyld 則會拋出異常,表現爲 App 崩潰。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"雖然從原理上看,Export Info 中的信息不可或缺。但是,對於一個 Mach-O 文件來說,並非所有的符號都是需要暴露給其他動態庫或可執行文件的。理想情況下,私有的符號應該在編碼時就應該以 "},{"type":"codeinline","content":[{"type":"text","text":"__attribute__((visibility(hidden)))"}]},{"type":"text","text":" 修飾。但在歷史代碼難以逐個添加修飾符的情況下,Exported Symbols 配置給了工程一個維護公有符號白名單的機會。如果填寫了有效的 "},{"type":"codeinline","content":[{"type":"text","text":"EXPORTED_SYMBOLS_FILE"}]},{"type":"text","text":" 配置,動態庫或者可執行文件會在靜態鏈接時去掉白名單以外的符號,起到縮減包大小、增加逆向難度的作用。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"今日頭條在使用 Exported Symbols 配置後,包大小減少了 2.1MB。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"3.4、屬性動態化"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"屬性是 OC 中最常見的概念之一。然而,一個屬性並沒有我們想象的這麼小。通過分析 Mach-O 文件,我們發現,一個屬性可以分爲三個部分:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"(1)成員變量部分:成員變量本質是一個大小 32B 的結構體,結構體中三個指針(Offset、Name、Type)指向的內容的大小分別爲 8B、10B、10B,其中 Name、Type 指針指向的內容的大小和成員變量的類型、名字長度相關。總大小大約 60B。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"@interface presentViewController ()\n@property (nonatomic,strong) UIImageView *imageView;\n@property (nonatomic,strong) UIButton *button;\n@property (nonatomic,strong) NSString *name;\n@end\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/c7\/c72401a301bcd99a80da0330b276e067.webp","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"(2)自動生成的 set\/get 方法部分:set\/get 方法本質是一個大小 24B 的結構體,結構體包含三個指針 Name、Type、Implementation,指向的內容大小大概爲 10B、10B、20B。一個方法大小大概是64B,set、get 兩個方法就是 128B。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/07\/078b56ec962f84d6273982a859f0cd78.webp","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"(3)property 部分:property 的本質仍然是個結構體,大小是 16B,結構體中兩個指針指向內容的大小分別大概是 10B、10B,和屬性的名字和類型相關。總大小大概 36B。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/ca\/ca21df4d45e5ba740c5376a007c6d8cd.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"即一個屬性佔用的包大小大約爲 224B。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果我們用 "},{"type":"codeinline","content":[{"type":"text","text":"@dynamic"}]},{"type":"text","text":" 修飾一個屬性,不生成成員變量、get\/set 方法,則一個屬性可以由 224B 減少到 36B,即僅包含 property 部分的大小。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"同時,代碼中存在大量通過腳本自動生成的 "},{"type":"codeinline","content":[{"type":"text","text":"JSONModel"}]},{"type":"text","text":" 子類,這些子類往往擁有大量屬性。這裏也就存在着包大小優化空間。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"於是我們通過修改生成 "},{"type":"codeinline","content":[{"type":"text","text":"JSONModel"}]},{"type":"text","text":" 子類的腳本,實現了:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"1、屬性全部使用 "},{"type":"codeinline","content":[{"type":"text","text":"@dynamic"}]},{"type":"text","text":" 修飾,基礎變量額外生成 IVAR"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"2、所有 "},{"type":"codeinline","content":[{"type":"text","text":"JSONModel"}]},{"type":"text","text":" 的子類繼承自新的父類,新的父類實現 "},{"type":"codeinline","content":[{"type":"text","text":"resolveInstanceMethod"}]},{"type":"text","text":",在該方法中用 "},{"type":"codeinline","content":[{"type":"text","text":"class_addMethod"}]},{"type":"text","text":" 統一爲屬性添加 get\/set 方法。對象類型的屬性使用關聯對象的方式存取,基礎類型的屬性使用額外生成的 IVAR 存取。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這一優化獲得了 800KB 的包大小收益,並且評估對讀寫的性能影響損耗可以接受。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"3.5、__TEXT 段遷移"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"安裝包經過壓縮後的 Download Size 若超過 200 MB,在蜂窩網絡下載 App 就會受到限制,這對新增會有較大影響。在 2020 年下半年,我們探索實踐了 __TEXT 段遷移技術:在鏈接階段使用 "},{"type":"codeinline","content":[{"type":"text","text":"-rename_section"}]},{"type":"text","text":" 選項將 "},{"type":"codeinline","content":[{"type":"text","text":"__TEXT,__text"}]},{"type":"text","text":" 遷移到 "},{"type":"codeinline","content":[{"type":"text","text":"__BD_TEXT"}]},{"type":"text","text":","},{"type":"codeinline","content":[{"type":"text","text":"__text"}]},{"type":"text","text":",減少蘋果對可執行文件的加密範圍,提升可執行文件的壓縮效率,從而減少 Download Size。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"使用該方案我們最終減少了 60 MB 的 Download Size 以及 2 MB 的 Install Size。詳細的原理可以參考:"},{"type":"link","attrs":{"href":"http:\/\/mp.weixin.qq.com\/s?__biz=MzI1MzYzMjE0MQ==&mid=2247487459&idx=1&sn=3dd9276f5af78ca5a377adec37e3e916&chksm=e9d0c401dea74d17e9f1bdd5ea764cc0cd7e845c6ebadde752d36608306b09e762a1681c7252&scene=21#wechat_redirect","title":"","type":null},"content":[{"type":"text","text":"《今日頭條優化實踐:iOS 包大小二進制優化,一行代碼減少 60 MB 下載大小》"}]},{"type":"text","text":"[5]。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"3.6、二進制段壓縮"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Mach-O 文件佔據了 Install Size 中很大一部分比例,但並不是文件中的每個段\/節在程序啓動的第一時間都要被用到。可以在構建過程中將 Mach-O 文件中的這部分段\/節壓縮,然後只要在這些段被使用到之前將其解壓到內存中,就能達到了減少包大小的效果,同時也能保證程序正常運行。由於蘋果的一些限制,我們目前只壓縮了 "},{"type":"codeinline","content":[{"type":"text","text":"__TEXT,__gcc_except_tab"}]},{"type":"text","text":" 與 "},{"type":"codeinline","content":[{"type":"text","text":"__TEXT,__objc_methtype"}]},{"type":"text","text":"兩個節,然後在 "},{"type":"codeinline","content":[{"type":"text","text":"_dyld_register_func_for_add_image"}]},{"type":"text","text":" 的回調中對它進行解壓。該方案累計優化了 3.5 MB Install Size。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"四、總結"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在以上優化項落地的同時,我們還與業務協作,通過挖掘無用代碼、無用資源等手段,進一步優化着安裝包大小。使得今日頭條在高速的業務迭代下,包大小仍能保持穩定。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"參考資料"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"[1] "},{"type":"link","attrs":{"href":"http:\/\/mp.weixin.qq.com\/s?__biz=MzI1MzYzMjE0MQ==&mid=2247484366&idx=1&sn=5a2d0e981c733e9eaec274b835600e67&chksm=e9d0c82cdea7413ada372bb936541c2a49de664b38daa6137c179ab6177231fa8b00ddf9acbc&scene=21#wechat_redirect","title":"","type":null},"content":[{"type":"text","text":"乾貨|今日頭條iOS端安裝包大小優化—思路與實踐 "}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"[2] Palette Images"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"http:\/\/www.manifold.net\/doc\/mfd9\/palette_images.htm"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"[3] WWDC 2019 What's New in Clang and LLVM"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"https:\/\/developer.apple.com\/videos\/play\/wwdc2019\/409\/"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"[4] WWDC 2016 What's New in LLVM"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"https:\/\/developer.apple.com\/videos\/play\/wwdc2016\/405\/"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"[5] "},{"type":"link","attrs":{"href":"https:\/\/www.infoq.cn\/article\/XUJL32hTDKYqAKz0hkMM","title":"","type":null},"content":[{"type":"text","text":"今日頭條優化實踐:iOS 包大小二進制優化,一行代碼減少 60 MB 下載大小"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"本文轉載自:字節跳動技術團隊(ID:outiaotechblog)"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"原文鏈接"},{"type":"text","text":":"},{"type":"link","attrs":{"href":"https:\/\/mp.weixin.qq.com\/s\/oyqAa8wKdioI5ZDG5LjkfA","title":"xxx","type":null},"content":[{"type":"text","text":"今日頭條 iOS 安裝包大小優化 - 新階段、新實踐"}]}]}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章