1. 程式人生 > >iOS 瘦身之道

iOS 瘦身之道

App 的包大小做優化的目的就是為了節省使用者流量,提高使用者的下載速度,也是為了使用者手機節省更多的空間。另外 App Store 官方規定 App 安裝包如果超過 150MB,那麼不可以使 OTA(over-the-air)環境下載,也就是隻可以在 WiFi 環境下載,企業或者獨立開發者萬萬不想看到這一點。免得失去大量的使用者。

同時如果你的 App 需要適配 iOS7、iOS8 那麼官方規定主二進位制 text 段的大小不能超過 60MB。如果不能滿足這個標準,則無法上架 App Store。

另一種情況是 App 包體積過大,對使用者更新升級率也會有很大影響。

所以應用包的瘦身迫在眉睫。

1. App Thinning

App Thinning 是指 iOS9 以後引入的一項優化,官方描述如下

The App Store and operating system optimize the installation of iOS, tvOS, and watchOS apps by tailoring app delivery to the capabilities of the user’s particular device, with minimal footprint. This optimization, called app thinning, lets you create apps that use the most device features, occupy minimum disk space, and accommodate future updates that can be applied by Apple. Faster downloads and more space for other apps and content provides a better user experience.

Apple 會盡可能,自動降低分發到具體使用者時,所需要下載的 App 大小。其中包含三項主要功能:Slicing、Bitcode、On-Demand Resources。

App Thinning 是蘋果公司推出的一項改善 App 下載程序的新技術,主要為了解決使用者下載 App 耗費過高流量的問題,同時還可以節省使用者裝置儲存空間。

1.1 Slicing

Slicing

當向 App Store Connect 上傳 .ipa 後,App Store Connect 構建過程中,會自動分割該 App,建立特定的變體(variant)以適配不同裝置。然後使用者從 App Store 中下載到的安裝包,即這個特定的變體,這一過程叫做 Slicing。

Slicing 是建立、分發不同變體以適應不同目標裝置的過程

而變體之間的差異,又具體體現在架構和資源上。換句話說,App Slicing 僅向裝置傳送與之相關的資源(取決於螢幕解析度、系統架構等等)

其中,2x 和 3x 的細分,要求圖片在 Assets 中管理。Bundle 內的則會同時包含。

變體

1.2 Bitcode

Bitcode is an intermediate representation of a compiled program. Apps you upload to iTunes Connect that contain bitcode will be compiled and linked on the App Store. Including bitcode will allow Apple to re-optimize your app binary in the future without the need to submit a new version of your app to the App Store.

Bitcode 是一種程式中間碼。包含 Bitcode 配置的程式將會在 App Store Connect 上被重新編譯和連結,進而對可執行檔案做優化。這部分都是在服務端自動完成的。所以假如以後 Apple 新推出了新的 CPU 架構或者以後 LLVM 推出了一系列優化,我們不需要重新為其釋出新的安裝包了。Apple Store 會為我們自動完成這步。然後提供對應的 variant 給具體裝置

對於 iOS 而言,Bitcode 是可選的(Xcode7 以後建立的新專案預設開啟),watchOS、tvOS 則是必須的。

開啟位置:Build Settings -> Enable Bitcode -> 設定為 YES

開啟 Bitcode,有這麼2點需要注意:

  • 全部都要支援。我們所依賴的靜態庫、動態庫、Cocoapods 管理的第三方庫,都需要開啟 Bitcode。否則會編譯失敗

  • 奔潰定位。開啟 Bitcode 後最終生成的可執行檔案是 Apple 自動生成的,同時會產生新的符號表檔案,所以我們無法使用自己包生成的 dYSM 符號化檔案來進行符號化。

For Bitcode enabled builds that have been released to the iTunes store or submitted to TestFlight, Apple generates new dSYMs. You’ll need to download the regenerated dSYMs from Xcode and then upload them to Crashlytics so that we can symbolicate crashes.For Bitcode enabled apps, ensure that you have checked “Include app symbols for your application…” so that we can provide the most accurate crash reports.

