一、背景
隨著業(yè)務(wù)的快速發(fā)展與持續(xù)迭代,玩物得志APP的包體積也在不斷增加,在僅僅四個(gè)月的時(shí)間,由V3.0.2的127.4M 增大到V3.5.0的174.5M,上漲了約37%,可想而知,如果不及時(shí)管控,包體積很快會突破200M。
安裝包過大,將會影響下載轉(zhuǎn)化率。google開發(fā)者大會上公布的統(tǒng)計(jì)數(shù)據(jù)顯示:
包體大小每上升 6MB,應(yīng)用下載轉(zhuǎn)化率就會下降 1%,
而每當(dāng)包體大小減少 10MB 的時(shí)候,平均下載轉(zhuǎn)化率也會有 0.5-1.5% 的增長。
安裝包大小有下載大小和安裝大小兩個(gè)概念。

下載大小:通過網(wǎng)絡(luò)下載的壓縮 App 大小。為了節(jié)省流量,用戶下載的都是壓縮包,而解壓的過程也就是我們說的安裝。
安裝大小:為 App解壓后將在用戶設(shè)備上占用的磁盤空間大小。也就是在App Store上看到的大小,安裝大小較大,通常會影響用戶的下載意愿。
下載大小過大,蘋果會限制用戶使用蜂窩網(wǎng)絡(luò)下載App。
2017 年 9 月,iOS 11 后,下載限制從 100 MB 提升至 150 MB
2019 年 5 月,下載限制從 150 MB 提升至 200 MB
2019 年 9 月,iOS 13 后,若下載大小超過 200 MB,用戶可選擇是否使用蜂窩網(wǎng)絡(luò)下載,但iOS 13以下的系統(tǒng)仍然無法通過蜂窩網(wǎng)絡(luò)下載

雖然蘋果在逐漸放寬限制。但下載大小若超出 200 MB,可以肯定對APP下載成本,推廣效率都會產(chǎn)生比較大的影響。
而安裝大小過大,是會影響用戶的留存率的,畢竟當(dāng)用戶手機(jī)內(nèi)存不夠用時(shí),肯定是優(yōu)先刪除占內(nèi)存比較大的App。
所以降低下載大小和安裝大小就是我們的目的。
二、包大小分析
通過解壓一個(gè)ipa文件,我們可以看到一個(gè).app文件中主要包括三個(gè)部分:
資源文件:主要是圖片、音頻、視頻、等資源。
可執(zhí)行文件:程序的主體,是將我們的代碼、靜態(tài)庫、動態(tài)庫通過編譯鏈接生成的文件。
bundle:工程中使用的三方或資源bundle。

