iOS安裝包瘦身總結

前段時間APP要做資源壓縮,需要把項目中使用的所有圖片資源進行壓縮,以減小APP安裝包體積。想着既然壓縮APP資源是爲了縮小APP體積,那麼就做到位吧,來一遍APP整體瘦身流程並做一下總結吧。
整個過程分三步:
1.瘦身前分析
2.瘦身策略制定並實施
3.結果對比

瘦身前分析
安裝包分析
iOS安裝包有兩種狀態:一種是提交給Apple市場的多架構綜合包;另一種是用戶下載的單架構特定包;
提交給Apple市場的多架構綜合包是Xcode構建出的產物ipad包,它裏面包含了多個架構的產物,多套尺寸的圖片資源,當上傳成功後蘋果會進行裁剪和二次分發,轉化爲供用戶下載的具體架構的App Store下載ipa包。
用戶下載的單架構特定包是用戶從App Store下載的包。它裏面包含了當前手機架構的靜態庫二進制文件、動態庫、asset.car(單尺寸)、其他資源文件。它是針對當前手機的架構的特定下載包。裏面的資源都是單份的。

對於多架構綜合包可以使用工具對特定架構進行拆分
使用lipo工具拆分單架構
lipo "originalExecutable" -thin arm64 -output "arm64Executable"
使用assetutil工具拆分不同倍圖的asset
xcrun --sdk iphoneos assetutil --scale 3 --output "$targetFolder/Assets3.car" "$sourceFolder/Assets.car"
源代碼分析
iOS工程結構:iOS工程由主工程和Pod模塊組成,模塊有靜態庫和動態庫兩種類型。
主工程的構成有主Target,裏面有源代碼文件(OC的.h和.m、Swift)、xcassets、nib、bunlde、多語言文件、各種配置文件(plist、json)。
模塊內部的構成有源代碼文件(OC、C、C++的.h和.m、Swift)、xcassets、nib、bunlde、多語言文件、各種配置文件(plist、json)。

源碼->IPA產物
iOS工程項目由源碼到IPA包的核心的變化是:編譯和文件拷貝。源碼會被編譯鏈接爲MachO可執行文件,xcassets文件夾會被轉化爲Assets.car,其他都可以簡單理解爲文件拷貝。

IPA包產物分析
對從App Store下載的IPA包解壓後是一個文件夾,內部主要包括MachO可執行文件、.framework(動態庫)、Assets.car、.strings(多語言)、.bundle、nib、json、png...。
可以根據產物資源體積由大到小進行優化,比如:MachO可執行文件(包含所有靜態庫)、動態庫、assets.car、bundle、nib、音頻、視頻。

瘦身策略
瘦身總體方向可以參下面5個方面入手:組件治理,資源治理,編譯優化,代碼治理,運行時下載。
組件治理[刪除淘汰業務代碼]:0代碼覆蓋率組件,無用組件,重複功能組件。
資源治理[正在用的壓縮,不用的刪除,太大的網絡下載,相似的使用iconfont共用]:大資源,可以有損壓縮資源,無用資源,重複圖標,iconfont,多語言文案
編譯優化[修改編譯策略]:精簡編譯產物,剝離符號表Strip Linked Product,刪除未引用的C/C++/Swift代碼,Asset Catalog Complier,C++導出必要符號, Symbols Hidden by Default, LTO優化跨模塊調用代碼
代碼治理[刪除無用類]: 運行時未加載類,編譯時無用類,業務重構
運行時下載[大資源網絡下載]:大資源,多語言文案

組件治理[刪除淘汰業務代碼]
主要看對應模塊的業務是否是線上正常運行,還是已經被廢棄,或者被其他方式(RN, H5, Flutter)所取代,如果實際不在使用,則刪除到此組件。

資源治理[正在用的壓縮,不用的刪除,太大的網絡下載,相似的使用iconfont共用]
是APP瘦身效果非常好的一個方向,通常會有不錯的收益。
這裏需要注意使用Asset Catalog管理切圖資源的包體積反彈問題,因爲Xcode在編譯時,會使用actool工具對Asset Catalog下的圖片資源優化,壓縮,這可能導致之前的壓縮過的圖片被重寫變大。