上面是 fabric 中關於 Downloading Bitcode dYSMs 的描述:

在上傳到 App Store 時需勾選“Includ app symbols for your application...”。勾選之後 Apple 會自動生成對應的 dYSM,然後可以在 Xcode -> Window -> Organizer 中,或者在 Apple Store Connect 中下載對應的 dYSM 來進行符號化

App Connect-dYSM

Xcode-dYSM

那麼 Bitcode 會對 App Thining 有什麼作用?

在 New Features in Xcode7 中有這麼一段描述:

Bitcode. When you archive for submission to the App Store, Xcode will compile your app into an intermediate representation. The App Store will then compile the bitcode down into the 64 or 32 bit executables as necessary.

即,App Store 會再按需將這個 bitcode 編譯進 32/64 位的可執行檔案。 所以網上鋪天蓋地地說 Bitcode 完成了具體架構的拆分,從而實現瘦包

1.3 on-Demand Resources

on-Demand Resource 即一部分圖片可以被放置在蘋果的伺服器上,不隨著 App 的下載而下載,直到使用者真正進入到某個頁面時才下載這些資原始檔。

on-DemandResources

應用場景:相機應用的貼紙或者濾鏡、關卡遊戲等

如需支援 iOS9 以下系統,那麼無法使用這個功能,否則上傳會失敗

2 包體積

2個概念

  • .ipa (iOS Application Package):iOS 應用程式歸檔檔案,即提交到 App Store Connect 的檔案

  • .app (Application):應用的具體描述,即安裝到 iOS 裝置上的檔案

當我們拿到 Archive 後的 .ipa,使用解壓軟體開啟後,Payload 目錄下存放的就是 .app 檔案,二者大小相當

包體積,評判標準是以 App Store 上看到的為準。但是上傳到 App Store Connect 處理完後,會自動幫我們生成具體裝置上看到的大小。如下:

App Store 包大小

這其中:又可以分為2類: Universal 和具體裝置 Universal 指通用裝置,即未應用 App slicing 優化,同時包含了所有架構、資源。所以包體積會比較大

觀察 .ipa 的大小和 Universal 對應的包大小相當,稍微小一點,因為 App Store 對 .ipa 做了加密處理

有時候下載 App 會提示“此專案大於 150MB,除非此專案支援增量下載,否則您必須連線至 WiFi 才能下載”。150MB 針對的是下載大小。

  • 下載大小:通過 WiFi 下載的壓縮 App 大小
  • 安裝大小:此 App 將在使用者裝置上佔用磁碟空間的大小

所以我們要瘦包,關鍵在於減小 .app 檔案的大小。

2.1 Architectures

如果不支援32位以及 iOS8 ,去掉 armv7 ,可執行檔案以及庫會減小,即本地 .ipa 也會減小

2.2 Resources

資源的優化也就是平時的細心與審查。

圖片、內建素材、Bundle、多語言、Json、字型、指令碼、Plist、音訊

圖片:Assets.car Bundle: 非放在 Asset Catlog 中管理的圖片資源。包括 Bundle,散落的 png、jpg 等

瘦包具體的方式:

  • 無用資源的刪除
  • 重複檔案的刪除
  • 大檔案壓縮
  • 圖片管理方式規範
  • on-Demand Resource(遊戲的、前置關卡依賴、濾鏡App 等的依賴資源,建議用這種方式動態下載圖片資源)

2.2.1 無用檔案的刪除

無用檔案主要包含:無用圖片、無用非圖片部分。

非圖片部分:資源較少,使用方式固定。比如音訊、字型。需要手動排查 圖片部分:主要使用一個開源的 Mac App LSUnusedResources 進行冗餘圖片的排查。