不過.app的大小并不完全就是包體積的大小,在APP上傳到 AppStore Connect 到之后,Apple 也會對安裝包做一些處理,測試安裝包的變化無法對應(yīng)到真正的下載大小變化的變化。處理主要包括:
App Slicing 對于不同架構(gòu)的裁剪,可執(zhí)行文件只剩下單架構(gòu);
Asset.car 中圖片只留下設(shè)備需要的特定尺寸和壓縮算法的變體;
__TEXT 段加密;
這也是在不同設(shè)備上看到的包大小不同的部分原因。
通過分析可知,瘦身的途徑主要還是針對可執(zhí)行文件和資源的優(yōu)化。
三、可執(zhí)行文件優(yōu)化
1、刪除無用類
一般的無用代碼篩查方式可以分為動態(tài)和靜態(tài)兩種方式。靜態(tài)的方式主要是通過代碼掃描、參與編譯構(gòu)建過程或者分析最終產(chǎn)物來確認(rèn)哪些代碼沒有被用到。而動態(tài)的方式主要是靠插樁或者運(yùn)行時(shí)信息來獲取哪些代碼沒有執(zhí)行。
1.1 動態(tài)查找
基于插樁的行級別代碼覆蓋率:
基于 GCOV 或者 LLVM Profile 二進(jìn)制的插樁方案可以實(shí)現(xiàn)在運(yùn)行時(shí)收集插樁數(shù)據(jù)來指導(dǎo)無用代碼的刪除。但插樁方案局限性也顯而易見,插樁會劣化二進(jìn)制本身的大小和性能,同時(shí)原生的插樁方案是無法過審上線。數(shù)據(jù)收集只能局限于線下。
基于 Runtime 的輕量級運(yùn)行時(shí)「類覆蓋率」方案:
Objc 的類首次調(diào)用類初始化時(shí),+initialize 被執(zhí)行,系統(tǒng)會自動標(biāo)記已被調(diào)用,在 metaClass 中 data 的 flags 字段第 29 位就存著這個(gè)這個(gè)狀態(tài)??梢允褂?flags & RW_INITIALIZED 獲取。
1.2 靜態(tài)查找
Mach-O文件中,__DATA`` __objc_classrefs 中記錄了引用類的地址,__DATA``__objc_classlist中記錄了所有類的地址,我們通過otool打印對應(yīng)的信息,然后兩者取差值,再進(jìn)行符號化,就得到?jīng)]有被引用的類信息。
通過
otool -v -s __DATA __objc_classrefs獲取到引用類(明確用到的)的地址。通過
otool -v -s __DATA __objc_classlist獲取所有類的地址。用所有類信息減去引用類的信息,此時(shí)我們可以拿到未使用類的地址信息。
通過
nm -nm命令可以得到地址和對應(yīng)的類名字。
通過otool -v -s __DATA __objc_classrefs獲取到引用類的地址。
def classref_pointers(path, binary_file_arch):
ref_pointers = set()
lines = os.popen('/usr/bin/otool -v -s __DATA __objc_classrefs %s' % path).readlines()
for line in lines:
pointers = pointers_from_binary(line, binary_file_arch)
ref_pointers = ref_pointers.union(pointers)
return ref_pointers
通過otool -v -s __DATA __objc_classlist獲取所有類的地址。
def classlist_pointers(path, binary_file_arch):
list_pointers = set()
lines = os.popen('/usr/bin/otool -v -s __DATA __objc_classlist %s' % path).readlines()
for line in lines:
pointers = pointers_from_binary(line, binary_file_arch)
list_pointers = list_pointers.union(pointers)
return list_pointers
用所有類信息減去引用類的信息,此時(shí)我們可以拿到未使用類的地址信息。
unref_pointers = classlist_pointers(path, binary_file_arch) - classref_pointers(path, binary_file_arch)
通過nm -nm命令可以得到地址和對應(yīng)的類名字。
def class_symbols(path):
symbols = {}
re_class_name = re.compile('(\w{16}) .* _OBJC_CLASS_\$_(.+)')
lines = os.popen('nm -nm %s' % path).readlines()
for line in lines:
result = re_class_name.findall(line)
if result:
(address, symbol) = result[0]
symbols[address] = symbol
return symbols
得到結(jié)果輸出到txt中

由于是靜態(tài)查找,對于動態(tài)生成的類,比如通過反射生成的類,會被認(rèn)為沒有被引用,所以查找出列表后,還需要人工檢查一遍。
優(yōu)化結(jié)果:刪除無用類110個(gè),收益0.5M。
2、編譯選項(xiàng)優(yōu)化
2.1 開啟LTO
編譯選項(xiàng)Link-Time Optimization優(yōu)化
蘋果官方介紹,開啟LTO后會使在release下的運(yùn)行速度提升10%,而且包體積會減小。
Apple uses LTO extensively internally
Typically 10% faster than executables from regular Release builds Multiplies
with Profile Guided Optimization (PGO)
Reduces code size when optimizing for size
但是有個(gè)缺點(diǎn),debug時(shí)的編譯速度慢了很多,而且二次編譯時(shí)會全部編譯,所以我們只是在release模式下開啟了LTO。

2.2 Optimization Level
Optimization Level是指clang采用什么樣的編譯優(yōu)化等級,在Clang的文檔里 clang - Code Generation Options 可以查閱到主要有以下等級:
-O0 Means “no optimization”: this level compiles the fastest and generates the most debuggable code.
-O1 Somewhere between -O0 and -O2.
-O2Moderate level of optimization which enables most optimizations.
-O3 Like -O2, except that it enables optimizations that take longer to perform or that may generate larger code (in an attempt to make the program run faster).
-Ofast Enables all the optimizations from -O3 along with other aggressive optimizations that may violate strict compliance with language standards.
-Os Like -O2 with extra optimizations to reduce code size.
-Oz Like -Os (and thus -O2), but reduces code size further.
Xcode默認(rèn)debug時(shí)為-O0不優(yōu)化,release時(shí)為-Os。經(jīng)過測試這里如果使用-Oz會大約減小3M左右的包體積,但是在一些頁面會出現(xiàn)crash, 經(jīng)過排查是一些延遲釋放導(dǎo)致的內(nèi)存問題。出于安全考慮,目前采用的是-Os這種優(yōu)化等級。