1.有損壓縮
XCode構建時會做“compile asset catalog”,會重新對圖片進行無損壓縮。因此使用imageoptim等工具進行無損壓縮效果不明顯,其中壓縮png圖片沒有效果,壓縮jpg圖片有一定效果。
根據實踐經驗,icon做有損壓縮並不影響視覺體驗,壓縮率可以達到70%~80%。業界有不少png壓縮工具,我們使用到的有tinypng、pngquant、pngcrush、optipng(無損)、advpng。

2.刪除無用圖片 - 未被使用的圖片資源
先解析出所有拷貝到構建產物的資源文件,再解析出代碼中實際引用到的資源文件,兩者的差集就是無用資源。
第一步獲取全量資源文件。
a.可以通過在Cocoapos工程中,“Pods-targetName-resource.sh”腳本負責拷貝Pod裏的文件資源到構建產物,包括所有文件類型bundle、xcassets、json、png。解析該腳本可以得到每個Pod模塊都拷貝了哪些圖片資源。
// Pods-targetName-resource.sh
install_resource "${PODS_ROOT}/APodName/APodName.framework/APodName.bundle"
install_resource "${PODS_ROOT}/BPodName/BPodName.framework/BPodName.xcassets"
install_resource "${PODS_ROOT}/BPodName/BPodName.framework/xxx.png"
b.也可以通過編寫shell腳本,掃描項目源文件,找出所有的png,jpg資源文件。
因爲我們項目中所有的圖片都放在了Asset中進行管理,所以這裏只掃描Asset下面的圖片資源特徵
targetPicture=".imageset"
function searchOneDir(){
    cd $1
    path=`pwd`
    for item in `ls`; do
        if [[ -d "${path}/${item}" ]] && [[ ! "${filterList[@]}" =~ "${item}" ]]
        then
            if [[ "${item}" =~ "${targetPicture}" ]]
            then
            echo "${path}/${item}"
            echo "內部:$item"
                itemSub="${path}/${item}"
                appContainPictureList+=(${itemSub})
            else
                if [[ -d ${item} ]]
                then
                    searchOneDir "$item"
                fi
            fi
        fi
    done
    cd ..
    path=`pwd`
    echo "path2: $path"
 
    return 0;
}
詳細腳本見如下地址:
https://github.com/zhfei/ios-scripts/blob/main/tool_project_batchZipPicture.sh

c.或者從IPA包中直接查看
iOS開發中,如果使用了Images.xcassets管理圖片,打包的時候會生成一個Assets.car文件,所有的圖片都在這裏面。
如果想查看裏面包含的圖片,則需要工具來解壓將Assets.car文件解包到指定文件夾,可使用的工具有:cartool,AssetCatalogTinkerer。

第二步,獲取代碼中實際引用到的資源文件。OC代碼中引用資源文件都是以字符串字面量的形式聲明,構建後存放在Mach-O文件"__cstring" section。
利用strings解析framework的二進制文件就可以得到代碼中所有的字符串聲明,進行過濾。
這裏有個注意點,對於Swift項目中使用的切圖在strings打印的符號表中並不能直接查出,這個問題暫時沒有明白爲什麼。
targetAssetDir=".xcassets"
targetPicture=".imageset"
 