刪除無用的圖片過程,可以概括為下面6步:

  1. 通過 find 命令獲取 App 安裝包中的所有資原始檔
  2. 設定用到的資源型別。比如 gif、jpg、jpeg、png、webp
  3. 使用正則匹配出在原始碼中使用到的資源名,比如 pattern = @"@"(.+?)""
  4. 使用 find 命令找到篇所有資原始檔,再去原始碼中找到使用到的資原始檔,2個集合的差集就是無用資源了。
  5. 確認無用資源後可以使用 NSFileManager 進行檔案的刪除。

如果不想重新寫一個工具,那麼可以直接使用開源的工具 LSUnusedResources

但是存在一點問題。會出現誤報,因為不同的專案,圖片使用方式不一樣。

- (BOOL)containsSimilarResourceName:(NSString *)name {
    NSString *regexStr = @"([-_]?\\d+)";
    NSRegularExpression* regexExpression = [NSRegularExpression regularExpressionWithPattern:regexStr options:NSRegularExpressionCaseInsensitive error:nil];
    NSArray* matchs = [regexExpression matchesInString:name options:0 range:NSMakeRange(0, name.length)];
    //...
}

原始碼中的正則表示式處理的情況並不是很準確。可以根據自己的情況修改正則即可

2.2.2 圖片資源的壓縮

刪除了無用的資源,那麼對於資源這塊還是有操作的空間的,比如圖片資源的壓縮。目前壓縮比較好的方案就是 WebP,它是谷歌公司的一個開源專案。

WebP 的優勢:

  • 壓縮率高。支援有損和無損2種方式,比如將 Gif 圖可以轉換為 Animated WebP,有損模式下可以減小 64%,無損模式下可以減小 19%
  • WebP 支援 Alpha 透明和 24-bit 顏色數,不會像 PNG8 那樣因為色彩不夠出現毛邊。

Google 公司在開源 WebP 的同時,還提供了一個圖片壓縮工具 cwebp。 壓縮完之後使用 WebP 格式的圖片還需使用 libwebp 進行解析,參考這個Demo

缺點:WebP 在 CUP 消耗和解碼時間上會比 PNG 高2倍,所以我們做選擇的時候需要取捨。

2.2.3 重複檔案刪除

重複檔案,即兩個內容完全一致的檔案。但是檔案命名不一樣。

藉助 fdupes 這個開源工具,校驗各資源的 MD5。

fdupes 是 Linux 下的一個工具,它由 Adrian Lopez 用 C 語言編寫並基於 MIT 許可證發行,該應用程式可以在指定的目錄及子目錄中查詢重複的檔案。fdupes 通過對比檔案的 MD5 簽名,以及逐位元組比較檔案來識別重複內容,fdupes 有各種選項,可以實現對檔案的列出、刪除、替換為檔案副本的硬連結等操作。

檔案對比從以下順序開始: 大小對比 > 部分 MD5 簽名對比 > 完整 MD5 簽名對比 > 逐位元組對比

執行結束後會在命令列展示出來,所以需要我們人工將這些檔案確認對比後刪除掉。

2.2.4 大檔案壓縮

圖片本身的壓縮,建議使用 ImageOptim。它整合了 Win、Linux 上諸多著名圖片處理工具的特色,比如 PNGOUT、AdvPNG、Pngcrush、OptiPNG、JpegOptim、Gifsicle 等。 Bundle 內的圖片資源必須壓縮,因為 Xcode 並不會對其進行壓縮。所以做好將圖片都用 Assets 管理。

Xcode 提供給我們2個編譯選項來幫助壓縮影象:

  • Compress PNG Files: 打包的時候自動對圖片進行無失真壓縮。使用的工具為 pngcrush,壓縮比蠻高。
  • Remove Text Medadata From PNG Files:移除 PNG 資源的文字字元,比如影象名稱、作者、版權、創作時間、註釋等資訊

2.2.5 圖片管理方式規範

2.2.5.1 主工程中的圖片管理

