iOS 包體積優化5 - 編譯優化

語言: CN / TW / HK

iOS包體積優化的系列文章,其中包括: * iOS 包體積優化1 - 總覽 * iOS 包體積優化2 - 如何分析ipa包? * iOS 包體積優化3 - 代碼管理 * iOS 包體積優化4 - 資源管理 * iOS 包體積優化5 - 編譯優化 * iOS 包體積優化6 - 長期維護

一. 指令集優化

1. ARM處理器指令集

ARM架構過去稱作進階精簡指令集機器(Advanced RISC Machine,更早稱作:Acorn RISC Machine),是一個32位精簡指令集(RISC)處理器架構,ARM處理器非常適用於移動通訊領域,符合其主要設計目標:體積小、低功耗、低成本、高性能。ARM指令集是指計算機ARM操作指令系統。

| 指令集 | 對應機型 | | ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | i386 | 模擬器32位處理器 | | x86_64 | 模擬器64位處理器 | | armv6 | iPhone, iPhone 3G, iPod 1G/2G | | armv7 | iPhone 3GS, iPhone 4, iPhone 4S, iPod 3G/4G/5G, iPad, iPad 2, iPad 3, iPad Mini | | armv7s | iPhone 5, iPhone 5c, iPad 4 | | arm64 | iPhone X,iPhone 8(Plus),iPhone 7(Plus),iPhone 6(Plus),iPhone 6s(Plus), iPhone 5s, iPad Air(2), Retina iPad Mini(2,3) | | arm64e | XS/XS Max/XR/ iPhone 11, iPhone 11 pro,iPhone 11 Pro Max,iPhone SE (2nd generation),iPhone 12 mini,iPhone 12,iPhone 12 Pro,iPhone 12 Pro Max,Phone 13 mini,Phone 13,iPhone 13 Pro,iPhone 13 Pro Max |

關於Architectures

architectures:n. 建築;架構(architecture 的複數)。

指定工程支持的指令集的集合,如果設置多個architecture,則生成的二進制數據包會包含多個指令集代碼,體積會變大。

Xcode 14的 Release Notes中提到: Building iOS projects with deployment targets for the armv7, armv7s, and i386 architectures is no longer supported. (92831716)。

不再支持構建 armv7、armv7s 以及 i386 架構的 iOS 項目。

Xcode 14 更新説明文檔

2. Excluded Architectures

Build Settings -> Architectures -> Excluded Architectures

結合自身項目支持情況,項目中不需要支持armv7s和armv7。

Excluded Architectures.png

該編譯項在 Release 下面增加配置項目
  • Any iOS SDK 設置為armv7 / &armv7s
  • `Any iOS Simulator SDK,設置為arm64

這個選項的意思是Release模式下針對真機armv7armv7s指令集排除,針對模擬器把arm64排除(模擬器不支持arm架構)。

二. Build Active Architecture Only配置

Build Settings -> Architectures -> Build Active Architecture Only

該編譯項用於設置是否只編譯當前使用的設備(連線設備)對應的arm指令集

App Clang  Optimization Level.png

該編譯選項設置為
  • Debug 模式設置為 Yes
  • Release 模式設置為 No

三. Optimization Level

優主要用來在 二進制大小運行時性能 做取捨。

1. OC 編譯最佳優化

Xcode -> Build Setting -> Apple Clang - Code Generation -> Optimization Level

Xcode 是使用 Clang 來編譯 Objective-C 語言,我們的 IDE-Xcode 提供給我們 6 個等級的編譯選項(官方説明),用來設置生成的代碼在速度和二進制大小方面的優化程度。

Optimization Level OC.png

| 優化選項 | 説明 | | ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------ | | None[-O0] | 【默認在 Debug 模式下開啟】使用此設置,編譯器的目標是減少編譯的成本,並使調試產生預期的結果,不會優化代碼,提供更快的編譯速度和更多的調試信息, | | Fast[-O,O1] | 【會優化代碼性能並且最小限度影響編譯時間,會佔用更多的內存】對於大型函數,優化編譯需要更多的時間和更多的內存。-O, -O1通過此設置,編譯器試圖減少代碼大小和執行時間,而不執行任何需要大量編譯時間的優化。 | | Faster[-O2] | 【開啟不依賴空間/時間折衷所有優化選項,會增加編譯時間並且提高代碼執行效率】編譯器執行幾乎所有不涉及空間速度權衡的支持優化。在此設置下,編譯器不執行循環展開、函數內聯或寄存器重命名。與Fast設置相比,該設置增加了編譯時間和生成代碼的性能。 | | Fastest[-O3] | 【不推薦使用此模式】編譯器會開啟所有的優化選項來提升代碼執行效率。此模式編譯器會執行函數內聯使得生成的可執行文件會變得更大。 | | Fastest Smallest[-Os] | 【性能和大小平衡較好,默認在 Release 模式下開啟】編譯器會開啟除了會明顯增加包大小以外的所有優化選項。默認-Os是性能和大小平衡比較好的 | | Smallest,Aggressive Size Optimization[-Oz] | 【最小的、激進的大小優化】Oz是Xcode 11之後才出現的編譯優化選項,核心原理是對重複的連續機器指令外聯成函數進行復用,因此開啟Oz,能減少二進制的大小,但同時會帶來執行效率但額外消耗。 | | Fastest, Aggressive Optimization[-Ofast] | 【不推薦使用此模式】啟動 -O3 中的所有優化,可能會開啟一些違反語言標準的一些優化選項。 |

空間/時間折衷

在計算機科學中,時空時內存權衡是通過使用更多的存儲空間(或內存)或通過花費很長時間在極小的空間中解決問題或計算的一種方式。大多數計算機有大量的空間,但不是無限的空間。此外,大多數人願意等待一段時間進行大計算,但不是永遠。因此,如果您的問題需要很長時間,但內存不多,時空權衡將允許您使用更多內存並更快地解決問題。或者,如果它可以很快解決,但需要比你更多的內存,你可以試着花更多的時間在有限的內存中解決問題。

在不同的選項對應的編譯速度和二進制文件大小變化趨勢

Optimization Level不同選項對比.png

Optimization Level默認是-Os-OzXcode 11新增的編譯優化選項,該設置通過將重複的代碼模式隔離到編譯器生成的函數中來實現額外的尺寸節省。

該編譯選項設置為
  • Debug 模式設置為[-O0]
  • Release 模式設置為 [-Oz] 或者[-Os]; (根據實際情況 選擇更偏性能還是包大小)

2. Swift 編譯最佳優化

Xcode -> Build Setting -> Swift Compiler - Code Generation -> Optimization Level

Swift 語言的編譯器是 swiftlang,同時也是基於 LLVM 後端的。Xcode 9.3 版本之後 Swift 編譯器會提供新的選項來幫助減少 Swift 可執行文件的大小. 我們的 IDE-Xcode 提供給我們 3 個等級的編譯選項

Optimization Level Swift.png

| 優化選項 | 説明 | | ------------------------- | ----------------------------------------------------------------------------------------- | | No optimization[-Onone] | 不進行優化,能保證較快的編譯速度 | | Optimize for Speed[-O] | 編譯器將會對代碼的執行效率進行優化,一定程度上會增加包大小 | | Optimize for Size[-Osize] | 編譯器會盡可能減少包的大小並且最小限度影響代碼的執行效率。根據項目不同,大致可以優化掉 5% - 30% 的代碼空間佔用。 相比 -0 來説,會損失大概 5% 的運行時性能。 |

-Osize 根據項目不同,大致可以優化掉 5% - 30% 的代碼空間佔用。 相比 -0 來説,會損失大概 5% 的運行時性能。 如果你的項目對運行速度不是特別敏感,並且可以接受輕微的性能損失,那麼 -Osize 就值得一用。

該編譯選項設置為
  • Debug 模式設置為[-Onone]
  • Release 模式設置為 [-Osize]

四. 編譯模式 Compilation Mode

Xcode -> Build Setting -> Swift Compiler - Code Generation -> Compilation Mode

編譯模式設置,9.3版本之後可以獨立設置了。

Single File 和 Whole Module.png

Incremental(增量)僅編譯已修改的文件.

優點:

  • 在進行增量編譯時,編譯器不必重新編譯整個項目,而只能重新編譯已更改的文件或依賴已更改的文件
  • 編譯器每個文件運行一個實例,因此在具有多個內核的計算機上,它可以編譯得更快

缺點:

  • 如果正在優化的內容跨越多個文件,則不會執行一些優化
  • 編譯器確實必須從其他文件中獲取一些信息,因此它可能會重複此工作超過必要的次數(如果6個文件引用另一個文件,則在只需要1個文件時,該文件可能會對它執行6次一些工作)
Whole Module(全量) 不考慮修改而構建項目中所有文件。

優點:

  • 這將執行快速編譯器可以執行的最大優化
  • 與單文件優化相比,執行更少的宂餘工作

缺點:

  • 這隻會使用一個CPU內核來運行代碼上所有快速的優化。這意味着多核計算機將無法充分利用編譯您的代碼
  • 在增量編譯中,您的整個模塊仍然需要每次都重新編譯
該編譯選項設置為
  • Debug 模式設置為Incremental
  • Release 模式設置為Whole Module

Xcode 10+的默認設置。

五. Link-Time Optimization

Xcode -> Build Settings -> Apple Clang - Code Generation - Link-Time Optimization

LTO(Link-Time Optimization) 就是對整個程序代碼進行的一種優化,是 LLVM 裏在鏈接時進行跨模塊間的優化。

Link-Time Optimization.png

支持的鏈接選項

  • monolithic(adj. 整體的;)

    大型 LTO:這種模式對二進制進行大型的鏈接時優化,合併所有的可執行代碼到一個單元,並且執行更加激進的編譯器優化。大型 LTO 的實現是把所有的輸入合併到一個模塊,並沒有考慮時間和內存的問題,而且還阻礙了增量編譯的執行。

  • incremental(adj. 增加的,遞增的;)

    這個模式可以對二進制執行部分的鏈接時優化,在編譯單元之間進行內聯,並行地在每個單元裏執行更激進的編譯器優化。這個可以允許更快的增量編譯,以及使用更少的內存。

  • No 不做優化

蘋果在WWDC2016對LTO的介紹如下:

What is Link-Time Optimization (LTO)?

Maximize runtime performance by optimizing at link-time Inline functions across source files Remove dead code Enable powerful whole program optimizations

將一些函數內聯化

去除了一些無用代碼

對程序有全局的優化作用

蘋果官方稱他們已經在他們的應用軟件中大量使用LTO,並且相比常規release模式在運行速度上提升了10%,此外它還會使用PGO(按配置優化來優化代碼,並且還能減小代碼體積

Apple uses LTO extensively internally Typically 10% faster than executables from regular Release builds Multiplies with Profile Guided Optimization (PGO) Reduces code size when optimizing for size

這裏也帶來了很明顯的缺點,特別是在有debug info的時候,代碼編譯耗時和更大的內存佔用且二次編譯的時候得全部重新編譯。

LTO trades compile time for runtime performance Large memory requirements Optimizations are not done in parallel Incremental builds repeat all the work

LTO用編譯時間來換取運行時性能 優化不是並行進行的 增量構建重複所有的工作

總結來説: 開啟LTO主要是對鏈接過程的一個優化,並且有link cache,使二次編譯的速度更快,另一方面它還很有可能減小code size。

該編譯選項設置為
  • Debug 模式設置為No
  • Release 模式設置為incremental

六. Asset Catalog Compiler 之 Optimization

Build Settings -> Asset Catalog Compiler - Options -> Optimization

這個選項可以改變actool在構建Assets.car時選取的編碼壓縮算法,減少包大小。

改變actool(使用內置在Xcode中的compile asset catalog工具)在構建Assets.car時會按照一定策略選取編碼算法,對其中的 png 圖片重新編碼, 從而減少包大小。Assets.xcassets 壓縮格式對最終ipa包下assets.car文件大小的影響較大。

Asset Catalog Compiler 之 Optimization.png

可以把對應的信息生成json文件,對比差異

1、打一個ipa包,改為zip格式解壓,進入Payload文件夾。打開終端執行  cd /Users/Desktop/pluto/Payload/pluto.app  ​  2、用find命令定位到Assets.car文件  find . -name 'Assets.car'  ​  3、使用 assetutil 命令導出圖片的信息存儲到Assets.json文件中  sudo xcrun --sdk iphoneos assetutil --info ./Assets.car > /tmp/Assets.json  ​  4、打開生成的Assets.json文件  open /tmp/Assets.json

以項目中這個頭像為例

AccountPhotoIcon.png

可以看到壓縮算法為: "Compression" : "lzfse" (蘋果開源的一種壓縮算法,還有其他的算法)。壓縮的算法不同,佔用空間的大小也不同。

{     "AssetType" : "Image",     "BitsPerComponent" : 8,     "ColorModel" : "RGB",     "Colorspace" : "srgb",     "Compression" : "lzfse",     "Encoding" : "ARGB",     "Idiom" : "universal",     "Name" : "AccountPhotoIcon",     "NameIdentifier" : 46082,     "Opaque" : false,     "PixelHeight" : 96,     "PixelWidth" : 96,     "RenditionName" : "[email protected]",     "Scale" : 2,     "SHA1Digest" : "436897BD6B134BBAE8C0ADE4D57C13AC8266307AC9767672E7B50B35274A86F0",     "SizeOnDisk" : 334,     "State" : "Normal",     "Template Mode" : "automatic",     "Value" : "Off"   }

該編譯選項設置為
  • Debug 模式設置為space
  • Release 模式設置為space

七. Make Strings Read-Only

Xcode -> Build Settings -> Apple Clang - Code Generation - Make Strings Read-Only

複用字符串字面量,顧名思義就是減少生成不必要的量。

 Make Strings Read-Only.png

該編譯選項設置為
  • Debug 模式設置為Yes
  • Release 模式設置為Yes

八. Dead Code Stripping

Xcode -> Build Settings -> Linking -> Make Strings Read-Only

C/C++/Swift 等靜態語言編譯器會在 link 的時候移除未使用的代碼,但是對於 Objective-C 等動態語言是無效的。因為 Objective-C 是建立在運行時上面的,底層暴露給編譯器的都是 Runtime 源碼編譯結果,所有的部分都會被判別為有效代碼。

Dead Code Stripping.png

該編譯選項設置為
  • Debug 模式設置為Yes
  • Release 模式設置為Yes
  • Xcode 默認會開啟此選項

九. 優化調試符號

可執行文件中的符號是指程序中的所有的變量、類、函數、枚舉、變量和地址映射關係,以及一些在調試的時候使用到的用於定位代碼在源碼中的位置的調試符號,符號和斷點定位以及堆棧符號化有很重要的關係。

1. iOS 的調試符號

iOS 的調試符號是 DWARF 格式的,相關概念如下:

  • Mach-O: 可執行文件,源文件編譯鏈接的結果。包含映射調試信息(對象文件)具體存儲位置的 Debug Map。
  • DWARF:一種通用的調試文件格式,支持源碼級別的調試,調試信息存在於對象文件中,一般都比較大。Xcode 調試模式下一般都是使用 DWARF 來進行符號化的。
  • dSYM:獨立的符號表文件,主要用來做發佈產品的崩潰符號化。dSYM 是一個壓縮包,裏面包含了 DWARF 文件。

Math-O 和 DWARF 和 dsUM.png

使用 Xcode 編譯打包的時候會先通過可執行文件的 Debug Map 獲取到所有對象文件的位置,然後使用 dsymutil將對象文件中的 DWARF 提取出來生成 dSYM 文件。

2. Strip Style

Xcode -> Build Settings -> Deployment -> Strip Style

表示的是我們需要去除的符號的類型選項

Strip Style.png

  • All Symbols: 去除所有符號,一般是在主工程中開啟。
  • Non-Global Symbols: 去除一些非全局的 Symbol(保留全局符號,Debug Symbols 同樣會被去除),鏈接時會被重定向的那些符號不會被去除,此選項是靜態庫/動態庫的建議選項。
  • Debug Symbols: 去除調試符號,去除之後將無法斷點調試。

選擇不同的Strip Style時,app構建末尾的Strip操作會被帶上對應的參數。如果選擇debugging symbols的話,函數調用棧中,類名和方法名還是可以看到的。

該編譯選項設置為
  • Debug 模式設置為All Symbols
  • Release 模式設置為All Symbols

3. Deployment Postprocessing (這個設置為Yes 有會報錯: error build: Command PhaseScriptExecution failed with a nonzero exit code)

Xcode -> Build Settings -> Deployment -> Deployment Postprocessing

Deployment Postprocessing是Strip配置的總開關,只有這個設置為YES之後,下面的Strip Linked Product、Strip Debug Symbols During Copy的設置才會生效。

PS:Deployment Postprocessing這個配置項如果使用xcode打包,xcode會默認把這個變量置為YES, 如果使用腳本打包,記得設置。

該編譯選項設置為
  • Debug 模式設置為No
  • Release 模式設置為Yes

4. Strip Linked Product

Xcode -> Build Settings -> Deployment -> Strip Linked Product

並不是所有的符號都是必須的,比如 Debug Map,所以 Xcode 提供給我們 Strip Linked Product 來去除不需要的符號信息(Strip Style 中選擇的選項相應的符號),去除了符號信息之後我們就只能使用 dSYM 來進行符號化了,所以需要將 Debug Information Format 修改為 DWARF with dSYM file

Debug Information Format.png

4.1 疑惑沒有 DWARF 調試信息之後 Xcode 是靠什麼來生成 dSYM 的?

答案其實還是 DWARF,因為 Xcode 編譯實際的操作步驟是:

生成帶有 DWARF 調試信息的可執行文件 -> 提取可執行文件中的調試信息打包成 dSYM -> 去除符號化信息

去除符號是單獨的步驟,使用的是 strip 命令。

4.2 去除符號化信息之後我們只能使用 dSYM 來進行符號化,那我們使用 Xcode 來進行調試的時候會不會太麻煩了?

其實我們完全不用擔心這個問題:Strip Linked Product 選項在 Deployment Postprocessing 設置為 Yes 的時候才生效,而在 Archive 的時候 Xcode 總是會把 Deployment Postprocessing 設置為 YES 。

所以我們可以打開 Strip Linked Product 並且把 Deployment Postprocessing 設置為 NO,而不用擔心調試的時候會影響斷點和符號化,同時打包的時候又會自動去除符號信息。

Strip Linked Product.png

該編譯選項設置為
  • Debug 模式設置為No
  • Release 模式設置為Yes
  • 這個選項是默認打開的;

5. Strip Debug Symbols During Copy

Xcode -> Build Settings -> Deployment -> Strip Debug Symbols During Copy

Strip Linked Product 類似,同樣也是使用的 strip 命令,將那些拷貝進項目包的三方庫、資源或者 Extension 的 Debug Symbol 去除掉。只需要在 Release 模式下開啟,否則會影響三方庫進行斷點調試和符號化。

Strip Debug Symbols During Copy.png

該編譯選項設置為
  • Debug 模式設置為No
  • Release 模式設置為Yes

6. Strip Swift Symbols

Xcode -> Build Settings -> Deployment -> Strip Swift Symbols

能幫助我們移除相應 Target 中的所有的 Swift 符號.

Strip Swift Symbols.png

該編譯選項設置為
  • Debug 模式設置為Yes
  • Release 模式設置為Yes
  • 該項是默認打開的;

十. Symbols Hidden by Default

Xcode -> Build Setting -> Apple Clang - Code Generation -> Symbols Hidden by Default

用於設置符號默認可見性,XCode會把所有符號都定義為private extern,移除符號信息,包大小會略有減少。動態庫設置為NO,否則會有鏈接錯誤。

Symbols Hidden by Default.png

該編譯選項設置為
  • Debug 模式設置為No
  • Release 模式設置為Yes
  • Framework工程 靜態庫/動態庫,設置為NO,否則會有鏈接錯誤。

十一. Debug Information Level

Xcode -> Build Setting -> Apple Clang - Code Generation -> Debug Information Level

切換啟用調試符號時發出的調試信息的數量。這可能會影響生成的調試信息的大小,這在大型項目的某些情況下可能很重要(例如使用LTO時)。

  • Compiler default

  • Line tables only

    這種類型的調試信息允許獲得帶有函數名、文件名和行號的函數調用棧,但是不包含其他數據(比如局部變量和函數參數)。所以當Debug Information Level設置為Line tables only的時候,斷點依然會中斷,但是無法在調試器中查看局部變量的值.

Debug Information Level.png

該編譯選項設置為
  • Debug 模式設置為Compiler default
  • Release 模式設置為Compiler default
  • 這個選項默認就是 Compiler default;

十二. Generate Debug Symbols (不建議)

Xcode -> Build Setting -> Apple Clang - Code Generation -> Generate Debug Symbols

Generate Debug Symbols.png

生成調試符號選項,當這個選項

  • 設置為YES時

    每個源文件在編譯成.o文件時,編譯參數多了-g和-gmodule,意思是generate complete debug info,所以產生的.o文件會大,從而最終生成的可執行文件也就會變大。

  • 設置為NO時

    在Xcode中不能斷點調試。且最後不能生成DSYM文件,即使設置 Debug Information Format設置了,也不能生成。

    因為首先要有調試信息然後才能生成DSYM文件,而設置為NO,意味着不產生調試信息,所以也就沒辦法生成DSYM文件。

該編譯選項設置為
  • 不建議改動,保持設置為 yes

十三. Compress PNG Files

Xcode -> Build Setting -> Compress PNG Files - Packaging -> Compress PNG Files

Compress PNG Files & Remove Text Metadata From PNG Fils.png

當我們在構建過程中,Xcode 會通過自己的壓縮算法重新對圖片進行處理。通過調研知道 Apple 為了在優化 iPhone 設備讀取 png 圖片速度,將 png 轉換成 CgBI 非標準的 png 格式:

  • extra critical chunk (CgBI)

    額外的關鍵數據塊 CgBI

  • byteswapped (RGBA -> BGRA) pixel data, presumably for high-speed direct blitting to the framebuffer

    字節轉換 加快數據交換

  • zlib header, footer, and CRC removed from the IDAT chunk

    移除一些輔助性數據塊

  • premultiplied alpha (color' = color * alpha / 255)

    預乘透明度

在蘋果此項的優化下,一般的壓縮(有損,無損)處理並不能達到很好的瘦身效果,對於大多數應用來説都是包大小的 負優化

該編譯選項設置為
  • 不建議改動,保持設置為 Yes

十四. Remove Text Metadata From PNG Fils

Xcode -> Build Setting -> Compress PNG Files - Packaging -> Remove Text Metadata From PNG Fils

能幫助我們移除 PNG 資源的文本字符,比如圖像名稱、作者、版權、創作時間、註釋等信息。

該編譯選項設置為
  • 保持設置為 Yes

十五. 去掉異常支持 (不建議)

Xcode -> Build Setting -> Apple Clang - Language - C++ -> Enable C++ Exceptions

Xcode -> Build Setting -> Apple Clang - Language - C++ -> Enable Objective-C Exceptions

Xcode -> Build Setting -> Apple Clang - Code Generation -> Other C Flags

1. Enable C++ Excptions 和 Enable Objective-C Exceptions 設置為 No

是指項目支持對錯誤的異常處理。比如try catch、throw之類的;所以如果項目中使用的有類似的異常處理的,這個關閉了之後會報錯(Cannot use '@try' with Objective-C exceptions disabled)。

包括宏定義中使用的有try{}、@finally{}之類的,比如@strongify等,如果關閉了最後打包的時候也會報錯。

2. Other C Flags添加 -fno-exceptions

-fno-exceptions的意思是禁用異常機制,當項目中有try thorw的時候,就不要設置這個。