iOS 瘦身之道
App 的包大小做優(yōu)化的目的就是為了節(jié)省用戶流量,提高用戶的下載速度,也是為了用戶手機節(jié)省更多的空間。另外 App Store 官方規(guī)定 App 安裝包如果超過 150MB,那么不可以使 OTA(over-the-air)環(huán)境下載,也就是只可以在 WiFi 環(huán)境下載,企業(yè)或者獨立開發(fā)者萬萬不想看到這一點。免得失去大量的用戶。
同時如果你的 App 需要適配 iOS7、iOS8 那么官方規(guī)定主二進制 text 段的大小不能超過 60MB。如果不能滿足這個標準,則無法上架 App Store。
另一種情況是 App 包體積過大,對用戶更新升級率也會有很大影響。
所以應(yīng)用包的瘦身迫在眉睫。
1. App Thinning
App Thinning 是指 iOS9 以后引入的一項優(yōu)化,官方描述如下
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 會盡可能,自動降低分發(fā)到具體用戶時,所需要下載的 App 大小。其中包含三項主要功能:Slicing、Bitcode、On-Demand Resources。
App Thinning 是蘋果公司推出的一項改善 App 下載進程的新技術(shù),主要為了解決用戶下載 App 耗費過高流量的問題,同時還可以節(jié)省用戶設(shè)備存儲空間。
1.1 Slicing
當向 App Store Connect 上傳 .ipa 后,App Store Connect 構(gòu)建過程中,會自動分割該 App,創(chuàng)建特定的變體(variant)以適配不同設(shè)備。然后用戶從 App Store 中下載到的安裝包,即這個特定的變體,這一過程叫做 Slicing。
Slicing 是創(chuàng)建、分發(fā)不同變體以適應(yīng)不同目標設(shè)備的過程
而變體之間的差異,又具體體現(xiàn)在架構(gòu)和資源上。換句話說,App Slicing 僅向設(shè)備傳送與之相關(guān)的資源(取決于屏幕分辨率、系統(tǒng)架構(gòu)等等)
其中,2x 和 3x 的細分,要求圖片在 Assets 中管理。Bundle 內(nèi)的則會同時包含。
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 配置的程序?qū)?App Store Connect 上被重新編譯和鏈接,進而對可執(zhí)行文件做優(yōu)化。這部分都是在服務(wù)端自動完成的。所以假如以后 Apple 新推出了新的 CPU 架構(gòu)或者以后 LLVM 推出了一系列優(yōu)化,我們不需要重新為其發(fā)布新的安裝包了。Apple Store 會為我們自動完成這步。然后提供對應(yīng)的 variant 給具體設(shè)備
對于 iOS 而言,Bitcode 是可選的(Xcode7 以后創(chuàng)建的新項目默認開啟),watchOS、tvOS 則是必須的。
開啟位置:Build Settings -> Enable Bitcode -> 設(shè)置為 YES
開啟 Bitcode,有這么2點需要注意:
-
全部都要支持。我們所依賴的靜態(tài)庫、動態(tài)庫、Cocoapods 管理的第三方庫,都需要開啟 Bitcode。否則會編譯失敗
-
奔潰定位。開啟 Bitcode 后最終生成的可執(zhí)行文件是 Apple 自動生成的,同時會產(chǎn)生新的符號表文件,所以我們無法使用自己包生成的 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 中關(guān)于 Downloading Bitcode dYSMs 的描述:
在上傳到 App Store 時需勾選“Includ app symbols for your application...”。勾選之后 Apple 會自動生成對應(yīng)的 dYSM,然后可以在 Xcode -> Window -> Organizer 中,或者在 Apple Store Connect 中下載對應(yīng)的 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 位的可執(zhí)行文件。 所以網(wǎng)上鋪天蓋地地說 Bitcode 完成了具體架構(gòu)的拆分,從而實現(xiàn)瘦包
1.3 on-Demand Resources
on-Demand Resource 即一部分圖片可以被放置在蘋果的服務(wù)器上,不隨著 App 的下載而下載,直到用戶真正進入到某個頁面時才下載這些資源文件。
應(yīng)用場景:相機應(yīng)用的貼紙或者濾鏡、關(guān)卡游戲等
如需支持 iOS9 以下系統(tǒng),那么無法使用這個功能,否則上傳會失敗
2 包體積
2個概念
-
.ipa (iOS Application Package):iOS 應(yīng)用程序歸檔文件,即提交到 App Store Connect 的文件
-
.app (Application):應(yīng)用的具體描述,即安裝到 iOS 設(shè)備上的文件
當我們拿到 Archive 后的 .ipa,使用解壓軟件打開后,Payload 目錄下存放的就是 .app 文件,二者大小相當
包體積,評判標準是以 App Store 上看到的為準。但是上傳到 App Store Connect 處理完后,會自動幫我們生成具體設(shè)備上看到的大小。如下:
這其中:又可以分為2類: Universal 和具體設(shè)備 Universal 指通用設(shè)備,即未應(yīng)用 App slicing 優(yōu)化,同時包含了所有架構(gòu)、資源。所以包體積會比較大
觀察 .ipa 的大小和 Universal 對應(yīng)的包大小相當,稍微小一點,因為 App Store 對 .ipa 做了加密處理
有時候下載 App 會提示“此項目大于 150MB,除非此項目支持增量下載,否則您必須連接至 WiFi 才能下載”。150MB 針對的是下載大小。
- 下載大小:通過 WiFi 下載的壓縮 App 大小
- 安裝大小:此 App 將在用戶設(shè)備上占用磁盤空間的大小
所以我們要瘦包,關(guān)鍵在于減小 .app 文件的大小。
2.1 Architectures
如果不支持32位以及 iOS8 ,去掉 armv7 ,可執(zhí)行文件以及庫會減小,即本地 .ipa 也會減小
2.2 Resources
資源的優(yōu)化也就是平時的細心與審查。
圖片、內(nèi)置素材、Bundle、多語言、Json、字體、腳本、Plist、音頻
圖片:Assets.car Bundle: 非放在 Asset Catlog 中管理的圖片資源。包括 Bundle,散落的 png、jpg 等
瘦包具體的方式:
- 無用資源的刪除
- 重復(fù)文件的刪除
- 大文件壓縮
- 圖片管理方式規(guī)范
- on-Demand Resource(游戲的、前置關(guān)卡依賴、濾鏡App 等的依賴資源,建議用這種方式動態(tài)下載圖片資源)
2.2.1 無用文件的刪除
無用文件主要包含:無用圖片、無用非圖片部分。
非圖片部分:資源較少,使用方式固定。比如音頻、字體。需要手動排查 圖片部分:主要使用一個開源的 Mac App LSUnusedResources 進行冗余圖片的排查。
刪除無用的圖片過程,可以概括為下面6步:
如果不想重新寫一個工具,那么可以直接使用開源的工具 LSUnusedResources
但是存在一點問題。會出現(xiàn)誤報,因為不同的項目,圖片使用方式不一樣。
- (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)];//... } 復(fù)制代碼源碼中的正則表達式處理的情況并不是很準確。可以根據(jù)自己的情況修改正則即可
2.2.2 圖片資源的壓縮
刪除了無用的資源,那么對于資源這塊還是有操作的空間的,比如圖片資源的壓縮。目前壓縮比較好的方案就是 WebP,它是谷歌公司的一個開源項目。
WebP 的優(yōu)勢:
- 壓縮率高。支持有損和無損2種方式,比如將 Gif 圖可以轉(zhuǎn)換為 Animated WebP,有損模式下可以減小 64%,無損模式下可以減小 19%
- WebP 支持 Alpha 透明和 24-bit 顏色數(shù),不會像 PNG8 那樣因為色彩不夠出現(xiàn)毛邊。
Google 公司在開源 WebP 的同時,還提供了一個圖片壓縮工具 cwebp。 壓縮完之后使用 WebP 格式的圖片還需使用 libwebp 進行解析,參考這個Demo。
缺點:WebP 在 CUP 消耗和解碼時間上會比 PNG 高2倍,所以我們做選擇的時候需要取舍。
2.2.3 重復(fù)文件刪除
重復(fù)文件,即兩個內(nèi)容完全一致的文件。但是文件命名不一樣。
借助 fdupes 這個開源工具,校驗各資源的 MD5。
fdupes 是 Linux 下的一個工具,它由 Adrian Lopez 用 C 語言編寫并基于 MIT 許可證發(fā)行,該應(yīng)用程序可以在指定的目錄及子目錄中查找重復(fù)的文件。fdupes 通過對比文件的 MD5 簽名,以及逐字節(jié)比較文件來識別重復(fù)內(nèi)容,fdupes 有各種選項,可以實現(xiàn)對文件的列出、刪除、替換為文件副本的硬鏈接等操作。
文件對比從以下順序開始: 大小對比 > 部分 MD5 簽名對比 > 完整 MD5 簽名對比 > 逐字節(jié)對比
執(zhí)行結(jié)束后會在命令行展示出來,所以需要我們?nèi)斯⑦@些文件確認對比后刪除掉。
2.2.4 大文件壓縮
圖片本身的壓縮,建議使用 ImageOptim。它整合了 Win、Linux 上諸多著名圖片處理工具的特色,比如 PNGOUT、AdvPNG、Pngcrush、OptiPNG、JpegOptim、Gifsicle 等。 Bundle 內(nèi)的圖片資源必須壓縮,因為 Xcode 并不會對其進行壓縮。所以做好將圖片都用 Assets 管理。
Xcode 提供給我們2個編譯選項來幫助壓縮圖像:
- Compress PNG Files: 打包的時候自動對圖片進行無損壓縮。使用的工具為 pngcrush,壓縮比蠻高。
- Remove Text Medadata From PNG Files:移除 PNG 資源的文本字符,比如圖像名稱、作者、版權(quán)、創(chuàng)作時間、注釋等信息
2.2.5 圖片管理方式規(guī)范
2.2.5.1 主工程中的圖片管理
工程中所有使用的 Asset Catlog 管理的圖片(在 .xcassets 文件夾下)最終都會輸出到 Asset.car 內(nèi)。不在 Asset.car 內(nèi)的都歸為 Bundle 管理。
- xcassets 里面的圖片。只能通過 imageNamed 加載。 Bundle 里面的圖片還可以通過 imageWithContentsOfFile 等方式
- xcassets 里面的 @2x、@3x 會根據(jù)具體設(shè)備分發(fā),不會同時包含。Bundle 都包含(不進行 App Slicing)
- xcassets 內(nèi)可以對圖片進行 Slicing,即裁剪和拉伸、Bundle 不支持
- Bundle 內(nèi)支持多語言,Images.xcassets 不支持
使用 imageNamed 創(chuàng)建的 UIImage 會被立即加入到 NSCache 中(解碼后的 Image Buffer),直到收到內(nèi)存警告的時候才會釋放不使用的 UIImage。而 imageWithContentsOfFile 會每次重新申請內(nèi)存,相同圖片不會緩存,所以 xcassets 內(nèi)的圖片,加載后會產(chǎn)生緩存
綜上:常用的、較小的圖建議存放在 Images.xcassets 內(nèi)管理。大圖放在 Bundle 內(nèi)管理。
這里講一個插曲了,曾經(jīng)很多文章都在談一個結(jié)論,那就是「圖片放在 Images.xcassets 里面更加快速且節(jié)省空間,直接放在 bundle 里面會比較慢」。我做過實驗,實驗環(huán)境和結(jié)論如下。使用 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; 復(fù)制代碼Timeprofile-imageNamedFromAssets
TimeProfile-imageWithContentsOfFile
Timeprofile-UIImageNamedFromFolder
Images.xcassets :
- 圖片大小要精確,不要出現(xiàn)圖片太大的情況
- 不要存放大圖,不然會產(chǎn)生緩存
- 不要存 jpg 圖片,打包會變大
- 圖片不需要額外壓縮(有人做過實驗,對放入 assets 里面的圖片進行壓縮后打包發(fā)現(xiàn)包體積反而增大,懷疑是 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 會幫我們自動優(yōu)化、可以使用 Slicing 等(這里不僅僅指的是 resource_bundle 下的 xcassets」。所以動手將各個 Pod 庫里面的圖片全都通過 Assets Catalog 的方式進行處理。
步驟:
-
在各個 Pod 組件庫里面的 Resources 目錄下新建 Asset Catalog 文件,命名為 Images.xcassets
-
將 Resources 里面零散的圖片資源拖進 Images.xcassets 里面
-
修改每個組件庫的 podspec 文件
點擊展開s.resource_bundles = {'XQ_UI' => ['XQ_UI/Assets/*.xcassets'] } </details> 復(fù)制代碼 -
主工程執(zhí)行 pod install
話說 resources 和 resource_bundles 都可以使用 Asset Catalog,那么有何區(qū)別?
- resources 只會將資源文件 copy 到 target 工程,最后和 target 工程的圖片資源以及同樣使用該方式的 Pod 庫的圖片資源共同打包到一個 Assets.car 中。因此圖片資源會有混亂的可能。
- resource_bundles 會生成一個你在 podspec 中指定名稱的 bundle,且在 bundle 中也會生成一個 Assets.car。所以圖片是肯定不會混亂的,但是圖片的訪問方式需要注意。
解決方法:為每個 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 復(fù)制代碼2.2.6 矢量圖的使用
事實上,對于 App 里面的單色圖標,比如左上角的返回按鈕、底部的 tabBar等,只要是單色的純色圖標都是可以使用矢量圖代替的,比如 PDF、ttf 字體圖標等。這樣就不需要添加 @2x、@3x 圖標,節(jié)省了空間。
iOS 中如何使用 ttf 矢量圖,可以查看這個 Repo
3. Executable file
3.1 編譯選項優(yōu)化
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.
調(diào)試符號是在編譯時形成的。當 Generate Debug Symbols 選項為 YES 的時,每個源文件在編譯成 .o 文件時,編譯參數(shù)多了 -g 和 -gmodules 兩項。打包會生成 symbols 文件。設(shè)置為 NO 則 ipa 中不會生成 symbol 文件,可以減少 ipa 大小。但會影響到崩潰的定位。保持默認的開啟,不做修改。
3.1.2 Asset Catalog Compiler
optimization 選項設(shè)置為 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.
刪除靜態(tài)鏈接的可執(zhí)行文件中未引用的代碼
Debug 設(shè)置為 NO, Release 設(shè)置為 YES 可減少可執(zhí)行文件大小。
Xcode 默認會開啟此選項,C/C++/Swift 等靜態(tài)語言編譯器會在 link 的時候移除未使用的代碼,但是對于 Objective-C 等動態(tài)語言是無效的。因為 Objective-C 是建立在運行時上面的,底層暴露給編譯器的都是 Runtime 源碼編譯結(jié)果,所有的部分應(yīng)該都是會被判別為有效代碼。
默認選項,不做修改。
3.1.4 Apple Clang - Code Generation
Optimization Level 編譯參數(shù)決定了程序在編譯過程中的兩個指標:編譯速度和內(nèi)存的占用,也決定了編譯之后可執(zhí)行結(jié)果的兩個指標:速度和文件大小。 Build Settings -> code Generation -> Optimization Level 默認情況下,Debug 設(shè)定為 None[-O0] ,Release 設(shè)定為 Fastest,Smallest[-Os]。
-
None[-O0]。 Debug 默認級別。不進行任何優(yōu)化,直接將源代碼編譯到執(zhí)行文件中,結(jié)果不進行任何重排,編譯時比較長。主要用于調(diào)試程序,可以進行設(shè)置斷點、改變變量 、計算表達式等調(diào)試工作。
-
Fast[-O,O1]。最常用的優(yōu)化級別,不考慮速度和文件大小權(quán)衡問題。與-O0級別相比,它生成的文件更小,可執(zhí)行的速度更快,編譯時間更少。
-
Faster[-O2]。在-O1級別基礎(chǔ)上再進行優(yōu)化,增加指令調(diào)度的優(yōu)化。與-O1級別相,它生成的文件大小沒有變大,編譯時間變長了,編譯期間占用的內(nèi)存更多了,但程序的運行速度有所提高。
-
Fastest[-O3]。在-O2和-O1級別上進行優(yōu)化,該級別可能會提高程序的運行速度,但是也會增加文件的大小。
-
Fastest Smallest[-Os]。Release 默認級別。這種級別用于在有限的內(nèi)存和磁盤空間下生成盡可能小的文件。由于使用了很好的緩存技術(shù),它在某些情況下也會有很快的運行速度。
-
Fastest, Aggressive Optimization[-Ofast]。 它是一種更為激進的編譯參數(shù), 它以點浮點數(shù)的精度為代價。
默認選項,不做修改。
3.1.5 Swift Compiler - Code Generation
Xcode 9.3 版本之后 Swift 編譯器提供了新的 Optimization Level 選項來幫助減少 Swift 可執(zhí)行文件的大小:
- No optimization[-Onone]:不進行優(yōu)化,能保證較快的編譯速度。
- Optimize for Speed[-O]:編譯器將會對代碼的執(zhí)行效率進行優(yōu)化,一定程度上會增加包大小。
- Optimize for Size[-Osize]:編譯器會盡可能減少包的大小并且最小限度影響代碼的執(zhí)行效率。
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 根據(jù)項目不同,大致可以優(yōu)化掉 5% - 30% 的代碼空間占用。 相比 -0 來說,會損失大概 5% 的運行時性能。 如果你的項目對運行速度不是特別敏感,并且可以接受輕微的性能損失,那么 -Osize 是首選。
除了 -O 和 -Osize, 還有另外一個概念也值得說一下。 就是 Single File 和 Whole Module 。 在之前的 XCode 版本,這兩個選項和 -O 是連在一起設(shè)置的,Xcode 9.3 中,將他們分離出來,可以獨立設(shè)置:
Single File 和 Whole Module 這兩個模式分別對應(yīng)編譯器以什么方式處理優(yōu)化操作。
-
Single File:逐個文件進行優(yōu)化,它的好處是對于增量編譯的項目來說,它可以減少編譯時間,對沒有更改的源文件,不用每次都重新編譯。并且可以充分利用多核 CPU,并行優(yōu)化多個文件,提高編譯速度。但它的缺點就是對于一些需要跨文件的優(yōu)化操作,它沒辦法處理。如果某個文件被多次引用,那么對這些引用方文件進行優(yōu)化的時候,會反復(fù)的重新處理這個被引用的文件,如果你項目中類似的交叉引用比較多,就會影響性能。
-
Whole Module: 將項目所有的文件看做一個整體,不會產(chǎn)生 Single File 模式對同一個文件反復(fù)處理的問題,并且可以進行最大限度的優(yōu)化,包括跨文件的優(yōu)化操作。缺點是,不能充分利用多核處理器的性能,并且對于增量編譯,每次也都需要重新編譯整個項目。
如果沒有特殊情況,使用默認的 Whole Module 優(yōu)化即可。 它會犧牲部分編譯性能,但的優(yōu)化結(jié)果是最好的。
故,在 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
設(shè)置為 YES 可以去掉不必要的符號信息,可以減少可執(zhí)行文件大小。但去除了符號信息之后我們就只能使用 dSYM 來進行符號化了,所以需要將 Debug Information Format 修改為 DWARF with dSYM file。
Symbols Hidden by Default 會把所有符號都定義成”private extern”,詳細信息見官方文檔。
故,Release 設(shè)置為 YES,Debug 設(shè)置為 NO。
3.1.7 Exceptions
在 iOS微信安裝包瘦身 一文中,有提到:
去掉異常支持,Enable C++ Exceptions和Enable Objective-C Exceptions設(shè)為NO,并且Other C Flags添加-fno-exceptions,可執(zhí)行文件減少了27M,其中__gcc_except_tab段減少了17.3M,__text減少了9.7M,效果特別明顯。可以對某些文件單獨支持異常,編譯選項加上-fexceptions即可。但有個問題,假如ABC三個文件,AC文件支持了異常,B不支持,如果C拋了異常,在模擬器下A還是能捕獲異常不至于Crash,但真機下捕獲不了(有知道原因可以在下面留言:)。去掉異常后,Appstore 后續(xù)幾個版本 Crash 率沒有明顯上升。
個人認為關(guān)鍵路徑支持異常處理就好,像啟動時NSCoder讀取setting配置文件得要支持捕獲異常,等等
看這個優(yōu)化效果,感覺發(fā)現(xiàn)了新大陸。關(guān)閉后驗證.. 毫無感知,基本沒什么變化。
可能和項目中用到比較少有關(guān)系。故保持開啟狀態(tài)。
3.1.8 Link-Time Optimization
Link-Time Optimization 是 LLVM 編譯器的一個特性,用于在 link 中間代碼時,對全局代碼進行優(yōu)化。這個優(yōu)化是自動完成的,因此不需要修改現(xiàn)有的代碼;這個優(yōu)化也是高效的,因為可以在全局視角下優(yōu)化代碼。
蘋果在 WWDC 2016 中,明確提出了這個優(yōu)化的概念,What’s New in LLVM。并且說在蘋果內(nèi)部已經(jīng)廣泛地使用這個優(yōu)化方法進行編譯。
它的優(yōu)化主要體現(xiàn)在如下幾個方面:
多余代碼去除(Dead code elimination):如果一段代碼分布在多個文件中,但是從來沒有被使用,普通的 -O3 優(yōu)化方法不能發(fā)現(xiàn)跨中間代碼文件的多余代碼,因此是一個“局部優(yōu)化”。但是Link-Time Optimization 技術(shù)可以在 link 時發(fā)現(xiàn)跨中間代碼文件的多余代碼。
跨過程優(yōu)化(Interprocedural analysis and optimization):這是一個相對廣泛的概念。舉個例子來說,如果一個 if 方法的某個分支永不可能執(zhí)行,那么在最后生成的二進制文件中就不應(yīng)該有這個分支的代碼。
內(nèi)聯(lián)優(yōu)化(Inlining optimization):內(nèi)聯(lián)優(yōu)化形象來說,就是在匯編中不使用 “call func_name” 語句,直接將外部方法內(nèi)的語句“復(fù)制”到調(diào)用者的代碼段內(nèi)。這樣做的好處是不用進行調(diào)用函數(shù)前的壓棧、調(diào)用函數(shù)后的出棧操作,提高運行效率與棧空間利用率。
在新的版本中,蘋果使用了新的優(yōu)化方式 Incremental,大大減少了鏈接的時間。建議開啟。
總結(jié),開啟這個優(yōu)化后,一方面減少了匯編代碼的體積,一方面提高了代碼的運行效率。
3.2 代碼瘦身
代碼的優(yōu)化,即通過刪除無用類、無用方法、重復(fù)方法等,來達到可執(zhí)行文件大小的減小。 而如何篩選出符合條件的無用類、方法,則需要通過一些工具來完成(fui)
掃描無用代碼的基本思路都是查找已經(jīng)使用的方法/類和所有的類/方法,然后從所有的類/方法當中剔除已經(jīng)使用的方法/類剩下的基本都是無用的類/方法,但是由于 Objective-C 是動態(tài)語言,可以使用字符串來調(diào)用類和方法,所以檢查結(jié)果一般都不是特別準確,需要二次確認。目前市面上的掃描的思路大致可以分為 3 種:
- 基于 Clang 掃描
- 基于可執(zhí)行文件掃描
- 基于源碼掃描
先談幾個概念。
可執(zhí)行文件就是 Mach-O 文件,其大小是油代碼量決定的,通常情況下,對可執(zhí)行文件進行瘦身,就是找到并刪除無用代碼的過程。找到無用代碼的過程類比找到無用圖片的思路。
- 找到類和方法的全集
- 找到使用過的類和方法集合
- 取2者差集得到無用代碼集合
- 工程師確認后,刪除即可
LinkMap 文件分為3部分:Object File、Section、Symbols。
- Object File:包含了代碼工程的所有文件
- Section:描述了代碼段在生成的 Mach-O 里的偏移位置和大小
- Symbols:會列出每個方法、類、Block,以及它們的大小
先說說如何快速找到方法和類的全集?
我們可以通過 LinkMap 來獲得所有的代碼類和方法的信息。獲取 LinkMap 可以通過將 Build Setting 里面的 Write Link Map File 設(shè)置為 YES,然后指定 Path to Link Map File 的路徑就可以得到每次編譯后的 LinkMap 文件了。
3.2.1 基于 clang 掃描
基本思路是基于 clang AST。追溯到函數(shù)的調(diào)用層級,記錄所有定義的方法/類和所有調(diào)用的方法/類,再取差集。具體原理參考 如何使用 Clang Plugin 找到項目中的無用代碼,目前只有思路沒有現(xiàn)成的工具。
3.2.2 基于可執(zhí)行文件掃描(LinkMap 結(jié)合 Mach-O 找無用代碼)
上面我們得知可以通過 LinkMap 統(tǒng)計出所有的類和方法,還可以清晰地看到代碼所占包大小的具體分布,進而有針對性地進行代碼優(yōu)化。
得到了代碼的全集信息后,我們還需要找到已經(jīng)使用過的方法和類,這樣才可以獲取差集,找到無用代碼。所以接下來就談?wù)勅绾瓮ㄟ^ Mach-O 取到使用過的類和方法。
Objective-C 中的方法都會通過 objc_msgSend 來調(diào)用,而 objc_msgSend 在 Mach-O 文件里是通過 _objc_selrefs 這個 section 來獲取 selector 這個參數(shù)的。
所以,_objc_selrefs 里的方法一定是被調(diào)用了的。_objc_classrefs 里是被調(diào)用過的類, objc_superrefs 是調(diào)用過 super 的類(繼承關(guān)系)。通過 _objc_classrefs 和 _objc_superrefs,我們就可以找出使用過的類和子類。
那么,Mach-O 文件中的 _objc_selrefs、_objc_classrefs、_objc_superrefs 如何查看呢?
下面舉例說明:
前置條件:先運行項目,在生成的 Products 目錄下的 BridgeLabiPhone.app 解壓,取出對應(yīng)的和工程同名的 BridgeLabiPhone。然后運行上面的 Github 項目。可以看到運行了一個 Mac App。點擊頂部的菜單欄里面的 File->Open。選擇電腦上的 BridgeLabiPhone.app 選擇里面的 BridgeLabiPhone。見下圖
由于 Objective-C 是一門動態(tài)語言,所以檢測出的結(jié)果仍舊需要我們2次確認。
3.2.3 基于源碼掃描
一般都是對源碼文件進行字符串匹配。例如將 A *a、[A xxx]、NSStringFromClass("A")、objc_getClass("A") 等歸類為使用的類,@interface A : B 歸類為定義的類,然后計算差集。
基于源碼掃描 有個已經(jīng)實現(xiàn)的工具 - fui,但是它的實現(xiàn)原理是查找所有 #import "A" 和所有的文件進行比對,所以結(jié)果相對于上面的思路來說可能更不準確。
3.2.4 通過 AppCode 查找無用代碼
AppCode 提供了 Inspect Code 來診斷代碼,其中含有查找無用代碼的功能。它可以幫助我們查找出 AppCode 中無用的類、無用的方法甚至是無用的 import ,但是無法掃描通過字符串拼接方式來創(chuàng)建的類和調(diào)用的方法,所以說還是上面所說的 基于源碼掃描 更加準確和安全。
說明:AppCode檢測出了實際上需要的大部分場景的問題,但是由于 Objective-C 是一門動態(tài)性語言,所以 AppCode 檢測出無用的方法等都需要工程師自己再次確認后刪除。(在我們的工程中有一些和 H5 交互的橋接方法,因此 AppCode 視為 Unused Method,但是你刪除的話,那就自己哭去吧 ?)。實際經(jīng)驗告訴我,使用 AppCode 的時候如果工程比較大,則整個 code inspect 會非常耗時(給你打個預(yù)防針哦,筆芯)
- 無用類:Unused class 是無用類,Unused import statement 是無用類引入聲明,Unused property 是無用的屬性;
- 無用方法:Unused method 是無用的方法,Unused parameter 是無用參數(shù),Unused instance variable 是無用的實例變量,Unused local variable 是無用的局部變量,Unused value 是無用的值;
- 無用宏:Unused macro 是無用的宏。
- 無用全局:Unused global declaration 是無用全局聲明。
3.2.5 運行時真正檢測類是否用過
通過上述手段找到并刪除了無用代碼。App 不斷上線迭代蠻多代碼都不會被調(diào)用了(業(yè)務(wù)被砍掉了)。這種方式下這些無用的代碼也是可以被刪除的。
通過 Objective-C 的 runtime 源碼,我們可以找到如何判斷一個類是否初始化過的函數(shù)。
#define RW_INITIALIZED (1<<29) bool isInitialized() {return getMeta()->data()->flags & RW_INITIALIZED; } 復(fù)制代碼isInitialized 的結(jié)果會保存到元類的 class_rw_t 結(jié)構(gòu)體的 flags 信息里, flags 的 1<<29 位記錄的就是這個類是否初始化了的信息,而 flags 的其他位記錄的信息,可以查看 rumtime 的源碼
// 類的方法列表已修復(fù) #define RW_METHODIZED (1<<30)// 類已經(jīng)初始化了 #define RW_INITIALIZED (1<<29)// 類在初始化過程中 #define RW_INITIALIZING (1<<28)// class_rw_t->ro 是 class_ro_t 的堆副本 #define RW_COPIED_RO (1<<27)// 類分配了內(nèi)存,但沒有注冊 #define RW_CONSTRUCTING (1<<26)// 類分配了內(nèi)存也注冊了 #define RW_CONSTRUCTED (1<<25)// GC:class 有不安全的 finalize 方法 #define RW_FINALIZE_ON_MAIN_THREAD (1<<24)// 類的 +load 被調(diào)用了 #define RW_LOADED (1<<23) 復(fù)制代碼既然可以在運行的期間知道類是否初始化了,那么就可以找出哪些類未初始化,即可以找到在真實環(huán)境里面沒有用到的類并刪除掉。
4. App Extension
App Extension 的占用,都放在 Plugin 文件夾內(nèi)。它是獨立打包簽名,然后再拷貝進 Target App Bundle 的。 關(guān)于 Extension,有兩個點要注意:
靜態(tài)庫最終會打包進可執(zhí)行文件內(nèi)部,所以如果 App Extension 依賴了三方靜態(tài)庫,同時主工程也引用了相同的靜態(tài)庫的話,最終 App 包中可能會包含兩份三方靜態(tài)庫的體積。
動態(tài)庫是在運行的時候才進行加載鏈接的,所以 Plugin 的動態(tài)庫是可以和主工程共享的,把動態(tài)庫的加載路徑 Runpath Search Paths 修改為跟主工程一致就可以共享主工程引入的動態(tài)庫。
所以,如果可能的話,把相關(guān)的依賴改成動態(tài)庫方式,達到共享。
5. 靜態(tài)庫瘦身
項目中都會引入第三方靜態(tài)庫。通過 lipo 工具可以查看支持的指令集,比如查看微博 SDK 終端切換到微博 SDK 的目錄下執(zhí)行下面命令
- 靜態(tài)庫指令集信息查看:lipo -info libname.a(或者libname.framework/libname)
我們知道 i386、x86_64 是模擬器的指令集。所以我們可以模擬器版本的指令集。因為 armv7 也可以兼容 armv7s。所以 armv7s 也可以刪除了。只保留 armv7 和 arm64
- 靜態(tài)庫拆分:lipo 靜態(tài)庫文件路徑 -thin CPU架構(gòu) -output 拆分后的靜態(tài)庫文件路徑
- 靜態(tài)庫合并:lipo -create 靜態(tài)庫1文件路徑 靜態(tài)庫2文件路徑... 靜態(tài)庫n文件路徑 -output 合并后的靜態(tài)庫文件徑
通過上面的操作我們將靜態(tài)庫里面支持模擬器的指令集給去掉了,所以模擬器是無法跑代碼的,如何解決?
補充2個說明:
- 自動生成。Xcode 會在工程編譯或者歸檔的時候自動生成 .dSYM 文件,在 Buld setting 設(shè)置中有開關(guān)可以設(shè)置去關(guān)掉 .dSYM 文件
- 手動生成。通過腳本從 Mach-O 文件中提取出來。
該方式通過 dsymutil 工具,從項目編譯結(jié)果 .app 目錄下的 Mach-O 文件中提取出調(diào)試符號表文件。Xcode 在歸檔的時候是通過它生辰的 .dSYM 文件
最后的一個對比效果圖:
總結(jié):瘦身技術(shù)常見操作就這些,但是維持應(yīng)用包體積的瘦身卻是一個觀念,從日常開發(fā)到線上發(fā)布都需要有這個意識。這樣當你在寫代碼的時候就會考慮同樣一個效果,你的具體實現(xiàn)手段是怎么樣的。比如為了一個稍微炫酷的效果就要引入一個很大的三方庫,有了“瘦身”的意識,你很大可能就是自己動手擼一個代碼。比如一些無用資源的管理方式、有用的圖片資源的高效管理方式等等。有了意識,行動自然會往這個方面去靠。(?大道理一套一套的。我也不想的,畢竟是playboy)
其中遇到了一個神奇的問題。lint 的時候看到一些未使用的依賴庫。見 問題
By the way: 如果在應(yīng)用包瘦身方面有其他的做法,請告知,完善文章。
參考文章:
- Humble Assets Catalog
- 關(guān)于 Pod 庫的資源引用 resource_bundles or resources
- 部分圖片或者文字內(nèi)容引用來自網(wǎng)絡(luò)(若有引用到,請告訴我地址,及時補充)
轉(zhuǎn)載于:https://juejin.im/post/5cdd27d4f265da036902bda5
總結(jié)
- 上一篇: ocelot简单入门
- 下一篇: 面向对象-抽象类