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
當向 App Store Connect 上傳 .ipa 後,App Store Connect 建構過程中,會自動分割該 App,建立特定的變體(variant)以适配不同裝置。然後使用者從 App Store 中下載下傳到的安裝包,即這個特定的變體,這一過程叫做 Slicing。
Slicing 是建立、分發不同變體以适應不同目标裝置的過程
而變體之間的差異,又具體展現在架構和資源上。換句話說,App Slicing 僅向裝置傳送與之相關的資源(取決于螢幕分辨率、系統架構等等)
其中,2x 和 3x 的細分,要求圖檔在 Assets 中管理。Bundle 内的則會同時包含。
[外鍊圖檔轉存失敗,源站可能有防盜鍊機制,建議将圖檔儲存下來直接上傳(img-p3BE4E2D-1585661724795)(https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2018-11-15-AppVariant.jpeg)]
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 來進行符号化
那麼 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 的下載下傳而下載下傳,直到使用者真正進入到某個頁面時才下載下傳這些資源檔案。
應用場景:相機應用的貼紙或者濾鏡、關卡遊戲等
如需支援 iOS9 以下系統,那麼無法使用這個功能,否則上傳會失敗
2 包體積
2個概念
- .ipa (iOS Application Package):iOS 應用程式歸檔檔案,即送出到 App Store Connect 的檔案
- .app (Application):應用的具體描述,即安裝到 iOS 裝置上的檔案
當我們拿到 Archive 後的 .ipa,使用解壓軟體打開後,Payload 目錄下存放的就是 .app 檔案,二者大小相當
包體積,評判标準是以 App Store 上看到的為準。但是上傳到 App Store Connect 處理完後,會自動幫我們生成具體裝置上看到的大小。如下:
這其中:又可以分為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步:
- 通過 find 指令擷取 App 安裝包中的所有資源檔案
- 設定用到的資源類型。比如 gif、jpg、jpeg、png、webp
- 使用正則比對出在源碼中使用到的資源名,比如 pattern = @"@"(.+?)""
- 使用 find 指令找到篇所有資源檔案,再去源碼中找到使用到的資源檔案,2個集合的差集就是無用資源了。
- 确認無用資源後可以使用 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 測量耗時。
點選展開
//實驗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;
Timeprofile-imageNamedFromAssets
[外鍊圖檔轉存失敗,源站可能有防盜鍊機制,建議将圖檔儲存下來直接上傳(img-rRYC73hx-1585661724797)(https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-05-07-Timeprofile-imageNamedFromAssets.png)]
TimeProfile-imageWithContentsOfFile
[外鍊圖檔轉存失敗,源站可能有防盜鍊機制,建議将圖檔儲存下來直接上傳(img-FFm3YoXb-1585661724797)(https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-05-07-TimeProfile-imageWithContentsOfFile.png)]
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 的方式進行處理。
[外鍊圖檔轉存失敗,源站可能有防盜鍊機制,建議将圖檔儲存下來直接上傳(img-gTwds9tw-1585661724798)(https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-05-08-Cocopod-Assets.png)]
步驟:
- 在各個 Pod 元件庫裡面的 Resources 目錄下建立 Asset Catalog 檔案,命名為 Images.xcassets
- 将 Resources 裡面零散的圖檔資源拖進 Images.xcassets 裡面
-
修改每個元件庫的 podspec 檔案
點選展開
s.resource_bundles = { 'XQ_UI' => ['XQ_UI/Assets/*.xcassets'] } </details>
- 主工程執行 pod install
話說
resources
和
resource_bundles
都可以使用 Asset Catalog,那麼有何差別?
- resources 隻會将資源檔案 copy 到 target 工程,最後和 target 工程的圖檔資源以及同樣使用該方式的 Pod 庫的圖檔資源共同打包到一個
中。是以圖檔資源會有混亂的可能。Assets.car
- resource_bundles 會生成一個你在
中指定名稱的 bundle,且在 bundle 中也會生成一個 Assets.car。是以圖檔是肯定不會混亂的,但是圖檔的通路方式需要注意。podspec
解決方法:為每個 pod 建立一個圖檔的分類,比如 UIImage+XQUIModule。然後通路圖檔的時候通過
[UIImage xquiModuleImageNamed:@"pull"]
通路。
點選展開
#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
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。并且說在蘋果内部已經廣泛地使用這個優化方法進行編譯。
它的優化主要展現在如下幾個方面:
- 多餘代碼去除(Dead code elimination):如果一段代碼分布在多個檔案中,但是從來沒有被使用,普通的 -O3 優化方法不能發現跨中間代碼檔案的多餘代碼,是以是一個“局部優化”。但是Link-Time Optimization 技術可以在 link 時發現跨中間代碼檔案的多餘代碼。
- 跨過程優化(Interprocedural analysis and optimization):這是一個相對廣泛的概念。舉個例子來說,如果一個 if 方法的某個分支永不可能執行,那麼在最後生成的二進制檔案中就不應該有這個分支的代碼。
- 内聯優化(Inlining optimization):内聯優化形象來說,就是在彙編中不使用 “call func_name” 語句,直接将外部方法内的語句“複制”到調用者的代碼段内。這樣做的好處是不用進行調用函數前的壓棧、調用函數後的出棧操作,提高運作效率與棧空間使用率。
在新的版本中,蘋果使用了新的優化方式 Incremental,大大減少了連結的時間。建議開啟。
總結,開啟這個優化後,一方面減少了彙編代碼的體積,一方面提高了代碼的運作效率。
3.2 代碼瘦身
代碼的優化,即通過删除無用類、無用方法、重複方法等,來達到可執行檔案大小的減小。
而如何篩選出符合條件的無用類、方法,則需要通過一些工具來完成(fui)
掃描無用代碼的基本思路都是查找已經使用的方法/類和所有的類/方法,然後從所有的類/方法當中剔除已經使用的方法/類剩下的基本都是無用的類/方法,但是由于 Objective-C 是動态語言,可以使用字元串來調用類和方法,是以檢查結果一般都不是特别準确,需要二次确認。目前市面上的掃描的思路大緻可以分為 3 種:
- 基于 Clang 掃描
- 基于可執行檔案掃描
- 基于源碼掃描
先談幾個概念。
可執行檔案就是 Mach-O 檔案,其大小是油代碼量決定的,通常情況下,對可執行檔案進行瘦身,就是找到并删除無用代碼的過程。找到無用代碼的過程類比找到無用圖檔的思路。
- 找到類和方法的全集
- 找到使用過的類和方法集合
- 取2者差集得到無用代碼集合
- 工程師确認後,删除即可
LinkMap 檔案分為3部分:Object File、Section、Symbols。
[外鍊圖檔轉存失敗,源站可能有防盜鍊機制,建議将圖檔儲存下來直接上傳(img-fHOP3AQB-1585661724798)(https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-05-06-LinkMap-Structure.png)]
- Object File:包含了代碼工程的所有檔案
- Section:描述了代碼段在生成的 Mach-O 裡的偏移位置和大小
- Symbols:會列出每個方法、類、Block,以及它們的大小
先說說如何快速找到方法和類的全集?
我們可以通過 LinkMap 來獲得所有的代碼類和方法的資訊。擷取 LinkMap 可以通過将 Build Setting 裡面的 Write Link Map File 設定為 YES,然後指定 Path to Link Map File 的路徑就可以得到每次編譯後的 LinkMap 檔案了。
3.2.1 基于 clang 掃描
基本思路是基于 clang AST。追溯到函數的調用層級,記錄所有定義的方法/類和所有調用的方法/類,再取差集。具體原理參考 如何使用 Clang Plugin 找到項目中的無用代碼,目前隻有思路沒有現成的工具。
3.2.2 基于可執行檔案掃描(LinkMap 結合 Mach-O 找無用代碼)
上面我們得知可以通過 LinkMap 統計出所有的類和方法,還可以清晰地看到代碼所占包大小的具體分布,進而有針對性地進行代碼優化。
[外鍊圖檔轉存失敗,源站可能有防盜鍊機制,建議将圖檔儲存下來直接上傳(img-t3xKxYss-1585661724799)(https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-05-05-LinkMap-ObjectFile.png)]
[外鍊圖檔轉存失敗,源站可能有防盜鍊機制,建議将圖檔儲存下來直接上傳(img-s7kUmx9L-1585661724800)(https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-05-05-LinkMap-Sections.png)]
[外鍊圖檔轉存失敗,源站可能有防盜鍊機制,建議将圖檔儲存下來直接上傳(img-eQK9SWbh-1585661724800)(https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-05-05-LinkMap-Symbols.png)]
得到了代碼的全集資訊後,我們還需要找到已經使用過的方法和類,這樣才可以擷取差集,找到無用代碼。是以接下來就談談如何通過 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 如何檢視呢?
- 使用 otool 等指令逆向可執行檔案中引用到的類/方法和所有定義的類/方法,然後計算差集。具體參考iOS微信安裝包瘦身,目前隻有思路沒有現成的工具。
- 使用 MachOView 檢視。但是這個項目運作不起來,這個新的 Repo 可以運作起來。
下面舉例說明:
前置條件:先運作項目,在生成的 Products 目錄下的 BridgeLabiPhone.app 解壓,取出對應的和工程同名的 BridgeLabiPhone。然後運作上面的 Github 項目。可以看到運作了一個 Mac App。點選頂部的菜單欄裡面的 File->Open。選擇電腦上的 BridgeLabiPhone.app 選擇裡面的 BridgeLabiPhone。見下圖
[外鍊圖檔轉存失敗,源站可能有防盜鍊機制,建議将圖檔儲存下來直接上傳(img-vybAdg7b-1585661724800)(https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-05-07-Mach-O-Inspect.png)]
由于 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檢測出了實際上需要的大部分場景的問題,但是由于 Objective-C 是一門動态性語言,是以 AppCode 檢測出無用的方法等都需要工程師自己再次确認後删除。(在我們的工程中有一些和 H5 互動的橋接方法,是以 AppCode 視為 Unused Method,但是你删除的話,那就自己哭去吧 😭)。實際經驗告訴我,使用 AppCode 的時候如果工程比較大,則整個 code inspect 會非常耗時(給你打個預防針哦,筆芯)
- 無用類:Unused class 是無用類,Unused import statement 是無用類引入聲明,Unused property 是無用的屬性;
- 無用方法:Unused method 是無用的方法,Unused parameter 是無用參數,Unused instance variable 是無用的執行個體變量,Unused local variable 是無用的局部變量,Unused value 是無用的值;
- 無用宏:Unused macro 是無用的宏。
- 無用全局:Unused global declaration 是無用全局聲明。
3.2.5 運作時真正檢測類是否用過
通過上述手段找到并删除了無用代碼。App 不斷上線疊代蠻多代碼都不會被調用了(業務被砍掉了)。這種方式下這些無用的代碼也是可以被删除的。
通過 Objective-C 的 runtime 源碼,我們可以找到如何判斷一個類是否初始化過的函數。
#define RW_INITIALIZED (1<<29)
bool isInitialized() {
return getMeta()->data()->flags & RW_INITIALIZED;
}
isInitialized 的結果會儲存到元類的 class_rw_t 結構體的 flags 資訊裡, flags 的 1<<29 位記錄的就是這個類是否初始化了的資訊,而 flags 的其他位記錄的資訊,可以檢視 rumtime 的源碼
// 類的方法清單已修複
#define RW_METHODIZED (1<<30)
// 類已經初始化了
#define RW_INITIALIZED (1<<29)
// 類在初始化過程中
#define RW_INITIALIZING (1<<28)
// class_rw_t->ro 是 class_ro_t 的堆副本
#define RW_COPIED_RO (1<<27)
// 類配置設定了記憶體,但沒有注冊
#define RW_CONSTRUCTING (1<<26)
// 類配置設定了記憶體也注冊了
#define RW_CONSTRUCTED (1<<25)
// GC:class 有不安全的 finalize 方法
#define RW_FINALIZE_ON_MAIN_THREAD (1<<24)
// 類的 +load 被調用了
#define RW_LOADED (1<<23)
既然可以在運作的期間知道類是否初始化了,那麼就可以找出哪些類未初始化,即可以找到在真實環境裡面沒有用到的類并删除掉。
4. App Extension
App Extension 的占用,都放在 Plugin 檔案夾内。它是獨立打包簽名,然後再拷貝進 Target App Bundle 的。
關于 Extension,有兩個點要注意:
靜态庫最終會打包進可執行檔案内部,是以如果 App Extension 依賴了三方靜态庫,同時主工程也引用了相同的靜态庫的話,最終 App 包中可能會包含兩份三方靜态庫的體積。
動态庫是在運作的時候才進行加載連結的,是以 Plugin 的動态庫是可以和主工程共享的,把動态庫的加載路徑 Runpath Search Paths 修改為跟主工程一緻就可以共享主工程引入的動态庫。
是以,如果可能的話,把相關的依賴改成動态庫方式,達到共享。
5. 靜态庫瘦身
項目中都會引入第三方靜态庫。通過 lipo 工具可以檢視支援的指令集,比如檢視微網誌 SDK
終端切換到微網誌 SDK 的目錄下執行下面指令
- 靜态庫指令集資訊檢視:
lipo -info libname.a(或者libname.framework/libname)
lipo -info libWeiboSDK.a
//Architectures in the fat file: libWeiboSDK.a are: armv7 arm64 i386 x86_64
我們知道 i386、x86_64 是模拟器的指令集。是以我們可以模拟器版本的指令集。因為 armv7 也可以相容 armv7s。是以 armv7s 也可以删除了。隻保留 armv7 和 arm64
- 靜态庫拆分:
lipo 靜态庫檔案路徑 -thin CPU架構 -output 拆分後的靜态庫檔案路徑
- 靜态庫合并:
lipo -create 靜态庫1檔案路徑 靜态庫2檔案路徑... 靜态庫n檔案路徑 -output 合并後的靜态庫檔案徑
lipo libWeiboSDK.a -thin armv7 -output libWeiboSDK-armv7.a
lipo libWeiboSDK.a -thin arm64 -output libWeiboSDK-arm64.a
lipo create libWeiboSDK-armv7.a libWeiboSDK-arm64.a -output libWeiboSDK.device.a
通過上面的操作我們将靜态庫裡面支援模拟器的指令集給去掉了,是以模拟器是無法跑代碼的,如何解決?
- 平時使用包含模拟器指令集的靜态庫,在 App 釋出的時候去掉
- 如果使用 Cocoapods 管理可以使用2份 Podfile 檔案。一份包含指令集一份不包含,釋出的時候切換 Podfile 檔案即可。或者一份 Podfile 檔案,但是配置不同的環境設定
補充2個說明:
-
dSYM 檔案
符号表檔案 .dSYM 檔案是從 Mach-O 檔案中抽取調試資訊而得到的檔案目錄,實際用于儲存調試資訊的是 DWARF 檔案
- 自動生成。Xcode 會在工程編譯或者歸檔的時候自動生成 .dSYM 檔案,在 Buld setting 設定中有開關可以設定去關掉 .dSYM 檔案
- 手動生成。通過腳本從 Mach-O 檔案中提取出來。
$ /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/dsymutil /Users/wangzz/Library/Developer/Xcode/DerivedData/YourApp-cqvijavqbptjyhbwewgpdmzbmwzk/Build/Products/Debug-iphonesimulator/YourApp.app/YourApp -o YourApp.dSYM
該方式通過 dsymutil 工具,從項目編譯結果 .app 目錄下的 Mach-O 檔案中提取出調試符号表檔案。Xcode 在歸檔的時候是通過它生辰的 .dSYM 檔案
-
DWARF 檔案
DebuggingWith Arbitrary Record Formats 是 ELF 和 Mach-O 等檔案格式中用來存儲和處理調試資訊的标準格式,.dSYM 檔案中真正儲存符号表資料的是 DWARF 檔案。DWARF 檔案中不同的資料都儲存在相應的 section 中。
最後的一個對比效果圖:
[外鍊圖檔轉存失敗,源站可能有防盜鍊機制,建議将圖檔儲存下來直接上傳(img-Xcp75scx-1585661724801)(https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-05-09-AppThinning-Comparation.png)]
總結:瘦身技術常見操作就這些,但是維持應用包體積的瘦身卻是一個觀念,從日常開發到線上釋出都需要有這個意識。這樣當你在寫代碼的時候就會考慮同樣一個效果,你的具體實作手段是怎麼樣的。比如為了一個稍微炫酷的效果就要引入一個很大的三方庫,有了“瘦身”的意識,你很大可能就是自己動手撸一個代碼。比如一些無用資源的管理方式、有用的圖檔資源的高效管理方式等等。有了意識,行動自然會往這個方面去靠。(😂大道理一套一套的。我也不想的,畢竟是playboy)
其中遇到了一個神奇的問題。lint 的時候看到一些未使用的依賴庫。見 問題
By the way:
如果在應用包瘦身方面有其他的做法,請告知,完善文章。
參考文章:
- Humble Assets Catalog
- 關于 Pod 庫的資源引用 resource_bundles or resources