工程中所有使用的 Asset Catlog 管理的圖片(在 .xcassets 資料夾下)最終都會輸出到 Asset.car 內。不在 Asset.car 內的都歸為 Bundle 管理。

  • xcassets 裡面的圖片。只能通過 imageNamed 載入。 Bundle 裡面的圖片還可以通過 imageWithContentsOfFile 等方式
  • xcassets 裡面的 @2x、@3x 會根據具體裝置分發,不會同時包含。Bundle 都包含(不進行 App Slicing)
  • xcassets 內可以對圖片進行 Slicing,即裁剪和拉伸、Bundle 不支援
  • Bundle 內支援多語言,Images.xcassets 不支援

使用 imageNamed 建立的 UIImage 會被立即加入到 NSCache 中(解碼後的 Image Buffer),直到收到記憶體警告的時候才會釋放不使用的 UIImage。而 imageWithContentsOfFile 會每次重新申請記憶體,相同圖片不會快取,所以 xcassets 內的圖片,載入後會產生快取

綜上:常用的、較小的圖建議存放在 Images.xcassets 內管理。大圖放在 Bundle 內管理。

這裡講一個插曲了,曾經很多文章都在談一個結論,那就是「圖片放在 Images.xcassets 裡面更加快速且節省空間,直接放在 bundle 裡面會比較慢」。我做過實驗,實驗環境和結論如下。使用 Instruments 測量耗時。

<details> <summary>點選展開</summary>

//實驗1
NSMutableArray *images = [NSMutableArray array];
for (NSInteger index = 0; index < 10; index++) {
    UIImage *image = [UIImage imageNamed:@"icon-iOS"];
    [images addObject:image];
}
self.imageView.image = images.lastObject;
//實驗2
NSMutableArray *images = [NSMutableArray array];
for (NSInteger index = 0; index < 10; index++) {
    NSString *imagePath = [[NSBundle mainBundle] pathForResource:@"iOS" ofType:@"png"];
    [UIImage imageNamed:@"icon-iOS"];
    UIImage *image = [UIImage imageWithContentsOfFile:imagePath];
    [images addObject:image];
}
self.imageView.image = images.lastObject;

</details>

Timeprofile-imageNamedFromAssets Timeprofile-imageNamedFromAssets

TimeProfile-imageWithContentsOfFile TimeProfile-imageWithContentsOfFile

Timeprofile-UIImageNamedFromFolder Timeprofile-UIImageNamedFromFolder

Images.xcassets :

  • 圖片大小要精確,不要出現圖片太大的情況
  • 不要存放大圖,不然會產生快取
  • 不要存 jpg 圖片,打包會變大
  • 圖片不需要額外壓縮(有人做過實驗,對放入 assets 裡面的圖片進行壓縮後打包發現包體積反而增大,懷疑是 Xcode 的編譯選項 Compress PNG Files 自動對圖片進行壓縮,2種壓縮起了衝突反而增大)
2.2.5.2 各個 pod 庫中的圖片管理

CocoPods 中兩種資源引用方式介紹下:

  • resource_bundles

    We strongly recommend library developers to adopt resource bundles as there can be name collisions using the resources attribute. 允許定義當前的 pod 庫的最遠包的名稱和檔案。用 hash 形式宣告,key 是 bundle 的名稱,value 是需要包含檔案的通配 patterns CocoPods 官方強烈推薦該方法引用資源,因為 key-value 可以避免相同資源的名稱衝突

  • resources

    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 optimised by Xcode. 使用該方法引用資源,被指定的資源會被拷貝進 target 工程的 main bundle 中。

