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的時候,就不要設定這個。