2.3 符號相關(guān)
symbols是指程序中的所有的變量、類、函數(shù)、枚舉、變量和地址映射關(guān)系,以及一些在調(diào)試的時(shí)候使用到的用于定位代碼在源碼中的位置的調(diào)試符號,符號和斷點(diǎn)定位以及堆棧符號化有很重要的關(guān)系。
2.3.1 Strip Linked Product (STRIP_INSTALLED_PRODUCT)
If enabled, the linked product of the build will be stripped of symbols when performing deployment postprocessing.
如果設(shè)置為yes,打包的時(shí)候會將symbols裁剪。
并不是所有的符號都是必須的,比如 Debug Map,所以 Xcode 提供給我們 Strip Linked Product 來去除不需要的符號信息(Strip Style 中選擇的選項(xiàng)相應(yīng)的符號),去除了符號信息之后我們就只能使用 dSYM 來進(jìn)行符號化了,所以需要將 Debug Information Format 修改為 DWARF with dSYM file。
2.3.2 **Strip Debug Symbols During Copy **(COPY_PHASE_STRIP)
Specifies whether binary files that are copied during the build, such as in a Copy Bundle Resources or Copy Files build phase, should be stripped of debugging symbols. It does not cause the linked product of a target to be stripped—use Strip Linked Product (STRIP_INSTALLED_PRODUCT) for that.
與 Strip Linked Product 類似,但是這個(gè)是將那些拷貝進(jìn)項(xiàng)目包的三方庫、資源或者 Extension 的 Debug Symbol 去除掉,同樣也是使用的 strip 命令。這個(gè)選項(xiàng)沒有前置條件,所以我們只需要在 Release 模式下開啟,不然就不能對三方庫進(jìn)行斷點(diǎn)調(diào)試和符號化了。
2.3.3 Symbols Hidden by Default (GCC_SYMBOLS_PRIVATE_EXTERN)
When enabled, all symbols are declared private extern unless explicitly marked to be exported using attribute((visibility("default"))) in code. If not enabled, all symbols are exported unless explicitly marked as private extern.
意思就是設(shè)置為yes后,所有的symbols都會被申明為private extern,經(jīng)過測試,確實(shí)可以減小包體積。
工程中的設(shè)置如下:
target.build_configurations.each do |config|
config.build_settings['COPY_PHASE_STRIP'] = 'YES'
config.build_settings['GCC_SYMBOLS_PRIVATE_EXTERN'] = 'YES'
config.build_settings['STRIP_INSTALLED_PRODUCT'] = 'YES'
編譯選項(xiàng)優(yōu)化結(jié)果:收益4.2M
3、__TEXT段遷移
iOS的可執(zhí)行文件就是一個(gè)MachO文件,MachO結(jié)構(gòu)主要分為 Header、Load Commands、Data三部分。
Header包含該二進(jìn)制文件的一般信息,字節(jié)順序、架構(gòu)類型、加載指令的數(shù)量等。使得可以快速確認(rèn)一些信息,比如當(dāng)前文件用于32位還是64位,對應(yīng)的處理器是什么、文件類型是什么。Load Commands是一張包含很多內(nèi)容的表。 內(nèi)容包括區(qū)域的位置、符號表、動態(tài)符號表等。它們描述了Data在二進(jìn)制文件和虛擬內(nèi)存中的布局信息,有了這個(gè)布局信息就能夠知道Data在二進(jìn)制文件中和虛擬內(nèi)存中是怎樣排布的。Data存儲了實(shí)際的內(nèi)容,通常是對象文件中最大的部分,包含Segement的具體數(shù)據(jù),如靜態(tài)C字符串,帶參數(shù)/不帶參數(shù)的OC方法,帶參數(shù)/不帶參數(shù)的C函數(shù)。
以下是在MachOView中查看的結(jié)構(gòu):

