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
當向 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 來進行符號化
那么 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步:
如果不想重新寫一個工具,那么可以直接使用開源的工具 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
TimeProfile-imageWithContentsOfFile
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 組件庫里面的 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 會生成一個你在 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 復制代碼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。
- 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 統計出所有的類和方法,還可以清晰地看到代碼所占包大小的具體分布,進而有針對性地進行代碼優化。
得到了代碼的全集信息后,我們還需要找到已經使用過的方法和類,這樣才可以獲取差集,找到無用代碼。所以接下來就談談如何通過 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 如何查看呢?
下面舉例說明:
前置條件:先運行項目,在生成的 Products 目錄下的 BridgeLabiPhone.app 解壓,取出對應的和工程同名的 BridgeLabiPhone。然后運行上面的 Github 項目。可以看到運行了一個 Mac App。點擊頂部的菜單欄里面的 File->Open。選擇電腦上的 BridgeLabiPhone.app 選擇里面的 BridgeLabiPhone。見下圖
由于 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)
我們知道 i386、x86_64 是模擬器的指令集。所以我們可以模擬器版本的指令集。因為 armv7 也可以兼容 armv7s。所以 armv7s 也可以刪除了。只保留 armv7 和 arm64
- 靜態庫拆分:lipo 靜態庫文件路徑 -thin CPU架構 -output 拆分后的靜態庫文件路徑
- 靜態庫合并:lipo -create 靜態庫1文件路徑 靜態庫2文件路徑... 靜態庫n文件路徑 -output 合并后的靜態庫文件徑
通過上面的操作我們將靜態庫里面支持模擬器的指令集給去掉了,所以模擬器是無法跑代碼的,如何解決?
補充2個說明:
- 自動生成。Xcode 會在工程編譯或者歸檔的時候自動生成 .dSYM 文件,在 Buld setting 設置中有開關可以設置去關掉 .dSYM 文件
- 手動生成。通過腳本從 Mach-O 文件中提取出來。
該方式通過 dsymutil 工具,從項目編譯結果 .app 目錄下的 Mach-O 文件中提取出調試符號表文件。Xcode 在歸檔的時候是通過它生辰的 .dSYM 文件
最后的一個對比效果圖:
總結:瘦身技術常見操作就這些,但是維持應用包體積的瘦身卻是一個觀念,從日常開發到線上發布都需要有這個意識。這樣當你在寫代碼的時候就會考慮同樣一個效果,你的具體實現手段是怎么樣的。比如為了一個稍微炫酷的效果就要引入一個很大的三方庫,有了“瘦身”的意識,你很大可能就是自己動手擼一個代碼。比如一些無用資源的管理方式、有用的圖片資源的高效管理方式等等。有了意識,行動自然會往這個方面去靠。(?大道理一套一套的。我也不想的,畢竟是playboy)
其中遇到了一個神奇的問題。lint 的時候看到一些未使用的依賴庫。見 問題
By the way: 如果在應用包瘦身方面有其他的做法,請告知,完善文章。
參考文章:
- Humble Assets Catalog
- 關于 Pod 庫的資源引用 resource_bundles or resources
- 部分圖片或者文字內容引用來自網絡(若有引用到,請告訴我地址,及時補充)
轉載于:https://juejin.im/post/5cdd27d4f265da036902bda5
總結
- 上一篇: ocelot简单入门
- 下一篇: 学习threeJS(一)--第一个thr