appContainPictureList=()
UnUsedPictureList=()
UsedPictureList=()
function checkMachOFile() {
    res=`strings $1`
 
    for assertName in ${appContainPictureList[@]}; do
        if [[ ! "$res" =~ "$assertName" ]]
        echo "$assertName"
        then
            UnUsedPictureList[${#UnUsedPictureList[*]}]=$assertName
        else
            UsedPictureList[${#UsedPictureList[*]}]=$assertName
        fi
    done
}
詳細腳本見如下地址:
https://github.com/zhfei/ios-scripts/blob/main/tool_project_findUnUsePicture.sh

3.重複圖標
解決方案是在構建時計算資源的哈希值,去重相同哈希資源,並保留源文件名和哈希值的映射表。運行時Hook 資源加載的”imageNamed“方法,根據映射表替換資源名稱。

4.iconfont
iconfont支持縮放、修改顏色,它size小,適合用於箭頭、佔位圖等圖標場景,使用iconfont可以減少包大小也能提高開發視覺體驗的統一性。對相似的場景進行限制,禁止隨意添加切圖。

5.多語言文案
6.運行時下載

編譯優化[修改編譯策略]
1.精簡編譯產物Oz:Optimization Level
Optimization Level多個級別,-Oz比-O3的編譯產物體積小10%左右。設置-Oz以後,XCode會優化連續的彙編指令,從而減少二進制大小,但副作用是執行速度會變慢。C++工程建議都開啓。
主工程Release
Optimization Level :-Oz
Framework工程
Optimization Level :-Oz
2.Strip Linked Product - 剝離符號表
在Xcode中,"Strip Linked Product"是一個構建設置選項,用於控制在構建過程中是否剝離(strip)可執行文件中的符號信息。
符號信息是一個標識符或者函數名稱,在可執行文件或動態鏈接庫中可用於調試和符號化。剝離符號信息可以減小可執行文件的大小,同時也可以防止他人通過分析符號信息來獲取敏感信息。
Strip Linked Product設置會剝離特定的符號,Debug環境不要設置YES,否則調試時看不到符號。

在Xcode中,"Deployment Postprocessing"是一個構建設置選項,它用於控制構建完成後是否執行部署後處理。
部署後處理是指構建過程完成對生成的可執行文件或應用程序包進行額外的處理。它可以包括諸如符號剝離、代碼簽名、資源壓縮等操作。
主工程Release
Deployment Postprocessing :YES
Strip Linked Product :YES
Strip Style :All Symbols(剝離所有符號表和重定向信息)

Framework工程
Deployment Postprocessing :YES
Strip Linked Product :YES
Strip Style :Non-Global Symbols(剝離包括調試信息等非全局的符號,保留外部符號)
說明:
a.靜態庫不能將Strip Style 設置爲All Symbols,因爲剝離了所有符號的靜態庫是無法被正常鏈接的
b.去除符號不影響 dSYM 文件中的符號信息,查看崩潰日誌時,可以從 dSYM 文件中找對應符號

3.Symbols Hidden by Default
Symbols Hidden by Default用於設置符號默認可見性,如果設置爲YES,XCode會把所有符號都定義爲”private extern”,包大小會略有減少。動態庫設置爲NO,否則會有鏈接錯誤。
主工程Release
Symbols Hidden by Default :Yes
Framework工程 靜態庫/動態庫
Symbols Hidden by Default :NO
4.剔除未引用的C/C++/Swift代碼:Dead Code Stripping
Dead Code Stripping開啓後會在鏈接時移除未使用的代碼,它對靜態語言C/C++/Swift有效,對動態語言OC無效。
主工程
Dead Code Stripping :Yes
5.Asset Catalog Compiler
Optimization有三個選項,空、time和Space,選擇Space可以優化包大小
主工程
Asset Catalog Compiler->Optimization設置爲space
6.C++導出必要符號 - 動態庫複用主二進制靜態庫(用的不多,因爲很多項目不會嵌入動態庫)
C++動態庫經常用到一些基礎庫比如openssl、libyuv、libcurl,他們一般是靜態庫。如果動態庫引用了靜態庫,它編譯時默認會內嵌靜態庫的所有符號。
雖然我們可以在動態庫中設置只導出需要用到的靜態庫符號,但是有可能多個動態庫都用到了同一個基礎庫,這樣還是會造成基礎庫的冗餘。
比如openssl大小1MB,如果A、B兩個動態庫依賴了openssl,APP也引用了openssl,最終ipa包實際有3個openssl,有2MB大小是冗餘的。
這種場景下,最佳解決方案是共享符號表,讓動態庫可以調用主二進制的基礎庫符號,從而可以去掉內置的靜態庫。只要修改XCode的Link配置,無需額外的代碼開發。
動態庫工程:
1設置當遇到未定義的函數時,動態查找APP主二進制符號表。
2 關閉bitcode
Other Linker Flags -> -undefined dynamic_lookup
Enable Bitcode -> No
3導出動態庫需要調用的外部符號,寫到一個文件exported_symbols內 nm -u xxx.framework/xxx > exported_symbols.txt
APP工程: 1配置需要導出exported_symbols文件內的所有符號,避免編譯時動態庫需要用到的符號被strip掉。 2關閉bitcode。 // exported_symbols.txt是需要被導出的符號文件路徑 EXPORTED_SYMBOLS_FILE -> exported_symbols.txt Enable Bitcode -> No
7.C++只導出必要符號:Symbol Visibility (用的不多,因爲很多項目沒有涉及自己構建C++靜態庫和動態庫)
用到哪部分導出哪部分,沒有使用的不導出。
C++的靜態庫和動態庫都只導出必須的符號,默認設置爲隱藏所有符號,然後用Visibility Attributes單獨控制需要導出的符號。

8.默認隱藏所有C++符號
Other C++ Flags->添加-fvisibility=hidden
設置需要導出的符號
__attribute__((visibility("default"))) void MyFunction1() {} 
__attribute__((visibility("default"))) void MyFunction2() {} 
代碼治理[刪除無用類]

刪除無用OC類 - 運行時Objc類覆蓋率
ObjC的類第一次被使用時會調用+initialize方法,類被加載過後cls->isInitialized會返回True。isInitialized方法讀取了metaClass的data變量裏的flags,如果flags裏的第29位爲1,則返回True。
// objc-class.mm
Class class_initialize(Class cls, id inst) {
    if (!cls->isInitialized()) {
        initializeNonMetaClass (_class_getNonMetaClass(cls, inst));
    }
    return cls;
}


// objc-runtime.h
#define RW_INITIALIZED        (1<<29)
bool isInitialized() {
    return getMeta()->data()->flags & RW_INITIALIZED;
}
刪除無用OC類 - 編譯時未被引用的類
iOS編譯的產物是Mach-o格式的,文件裏 __DATA __objc_classrefs 段記錄了所有引用過的類的地址,__DATA __objc_classlist段記錄了所有類的地址,兩者Differ可以得到未被引用類的地址。
然後將地址符號化,就可以得到未被引用類信息。因爲Objc是動態語言,如果使用runtime動態調用某個class,這種情況掃描不出來。(比如Target Action和 JS Core)
otool -v -s __DATA __objc_classrefs xxxMainClient  #讀取__DATA Segment中section爲__objc_classrefs的符號
otool -v -s __DATA __objc_classlist xxxMainClient #讀取__DATA Segment中section爲__objc_classlist的符號
nm -nm xxxMainClient
運行時下載[大資源網絡下載]
大資源
多語言文案

瘦身結果
項目中切圖通過壓縮少了7M,Xcode編譯產生的APP包少了1.2M。
項目切圖資源小了7M,實際打包只少了1.2M的原因爲:
對應Asset Catalog下管理的切圖資源,Xcode打包編譯時會用actool工具處理圖片,優化圖片大小。對於壓縮後的產物,在Xcode編譯打包後,被壓縮的切圖都會重新變大,包括:pngquant有損壓縮和imageoptim無損壓縮。
解決方法
1.關掉Xcode的PNG優化開關(設置Targets->Build Settings->Compress PNG Files爲YES)。
2.可以採用將壓縮後的切圖放在一個單獨的目錄下,脫離Asset Catalog的管理,避免被壓縮後的切圖重新被優化大。
3.修改壓縮策略,將Asset Catalog Compiler->Optimization設置爲space

另外
爲了後續的內存變化檢測,可以爲每個模塊做增量變化統計,靜態庫和動態庫計算的原理不同。
對於靜態庫,先解析linkmap數據,計算出模塊代碼大小,在解析Pods-targetName-resource.sh的資源拷貝代碼,計算出拷貝到Pod模塊的資源大小。
對於動態庫,先使用lipo拆分動態庫的二進制文件,計算出單架構的代碼大小,然後再計算動態庫framework內的資源文件,得到動態庫的資源文件大小。

參考文章:
https://developer.aliyun.com/article/981881


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章