Data的結(jié)構(gòu)又可以分為多個(gè)Segment,主要有__PAGEZERO 、__TEXT 、__DATA 、__LINKEDIT :
__PAGEZERO是在可執(zhí)行文件有的,動態(tài)庫里沒有。這個(gè)段開始地址為0(NULL指針指向的位置),是一個(gè)不可讀、不可寫、不可執(zhí)行的空間,能夠在空指針訪問時(shí)拋出異常。__TEXT是代碼段,里面主要是存放代碼的,該段是可讀可執(zhí)行,但是不可寫。__DATA是數(shù)據(jù)段,里面主要是存放數(shù)據(jù),該段是可讀可寫,但不可執(zhí)行。__LINKEDIT段用于存放簽名信息,該段是只可讀,不可寫不可執(zhí)行。
其中的每一個(gè)Segment又可以分為一個(gè)或多個(gè)Section,而__TEXT是Data中的一個(gè)Segment。
__TEXT段遷移的方式:
一個(gè)Mach-O文件構(gòu)建的構(gòu)成主要包括 預(yù)處理 -> 編譯 -> 匯編 -> 鏈接 等 4 個(gè)階段。
我們通過在 Other Linker Flags 中添加參數(shù)可以在鏈接期移動Section。
-Wl,-rename_section,__TEXT,__cstring,__RODATA,__cstring
-Wl,-segport,__RODATA,rx,rx
其中 -Wl 的作用就是告訴 Xcode 它后面的參數(shù)是添加給 Ld 鏈接器的,這些參數(shù)將在鏈接階段生效。
第一行參數(shù)會新創(chuàng)建一個(gè) __RODATA 段,并把 __TEXT,__cstring 移動到 __RODATA,__cstring。
第二行參數(shù)是給 __RODATA 賦予可讀和可執(zhí)行權(quán)限。
我們先來看移動__TEXT,__cstring前的 Mach-O 文件:

構(gòu)建完成后再來看一下移動__TEXT,__cstring后的 Mach-O 文件:

這樣就成功的移動了__TEXT段中的一些Section。
facebook早期解決__TEXT段大小限制問題就是使用的這種方式,具體參考: Analysis of the Facebook.app for iOS
Facebook avoids this limitation by moving some if the __TEXT sections into the read only __RODATA segment. Implementing this trick is really simple: you just need to add a linker flag to rename the chosen sections. And it appears you need absolutely nothing at runtime: the renamed sections will be found automatically. This linker flag is described in the ld man page:
-rename_section orgSegment orgSection newSegment newSection
Renames section orgSegment/orgSection to newSegment/newSection.
You could use it to rename the (__TEXT, __cstring) section to (__RODATA, __cstring) by simply adding this line into the Other Linker Flags (OTHER_LDFLAGS):
<pre data-language="plain" id="67Kfj" class="ne-codeblock language-plain" style="border: 1px solid rgb(232, 232, 232); border-top-left-radius: 2px; border-top-right-radius: 2px; border-bottom-right-radius: 2px; border-bottom-left-radius: 2px; background-color: rgb(249, 249, 249); padding: 16px; font-size: 13px; color: rgb(89, 89, 89);">-Wl,-rename_section,__TEXT,__cstring,__RODATA,__cstring</pre>
今日頭條在減小下載大小時(shí)也采用了這種方式,
通過在Other Linker Flags中添加下面的參數(shù),就可以達(dá)到這樣的目的