說說專案中的情況吧:在工程中之前是通過 resource_bundles 引用資源的。資源是放在 Resources 目錄下的圖片引用。查詢資料後說「如果圖片資源放到 .xcasset 裡面 Xcode 會幫我們自動優化、可以使用 Slicing 等(這裡不僅僅指的是 resource_bundle 下的 xcassets」。所以動手將各個 Pod 庫裡面的圖片全都通過 Assets Catalog 的方式進行處理。

Pod元件庫圖片處理前後對比

步驟:

  • 在各個 Pod 元件庫裡面的 Resources 目錄下新建 Asset Catalog 檔案,命名為 Images.xcassets

  • 將 Resources 裡面零散的圖片資源拖進 Images.xcassets 裡面

  • 修改每個元件庫的 podspec 檔案

    <details>

    <summary>點選展開</summary>

    s.resource_bundles = {
        'XQ_UI' => ['XQ_UI/Assets/*.xcassets']
    }
    </details>
    
  • 主工程執行 pod install

話說 resourcesresource_bundles 都可以使用 Asset Catalog,那麼有何區別?

  • resources 只會將資原始檔 copy 到 target 工程,最後和 target 工程的圖片資源以及同樣使用該方式的 Pod 庫的圖片資源共同打包到一個 Assets.car 中。因此圖片資源會有混亂的可能。
  • resource_bundles 會生成一個你在 podspec 中指定名稱的 bundle,且在 bundle 中也會生成一個 Assets.car。所以圖片是肯定不會混亂的,但是圖片的訪問方式需要注意。

解決方法:為每個 pod 新建一個圖片的分類,比如 UIImage+XQUIModule。然後訪問圖片的時候通過 [UIImage xquiModuleImageNamed:@"pull"] 訪問。

<details> <summary>點選展開</summary>

#import "UIImage+XQUIModule.h"
#import <SDGBase/UIImage+Bundle.h>

@implementation UIImage (XQUIModule)

+ (nonnull UIImage *)xquiModuleImageNamed:(nonnull NSString *)name
{
    return [UIImage imageNamed:name inBundleName:@"XQ_UI"];
}
@end

//UIImage+Bundle.m
#import "UIImage+Bundle.h"

@implementation UIImage (Bundle)

+ (nullable UIImage *)imageNamed:(NSString *)name inBundleName:(nullable NSString *)bundleName {
    NSBundle *bundle = [NSBundle bundleWithURL:[[NSBundle mainBundle] URLForResource:bundleName withExtension:@"bundle"]];
    return  [UIImage imageNamed:name inBundle:bundle compatibleWithTraitCollection:nil];
}
@end

</details>

2.2.6 向量圖的使用

事實上,對於 App 裡面的單色圖示,比如左上角的返回按鈕、底部的 tabBar等,只要是單色的純色圖示都是可以使用向量圖代替的,比如 PDF、ttf 字型圖示等。這樣就不需要新增 @2x、@3x 圖示,節省了空間。

iOS 中如何使用 ttf 向量圖,可以檢視這個 Repo

3. Executable file

3.1 編譯選項優化

3.1.1 Generate Debug Symbols

Enables or disables generation of debug symbos. When debug symbols are enabled, the level of detail can be controller by the build 'Level of Debug Symbols' Setting.

除錯符號是在編譯時形成的。當 Generate Debug Symbols 選項為 YES 的時,每個原始檔在編譯成 .o 檔案時,編譯引數多了 -g 和 -gmodules 兩項。打包會生成 symbols 檔案。設定為 NO 則 ipa 中不會生成 symbol 檔案,可以減少 ipa 大小。但會影響到崩潰的定位。保持預設的開啟,不做修改。

3.1.2 Asset Catalog Compiler

optimization 選項設定為 space 可以減少包大小 預設選項,不做修改。

3.1.3 Dead Code Stripping

For statically linked executables, dead-code stripping is the process of removing unreferenced code from the executable file. If the code is unreferenced, it must not be used and therefore is not needed in the executable file. Removing dead code reduces the size of your executable and can help reduce paging.

刪除靜態連結的可執行檔案中未引用的程式碼

Debug 設定為 NO, Release 設定為 YES 可減少可執行檔案大小。

Xcode 預設會開啟此選項,C/C++/Swift 等靜態語言編譯器會在 link 的時候移除未使用的程式碼,但是對於 Objective-C 等動態語言是無效的。因為 Objective-C 是建立在執行時上面的,底層暴露給編譯器的都是 Runtime 原始碼編譯結果,所有的部分應該都是會被判別為有效程式碼。

預設選項,不做修改。

3.1.4 Apple Clang - Code Generation

Optimization Level 編譯引數決定了程式在編譯過程中的兩個指標:編譯速度和記憶體的佔用,也決定了編譯之後可執行結果的兩個指標:速度和檔案大小。 Build Settings -> code Generation -> Optimization Level 預設情況下,Debug 設定為 None[-O0] ,Release 設定為 Fastest,Smallest[-Os]。

  • None[-O0]。 Debug 預設級別。不進行任何優化,直接將原始碼編譯到執行檔案中,結果不進行任何重排,編譯時比較長。主要用於除錯程式,可以進行設定斷點、改變變數 、計算表示式等除錯工作。

  • Fast[-O,O1]。最常用的優化級別,不考慮速度和檔案大小權衡問題。與-O0級別相比,它生成的檔案更小,可執行的速度更快,編譯時間更少。

  • Faster[-O2]。在-O1級別基礎上再進行優化,增加指令排程的優化。與-O1級別相,它生成的檔案大小沒有變大,編譯時間變長了,編譯期間佔用的記憶體更多了,但程式的執行速度有所提高。

  • Fastest[-O3]。在-O2和-O1級別上進行優化,該級別可能會提高程式的執行速度,但是也會增加檔案的大小。

  • Fastest Smallest[-Os]。Release 預設級別。這種級別用於在有限的記憶體和磁碟空間下生成儘可能小的檔案。由於使用了很好的快取技術,它在某些情況下也會有很快的執行速度。

  • Fastest, Aggressive Optimization[-Ofast]。 它是一種更為激進的編譯引數, 它以點浮點數的精度為代價。

預設選項,不做修改。

3.1.5 Swift Compiler - Code Generation

Xcode 9.3 版本之後 Swift 編譯器提供了新的 Optimization Level 選項來幫助減少 Swift 可執行檔案的大小:

  • No optimization[-Onone]:不進行優化,能保證較快的編譯速度。
  • Optimize for Speed[-O]:編譯器將會對程式碼的執行效率進行優化,一定程度上會增加包大小。
  • Optimize for Size[-Osize]:編譯器會盡可能減少包的大小並且最小限度影響程式碼的執行效率。

We have seen that using -Osize reduces code size from 5% to even 30% for some projects. But what about performance? This completely depends on the project. For most applications the performance hit with -Osize will be negligible, i.e. below 5%. But for performance sensitive code -O might still be the better choice.

官方提到,-Osize 根據專案不同,大致可以優化掉 5% - 30% 的程式碼空間佔用。 相比 -0 來說,會損失大概 5% 的執行時效能。 如果你的專案對執行速度不是特別敏感,並且可以接受輕微的效能損失,那麼 -Osize 是首選。

除了 -O 和 -Osize, 還有另外一個概念也值得說一下。 就是 Single File 和 Whole Module 。 在之前的 XCode 版本,這兩個選項和 -O 是連在一起設定的,Xcode 9.3 中,將他們分離出來,可以獨立設定:

Single File 和 Whole Module 這兩個模式分別對應編譯器以什麼方式處理優化操作。

  • Single File:逐個檔案進行優化,它的好處是對於增量編譯的專案來說,它可以減少編譯時間,對沒有更改的原始檔,不用每次都重新編譯。並且可以充分利用多核 CPU,並行優化多個檔案,提高編譯速度。但它的缺點就是對於一些需要跨檔案的優化操作,它沒辦法處理。如果某個檔案被多次引用,那麼對這些引用方檔案進行優化的時候,會反覆的重新處理這個被引用的檔案,如果你專案中類似的交叉引用比較多,就會影響效能。

  • Whole Module: 將專案所有的檔案看做一個整體,不會產生 Single File 模式對同一個檔案反覆處理的問題,並且可以進行最大限度的優化,包括跨檔案的優化操作。缺點是,不能充分利用多核處理器的效能,並且對於增量編譯,每次也都需要重新編譯整個專案。

如果沒有特殊情況,使用預設的 Whole Module 優化即可。 它會犧牲部分編譯效能,但的優化結果是最好的。

故,在 Relese 模式下 -Osize 和 Whole Module 同時開啟效果會最好!

3.1.6 Strip Symbol Information

1、Deployment Postprocessing 2、Strip Linked Product 3、Strip Debug Symbols During Copy 4、Symbols hidden by default

設定為 YES 可以去掉不必要的符號資訊,可以減少可執行檔案大小。但去除了符號資訊之後我們就只能使用 dSYM 來進行符號化了,所以需要將 Debug Information Format 修改為 DWARF with dSYM file。

Symbols Hidden by Default 會把所有符號都定義成”private extern”,詳細資訊見官方文件。

故,Release 設定為 YES,Debug 設定為 NO。

3.1.7 Exceptions

在 iOS微信安裝包瘦身 一文中,有提到:

去掉異常支援,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配置檔案得要支援捕獲異常,等等

看這個優化效果,感覺發現了新大陸。關閉後驗證.. 毫無感知,基本沒什麼變化。

可能和專案中用到比較少有關係。故保持開啟狀態。

3.1.8 Link-Time Optimization

Link-Time Optimization 是 LLVM 編譯器的一個特性,用於在 link 中間程式碼時,對全域性程式碼進行優化。這個優化是自動完成的,因此不需要修改現有的程式碼;這個優化也是高效的,因為可以在全域性視角下優化程式碼。

蘋果在 WWDC 2016 中,明確提出了這個優化的概念,What’s New in LLVM。並且說在蘋果內部已經廣泛地使用這個優化方法進行編譯。

它的優化主要體現在如下幾個方面:

  1. 多餘程式碼去除(Dead code elimination):如果一段程式碼分佈在多個檔案中,但是從來沒有被使用,普通的 -O3 優化方法不能發現跨中間程式碼檔案的多餘程式碼,因此是一個“區域性優化”。但是Link-Time Optimization 技術可以在 link 時發現跨中間程式碼檔案的多餘程式碼。

  2. 跨過程優化(Interprocedural analysis and optimization):這是一個相對廣泛的概念。舉個例子來說,如果一個 if 方法的某個分支永不可能執行,那麼在最後生成的二進位制檔案中就不應該有這個分支的程式碼。

  3. 內聯優化(Inlining optimization):內聯優化形象來說,就是在彙編中不使用 “call func_name” 語句,直接將外部方法內的語句“複製”到呼叫者的程式碼段內。這樣做的好處是不用進行呼叫函式前的壓棧、呼叫函式後的出棧操作,提高執行效率與棧空間利用率。

在新的版本中,蘋果使用了新的優化方式 Incremental,大大減少了連結的時間。建議開啟。

總結,開啟這個優化後,一方面減少了彙編程式碼的體積,一方面提高了程式碼的執行效率。

3.2 程式碼瘦身

程式碼的優化,即通過刪除無用類、無用方法、重複方法等,來達到可執行檔案大小的減小。 而如何篩選出符合條件的無用類、方法,則需要通過一些工具來完成(fui)

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

  • 基於 Clang 掃描
  • 基於可執行檔案掃描
  • 基於原始碼掃描

先談幾個概念。

可執行檔案就是 Mach-O 檔案,其大小是油程式碼量決定的,通常情況下,對可執行檔案進行瘦身,就是找到並刪除無用程式碼的過程。找到無用程式碼的過程類比找到無用圖片的思路。

  • 找到類和方法的全集
  • 找到使用過的類和方法集合
  • 取2者差集得到無用程式碼集合
  • 工程師確認後,刪除即可

LinkMap 檔案分為3部分:Object File、Section、Symbols。 LinkMap結構

  • Object File:包含了程式碼工程的所有檔案
  • Section:描述了程式碼段在生成的 Mach-O 裡的偏移位置和大小
  • Symbols:會列出每個方法、類、Block,以及它們的大小

先說說如何快速找到方法和類的全集?

我們可以通過 LinkMap 來獲得所有的程式碼類和方法的資訊。獲取 LinkMap 可以通過將 Build Setting 裡面的 Write Link Map File 設定為 YES,然後指定 Path to Link Map File 的路徑就可以得到每次編譯後的 LinkMap 檔案了。 Xcode中設定獲取LinkMap

3.2.1 基於 clang 掃描

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

3.2.2 基於可執行檔案掃描(LinkMap 結合 Mach-O 找無用程式碼)

上面我們得知可以通過 LinkMap 統計出所有的類和方法,還可以清晰地看到程式碼所佔包大小的具體分佈,進而有針對性地進行程式碼優化。

LinkMap-Object file

LinkMap-Sections

LinkMap-Symbols

得到了程式碼的全集資訊後,我們還需要找到已經使用過的方法和類,這樣才可以獲取差集,找到無用程式碼。所以接下來就談談如何通過 Mach-O 取到使用過的類和方法。

Objective-C 中的方法都會通過 objc_msgSend 來呼叫,而 objc_msgSend 在 Mach-O 檔案裡是通過 _objc_selrefs 這個 section 來獲取 selector 這個引數的。

所以,_objc_selrefs 裡的方法一定是被呼叫了的。_objc_classrefs 裡是被呼叫過的類, objc_superrefs 是呼叫過 super 的類(繼承關係)。通過 _objc_classrefs 和 _objc_superrefs,我們就可以找出使用過的類和子類。

那麼,Mach-O 檔案中的 _objc_selrefs、_objc_classrefs、_objc_superrefs 如何檢視呢?

  1. 使用 otool 等命令逆向可執行檔案中引用到的類/方法和所有定義的類/方法,然後計算差集。具體參考iOS微信安裝包瘦身,目前只有思路沒有現成的工具。
  2. 使用 MachOView 檢視。但是這個專案執行不起來,這個新的 Repo 可以執行起來。

下面舉例說明:

前置條件:先執行專案,在生成的 Products 目錄下的 BridgeLabiPhone.app 解壓,取出對應的和工程同名的 BridgeLabiPhone。然後執行上面的 Github 專案。可以看到運行了一個 Mac App。點選頂部的選單欄裡面的 File->Open。選擇電腦上的 BridgeLabiPhone.app 選擇裡面的 BridgeLabiPhone。見下圖

Mach-O-inspect

由於 Objective-C 是一門動態語言,所以檢測出的結果仍舊需要我們2次確認。

3.2.3 基於原始碼掃描

一般都是對原始碼檔案進行字串匹配。例如將 A *a、[A xxx]、NSStringFromClass("A")、objc_getClass("A") 等歸類為使用的類,@interface A : B 歸類為定義的類,然後計算差集。

基於原始碼掃描 有個已經實現的工具 - fui,但是它的實現原理是查詢所有 #import "A" 和所有的檔案進行比對,所以結果相對於上面的思路來說可能更不準確。

3.2.4 通過 AppCode 查詢無用程式碼

AppCode 提供了 Inspect Code 來診斷程式碼,其中含有查詢無用程式碼的功能。它可以幫助我們查找出 AppCode 中無用的類、無用的方法甚至是無用的 import ,但是無法掃描通過字串拼接方式來建立的類和呼叫的方法,所以說還是上面所說的 基於原始碼掃描 更加準確和安全。

AppCode-code inspect

說明:AppCode檢測出了實際上需要的大部分場景的問題,但是由於 Objective-C 是一門動態性語言,所以 AppCode 檢測出無用的方法等都需要工程師自己再次確認後刪除。(在我們的工程中有一些和 H5 互動的橋接方法,因此 AppCode 視為 Unused Method,但是你刪除的話,那就自己哭去吧