這里的作用就是對__TEXT段中的section移動到其他section,然后賦予讀權(quán)限和可執(zhí)行權(quán)限。
那么__TEXT段遷移為什么會減小下載大小呢?
原因就是App在上傳到App Store Connect后,蘋果會對其進(jìn)行加密,然后壓縮成ipa。加密對可執(zhí)行文件本身的大小幾乎沒有影響,但是卻大大影響了壓縮效率。而__TEXT段又是加密段中最主要的一部分,通過減小__TEXT段就可以減小加密范圍,所以就可以將__TEXT段中的一些Section遷移到其它Segment中。
優(yōu)化結(jié)果:安裝大小減小0.2M,下載大小減小25M。
4、三方庫相關(guān)
1、推動直播SDK使用精簡版,由于直播場景不需要實(shí)時(shí)音視頻、超級播放器SDK以及 AI 特效組件的能力,所以修改了直播SDK。為了防止更換SDK出現(xiàn)問題,進(jìn)行了兩個(gè)版本的灰度觀察,經(jīng)過充分測試無誤,才確定更換。
2、推動一些SDK的刪除。刪除了一些可以被取代的SDK。
優(yōu)化結(jié)果:收益7M。
四、資源優(yōu)化
資源優(yōu)化主要是對圖片資源和其他一些json、音頻、視頻等資源的優(yōu)化。
1、PNG圖片壓縮
png壓縮主要對比了兩種方案:
TinyPNG
有損壓縮,主要是使用Quantization的技術(shù),通過合并圖片中相似的顏色,通過將 24 位的 PNG 圖片壓縮成小得多的 8 位色值的圖片,并且去掉了圖片中不必要的 metadata,這種方式幾乎能完美支持原圖片的透明度。
網(wǎng)站: https://tinypng.com/
ImageOptim
無損壓縮,圖片文件中往往包含一些注釋、顏色 Profile 等多余信息,移除后圖像質(zhì)量不變,體積更小載入更快。ImageOptim 以此方式壓縮圖片,先分析圖片,找到最優(yōu)壓縮參數(shù),去除無關(guān)信息減小體積。
網(wǎng)站:https://imageoptim.com/mac
經(jīng)過壓縮測試,發(fā)現(xiàn)TinyPNG壓縮效果遠(yuǎn)好于ImageOptim,TinyPNG壓縮比約為65%,ImageOptim壓縮比約為30%,并且肉眼看起來無差異。
使用過程中發(fā)現(xiàn),一些png圖片雖然壓縮后變小了,但是打包后變化并不明顯,有些甚至變大了。通過分析ipa中的png圖片以及查閱資料了解到,由于蘋果本身也會對png圖片進(jìn)行壓縮,這個(gè)壓縮過程是為了加快對圖片的處理速度,將其轉(zhuǎn)換為更方便處理的格式--CgBI格式。并且修改了存放實(shí)際的圖像數(shù)據(jù)的IDAT數(shù)據(jù)塊,改變了決定IDAT數(shù)據(jù)塊大小的Filter方式、zlib的壓縮方式。因?yàn)镃gBI的IDAT是BGRA格式的,所以不管之前的IDAT是否有Alpha通道,在處理的時(shí)候,都會增加alpha通道,其次就是因?yàn)槊恳恍袛?shù)據(jù)的filter不同,蘋果處理的時(shí)候,默認(rèn)每一行都使用相同的filter,而原始文件則可以通過更好的算法,對不同的數(shù)據(jù)行使用不同的filter,為后面的數(shù)據(jù)壓縮提供更容易壓縮的數(shù)據(jù)。因此蘋果對于png的優(yōu)化可能會導(dǎo)致部分png圖片變大的情況。
所以對于部分壓縮后的png圖片,我們也會采用轉(zhuǎn)為WebpP的方式進(jìn)行進(jìn)一步處理。
優(yōu)化結(jié)果:收益5M
2、PNG圖片轉(zhuǎn)為WebP圖片
相較于PNG格式, WebP具有更加優(yōu)秀的圖像數(shù)據(jù)壓縮算法,能帶來更小的圖片體積。所以會對一些較大的圖片轉(zhuǎn)換為WebP圖片。
壓縮采用的是: cwebp -- Compress an image file to a WebP file
安裝方式:brew install webp
使用方式:
cwebp [options] -q quality input.png -o output.webp
其中option可選:-loss(有損壓縮,默認(rèn)),-lossless(無損壓縮)
-q:質(zhì)量指數(shù)(壓縮率),有損壓縮有效,無損壓縮忽略
input.png:待轉(zhuǎn)換圖片
-o:輸入圖片名稱格式
#png 轉(zhuǎn)換為webp
toWebp() {
filePath=echo $1 |sed 's/ /\ /g'
fileName={fileName##*/}
fileName=echo $fileName|sed 's/ /_/g'
fileName=${fileName%.*}
# 靜默模式 轉(zhuǎn)換時(shí)將不會打印轉(zhuǎn)換日志
if [[ -e $LOCAL_CWEBP_PATH ]]; then
cwebp -quiet "$filePath" -o $newFilePath$fileName.webp
else $basedir/bin/cwebp -quiet "$filePath" -o $newFilePath$fileName.webp
fi
echo $filePath
printResult $? "${filePath##*/} ? $newFilePath$fileName.webp"
}
在轉(zhuǎn)換時(shí),本以為腳本將png圖片轉(zhuǎn)為webp圖片,然后hook圖片加載方式就可以讀取webp圖片了,但是發(fā)現(xiàn)png圖片是由imageset管理的,代碼中使用的圖片名稱和png圖片名稱可能不一致。
解決方法:腳本轉(zhuǎn)換成webp的時(shí)候,不能直接使用png圖片名稱,而是要使用管理png的imageset名稱。
解決完,run起來后又發(fā)現(xiàn)了另外一個(gè)問題,圖片展示都放大了,經(jīng)過排查,發(fā)現(xiàn)png有1x、2x、3x三種,一個(gè)60x60像素的3x圖片生成的UIImage對象scale為3,size為20x20,但是轉(zhuǎn)為webp再生成UIImage之后,UIImage的scale為1,size為60,所以顯示時(shí)圖片變大了。還好在SDImageCoder中找到了一個(gè)修改scale參數(shù)的轉(zhuǎn)換方法。
/**
Decode the image data to image.
@note This protocol may supports decode animated image frames. You can use `+[SDImageCoderHelper animatedImageWithFrames:]` to produce an animated image with frames.
@param data The image data to be decoded
@param options A dictionary containing any decoding options. Pass @{SDImageCoderDecodeScaleFactor: @(1.0)} to specify scale factor for image. Pass @{SDImageCoderDecodeFirstFrameOnly: @(YES)} to decode the first frame only.
@return The decoded image from data
*/
- (nullable UIImage *)decodedImageWithData:(nullable NSData *)data
options:(nullable SDImageCoderOptions *)options;
但是另外一個(gè)問題又出現(xiàn)了,本想著統(tǒng)一由3x的png圖片轉(zhuǎn)為webp,然后scale參數(shù)傳3,但是由于以前圖片管理不規(guī)范,導(dǎo)致png圖片有些只有1x圖,有些只有2x或者3x圖,所以這里還需要根據(jù)webp是哪種png圖片轉(zhuǎn)換來的,傳對應(yīng)的scale參數(shù)。
然后后面測試時(shí)又發(fā)現(xiàn)部分圖片加載不出來,排查發(fā)現(xiàn)這些圖片是在xib中讀取的,而xib讀取png的方式并不通過imageNamed方法,然后首先第一個(gè)想法自然是hook xib讀取png的方法,但是蘋果并沒有暴露給我們xib加載png的方法。也有一些資料說可以通過hook UINibDecoder的decodeObjectFotKey方法,但是覺得并不十分嚴(yán)謹(jǐn),所以xib中的使用的png, 我采用了另外一種方式:在代碼中將控件使用imageNamed方法再讀取一遍圖片。
還有一點(diǎn)需要注意的是,一些可以支持區(qū)域拉伸的png圖轉(zhuǎn)為webp后拉升是會變形的。這部分圖是不適合轉(zhuǎn)為webp的。
優(yōu)化結(jié)果:收益6M
3、修改組件庫中圖片管理方式
Asset Catalog,是Xcode提供的一項(xiàng)圖片資源管理方式。每個(gè)Asset表示一個(gè)圖片資源,但是可以對應(yīng)一張或者多張PNG圖,比如可以提供@1x, @2x, @3x多張尺寸的圖進(jìn)行適配;
Asset Catalog中的圖片,在編譯時(shí)會被壓縮,然后在App運(yùn)行時(shí),可以通過API動態(tài)根據(jù)設(shè)備scale factor來選擇對應(yīng)的真實(shí)的圖片渲染,使用Asset Catalog管理的圖片會在ipa包中生成一個(gè)Assets.car文件。
App Thing,是蘋果平臺上的一個(gè)用于優(yōu)化App包下載資源大小的方案。在App包提交上傳到App Store后,蘋果后臺服務(wù)器,會對不同的設(shè)備,根據(jù)設(shè)備的scale factor,重新把App包進(jìn)行精簡,這樣不同設(shè)備從App Store下載需要的容量不同,3x設(shè)備不需要同時(shí)下載1x和2x的圖。
但是,這套機(jī)制直接基于Asset Catalog,也就是說,只有在Asset Catalog中引入的圖片,才能享受到App Thinning。直接拷貝到App Bundle中的散落圖片,所有設(shè)備還是都會全部下載。
因此盡量提升Asset Catalog利用率,是一個(gè)很大的包大小優(yōu)化點(diǎn)。

所以在使用cocoapods進(jìn)行組件庫管理時(shí),組件庫中的PNG圖片也都使用Asset Catalog來管理。
除此之外還有一個(gè)資源引入方式的不同:pod中的資源引入方式有兩種,resource_bundles和resources。
使用resources,會在主bundle中導(dǎo)入。這種方式讀取圖片不需要修改讀取方式。
s.resources = ['ResourcesTest/Assets/*.xcassets']
使用resource_bundles,會在主bundle中生成一個(gè)自定義的bundle,bundle中存放著資源。讀取資源時(shí)需要到對應(yīng)bundle下讀取。這種方式可以避免命名沖突。
s.resource_bundles = {
'ResourcesBubdlesTest' => ['ResourcesBubdlesTest/Assets/*.xcassets']
}
在修改的過程中,也有一些意外收獲,發(fā)現(xiàn)項(xiàng)目中部分組件庫引入資源方式存在問題,同時(shí)指定了resource_bundles和resources兩種方式,這樣會導(dǎo)致圖片既存在main bundle中又存在resource_bundles生成的bundle中。
這里推薦使用resource_bundles + Asset Catalog的方式來管理組件庫中的PNG圖片。
優(yōu)化結(jié)果:收益4.5M
4、刪除無用PNG圖片
通過工具篩查:LSUnusedResources

優(yōu)化結(jié)果:刪除圖片56張,收益1.2M
5、壓縮文本文件
玩物得志APP中有一些Lottie動畫的json文件存放在本地,通過對這些文件的打包壓縮也取得了一些優(yōu)化效果。
將本地中較大的json文件放到一起壓縮成zip;
啟動時(shí)在異步線程解壓zip,存放到沙盒中;
運(yùn)行時(shí)從沙盒中讀取json;

后續(xù)可以對更多的資源類型采用這種方式,比如音頻、視頻。
優(yōu)化結(jié)果:收益1.2M
五、包大小監(jiān)控
為了控制增量,我們也對每個(gè)版本做了包大小的監(jiān)控。
針對可執(zhí)行文件的變化,我們采用的是LinkMap來分析每個(gè)組件的大小變化并進(jìn)行記錄。
針對資源的變化,我們也會從每個(gè)版本的ipa包中分析出資源大小的變化并記錄。

以后,我們計(jì)劃對增量進(jìn)行卡口,結(jié)合盤古打包平臺進(jìn)行包體積自動分析,在每次分支被合并到主分支之前,就能體現(xiàn)出增量大小,這樣可以使得開發(fā)對自己開發(fā)的代碼有更直觀的感受,加強(qiáng)開發(fā)在日常編碼中的瘦身意識,有意識的去對資源、代碼進(jìn)行清理。爭取做到包體積的零增量。
六、效果
以上所有方案都在玩物得志APP中進(jìn)行了實(shí)踐和落地,經(jīng)過一系列優(yōu)化,玩物得志APP包體積整體收益如下:
下載大小由136.2M降為78.6M,減小57.6M
安裝大小由174.5M降為140M,減小34.5M

下載大小最直觀的體現(xiàn)就是下載時(shí)間的長短,以下對比了3.5.0和3.6.7兩個(gè)版本的下載安裝時(shí)長:
優(yōu)化前:下載安裝時(shí)長為64s
優(yōu)化后:下載安裝時(shí)長為43s
下載時(shí)長縮短32.8%
以下是記錄的優(yōu)化以來各個(gè)版本下載大小與安裝大小的變化:
