燃燒app的卡路里--app瘦身之路

引言

app隨著需求增加,體積逐步增大,影響用戶的安裝意愿。所以需要對(duì)app進(jìn)行瘦身,輕裝上陣~

我們的原則就是:

  • “知己知彼”
    我們需要了解app由哪些部門(mén)構(gòu)成,哪些體積加起來(lái)構(gòu)成了最終臃腫的app?
  • “高效打擊”
    其次我們要對(duì)不同組成部分進(jìn)行剖析,精確重點(diǎn)地對(duì)重要的模塊進(jìn)行優(yōu)先處理,提高處理效率。為什么叫“高效打擊”,因?yàn)槲覀兊氖萆硇枨蟛皇且豁?xiàng)研究論文,可以長(zhǎng)期深入的探索,通常是一個(gè)要在指定周期內(nèi)完成的工作,所以我們需要有效率,快速地提升,所以本文的方式就是從最大可處理文件開(kāi)始,依次處理更小的文件。畢竟對(duì)大文件處理得到的收益是最明顯的。

所以本文主要方法就是按大小排序待處理的文件,分析每個(gè)待處理文件如何構(gòu)成的,然后對(duì)構(gòu)成文件遞歸分析構(gòu)成、排序......


IPA文件構(gòu)成

我們給用戶或者appstore上傳的應(yīng)用文件,其實(shí)就是ipa文件,通過(guò)xcode中的archive而來(lái)。所以對(duì)于應(yīng)用瘦身我們就是首先要對(duì)ipa文件進(jìn)行瘦身。那么ipa文件是什么,包含什么? 我們先給出總圖,再逐步說(shuō)明:


ipa解析

ipa文件其實(shí)就是一個(gè)壓縮文件,我們通過(guò)解壓縮看到內(nèi)部情況:

ipa文件 = app文件 + 圖片文件 + meta信息 + plist文件

其中后面三項(xiàng)都是無(wú)法優(yōu)化改動(dòng),是打包時(shí)生成的。所以現(xiàn)在我們ipa瘦身就等同于對(duì)app文件瘦身。


app文件瘦身:

app文件其實(shí)就是我們xcode中run出來(lái)的產(chǎn)出,也是一個(gè)文件夾,我們通過(guò)“查看包內(nèi)容”,可以查看內(nèi)部情況,主要包含:
app文件 = 可執(zhí)行文件 + codingsign文件 + nib文件 + Assets.car+ 圖片 +視頻 + framework +語(yǔ)言包+ rn/ js/html文件 ......

每個(gè)app都不盡相同,可能與上述有所不同,但這不是重點(diǎn),重點(diǎn)是我們要逐個(gè)對(duì)這些文件進(jìn)行分析處理。我們目標(biāo)是高效打擊,那就首先對(duì)這些文件按照大小進(jìn)行排序, 按照排序后文件進(jìn)行逐個(gè)擊破!但是本文描述的順序是按照處理的難易程度來(lái)排列的,便于讀者閱讀。

  • _CodeSignature:

文件的 hash 列表。里面有一個(gè)文件 CodeResources 是一個(gè)屬性列表,包含 bundle 中所有其他文件的列表。用來(lái)判斷一個(gè)應(yīng)用程序是否完好無(wú)損。

  • Assets.car

Assets.xcassets 好處是不同分辨率的圖片好管理\工程打包后會(huì)對(duì)圖片進(jìn)行壓縮. 如果將圖片直接放在工程目錄下面,打包后文件散落在包里面,不會(huì)對(duì)圖片進(jìn)行壓縮,而如果放在xcassets中,會(huì)將這些圖片(AppIcon和LaunchImage是直接放在包中的)統(tǒng)一壓縮成一個(gè)Assets.car的文件。獲取Assets.car里面的圖片需要用到一個(gè)命令行工具叫cartool:https://github.com/steventroughtonsmith/cartool

  • 語(yǔ)言字體包

對(duì)于一些定制設(shè)計(jì)的語(yǔ)言或者字體包,可以考慮是否有必要?是否可以通過(guò)下載形式?

  • framework

如果我們的工程引入動(dòng)態(tài)framework,這些framework會(huì)被引入到app文件夾的一個(gè)framework文件中,直接占用整個(gè)app的體積,所以對(duì)于framework的瘦身我們通??梢园凑湛蓤?zhí)行文件相似的原理來(lái)處理。

  • 大視頻文件

視頻文件我們首先要考慮是否可以通過(guò)網(wǎng)絡(luò)下載來(lái)使用,然后再考慮是否可以換成其他壓縮編碼效率更好的類(lèi)型文件。

  • 大音樂(lè)文件

例如wav文件這種無(wú)損音頻格式,雖然質(zhì)量好,但是文件很大,我們可以選擇合適的有損音頻格式來(lái)有效減小體積。

  • 動(dòng)圖

gif格式的動(dòng)圖,是直接將圖片放在一起播放,并沒(méi)有去除時(shí)間冗余度,所以可壓縮空間很大,我們可以考慮換成其他格式的動(dòng)圖。

  • 圖片

對(duì)于圖片我們需要從兩方面處理,刪減+壓縮。
首先使用LSUnusedResources(https://github.com/tinymind/LSUnusedResources)工具找到無(wú)用圖片,要注意代碼中可能包含拼接文件名字的圖片。
然后我們需要考慮是否對(duì)某些大圖進(jìn)行壓縮,例如考慮是使用jpg還是png,還是WebP,當(dāng)然更高的壓縮編碼可能帶來(lái)圖片質(zhì)量的下降,需要權(quán)衡。

  • RN/js/html代碼

對(duì)于RN/js/html代碼,我們需要定期清理不用的靜態(tài)代碼,并按照oc源碼相似原理來(lái)清除無(wú)用和過(guò)載的代碼。

  • 可執(zhí)行文件

經(jīng)過(guò)排序后,排在第一位的通常就是與app同名的這個(gè)沒(méi)有后綴的文件,這個(gè)文件是什么呢?它是怎么構(gòu)成的呢?


可執(zhí)行文件瘦身

首先我們通過(guò)命令看一下:

file 可執(zhí)行文件

這個(gè)命令會(huì)輸出:

xxx可執(zhí)行文件: Mach-O executable arm_v7

可見(jiàn)這個(gè)文件是只包含arm_v7的executable文件。這樣便可得到這個(gè)文件的架構(gòu)信息,通常一個(gè)可執(zhí)行文件可能包含armv7 arm64等架構(gòu)。

1. 這里我們首先有一個(gè)可優(yōu)化點(diǎn):可執(zhí)行文件是否包含不需要的架構(gòu)?

例如模擬器的X86_64 I386首先是不能包含在其中的,因?yàn)閍ppstore不允許,并且也是不需要提供給用的。另外是否需要支持armv7 也可以根據(jù)業(yè)務(wù)和需求來(lái)決定,如果可執(zhí)行文件的size包含n個(gè)架構(gòu),通常每個(gè)架構(gòu)的體積占到size/n,所以處理掉不需要的架構(gòu),帶來(lái)的體積減小是最客觀的!

處理的方式,一種是配置xcode的build settings中architecture,然后重新編譯打包;另一種是使用lipo -thin命令,好處是不需要重新編譯打包。(由于涉及到簽名,這種方式無(wú)法提交appstore)

2. 架構(gòu)處理之后,我們需要對(duì)最終保留的指定架構(gòu)的可執(zhí)行文件進(jìn)一步處理:


可執(zhí)行文件

可執(zhí)行文件是有工程中的所有代碼編譯生成,通常由幾部分組成: mach header + load command + segment1 + segment2 ......

  • header
    我們可以通過(guò)otool的一些指令查看這個(gè)可執(zhí)行文件,例如查看header部分:
otool -h 可執(zhí)行文件

輸出結(jié)果如下:

Mach header

magic cputype cpusubtype caps filetype ncmds sizeofcmds flags

MH_MAGIC ARM V7 0x00 EXECUTE 55 5644 NOUNDEFS DYLDLINK TWOLEVEL WEAK_DEFINES BINDS_TO_WEAK PIE

還可以使用otool -l xxx輸出圖中第二個(gè)部分的load command,其他命令可以通過(guò)otool命令自己查看。

  • 加載命令
    Mach-O中最重要的部分,它說(shuō)明了操作系統(tǒng)應(yīng)當(dāng)如何加載文件中的各個(gè)segment數(shù)據(jù)。
  • 段數(shù)據(jù)(Segments)
    每一個(gè)segment定義了一些Mach-O文件的數(shù)據(jù)、地址和內(nèi)存保護(hù)屬性,這些數(shù)據(jù)在動(dòng)態(tài)鏈接器加載程序時(shí)被映射到了app所屬進(jìn)程的虛擬內(nèi)存中。
    1). __PAGEZERO: 空指針陷阱段,映射到虛擬內(nèi)存空間的第一頁(yè),用于捕捉對(duì)NULL指針的引用;
    2). __TEXT: 包含了執(zhí)行代碼以及其他只讀數(shù)據(jù)。 為了讓內(nèi)核將它 直接從可執(zhí)行文件映射到共享內(nèi)存, 靜態(tài)連接器設(shè)置該段的虛擬內(nèi)存權(quán)限為不允許寫(xiě)。
    3). __DATA: 包含了程序數(shù)據(jù),該段可寫(xiě);
    4). __OBJC: Objective-C運(yùn)行時(shí)支持庫(kù);
    5). __LINKEDIT: 含有為動(dòng)態(tài)鏈接庫(kù)使用的原始數(shù)據(jù),比如符號(hào),字符串,重定位表?xiàng)l目等等。

綜上可知,我們工程中的代碼(包括常量、變量、字符串、代碼)和動(dòng)態(tài)庫(kù)等都被鏈接到了這個(gè)可執(zhí)行文件,所以這些元素都是我們的優(yōu)化點(diǎn)。從哪入手呢?

工程配置

首先從工程配置入手:
我們通過(guò)對(duì)xcode的工程進(jìn)行配置,也可以減小可執(zhí)行文件:

  • Strip Link Product設(shè)成YES,通常默認(rèn)開(kāi)啟;
  • 去掉異常支持,微信號(hào)稱減重明顯,但在我們的工程中不明顯。配置過(guò)程中,遇到了坑,這里列出來(lái):

Enable C++ Exceptions和Enable Objective-C Exceptions設(shè)為NO,并且Other C Flags添加-fno-exceptions。 這種配置之后如果你的工程中沒(méi)有使用try 或者 throw語(yǔ)句,可以編譯通過(guò),但是使用了try 或者 throw語(yǔ)句將會(huì)報(bào)錯(cuò),大意是含有這些語(yǔ)句的文件不支持異常捕獲。 這時(shí)我們需要對(duì)這些文件的編譯選項(xiàng)進(jìn)行配置,在build phases中查找到編譯的源文件,然后添加compile flags。 這里需要注意我們需要根據(jù)這個(gè)文件類(lèi)型添加不同的compile flag,對(duì)于oc文件需要使用下圖的-fobjc-exceptions,而對(duì)于c++文件,需要使用-fexceptions,添加不當(dāng)?shù)脑?,編譯還是會(huì)報(bào)錯(cuò)。


build settings
compile flag

代碼

LinkMap文件是Xcode產(chǎn)生可執(zhí)行文件(Mach-O)的同時(shí)生成的鏈接信息,用來(lái)描述可執(zhí)行文件的構(gòu)造成分,包括代碼段(__TEXT)和數(shù)據(jù)段(__DATA)的分布情況。首先需要在工程中打開(kāi):XCode -> Project -> Build Settings -> 搜map -> 把Write Link Map File選項(xiàng)設(shè)為yes,并指定好linkMap的存儲(chǔ)位置

LinkMap里展示了整個(gè)可執(zhí)行文件的全貌,分為三段,分別是:

  • 以# Object files:為分割標(biāo)志,列出所有.o目標(biāo)文件的信息(包括靜態(tài)鏈接庫(kù).a里的),
  • 以# Sections:為分割標(biāo)志,描述各個(gè)段在最后編譯成的可執(zhí)行文件中的偏移位置及大小,包括了代碼段(__TEXT,保存程序代碼段編譯后的機(jī)器碼)和數(shù)據(jù)段(__DATA,保存變量值),字段的含義在Mach-o中已詳細(xì)介紹。
  • 以# Symbols:為分割標(biāo)志,列出具體的按每個(gè)文件列出每個(gè)對(duì)應(yīng)字段的位置和占用空間

有個(gè)LinkMap分析工具:https://github.com/huanxsd/LinkMap,可以排序列出當(dāng)前鏈接的所有的.o文件,這些就是我們需要優(yōu)化的地方。不管是三方庫(kù)還是我們自己代碼中的o文件,都是我們需要逐個(gè)考慮的,尤其是排序靠前的文件。主要包含兩類(lèi)問(wèn)題需要處理:

1. 過(guò)載
引入某些三方庫(kù),我們可能只是用其中一個(gè)或幾個(gè)類(lèi),而其他所有類(lèi)都是不會(huì)使用的,但是作為一個(gè)靜態(tài)庫(kù),也會(huì)被鏈接到可執(zhí)行文件中,這時(shí)我就需要將這些不需要的類(lèi)在編譯的工程中移除,使其不進(jìn)入到最終的靜態(tài)庫(kù)。例如某加密解密三方庫(kù),我們可能只使用其中一種加密解密的類(lèi),那么其他的類(lèi)文件都可以從工程中刪除。

2. 無(wú)用類(lèi)
例如afnetworking三方庫(kù),如果我們的代碼中只是簡(jiǎn)單一個(gè)http請(qǐng)求,用處很少,那就可以考慮將afnetworking移除,因?yàn)閍fnetworking涉及到的o文件總體積可能將近1M。 如果我們?cè)谀硞€(gè)子工程可能只用到一個(gè)http請(qǐng)求,并且不需要復(fù)雜的處理,那么完全可以自己使用NSUrlsession來(lái)實(shí)現(xiàn)請(qǐng)求,而不必引入afnetworking。

由于這里是對(duì)app的可執(zhí)行文件所有o文件進(jìn)行排序,會(huì)將所有三方庫(kù)的o文件混雜一起,為了便于查到精確處理,我們通??梢詫⑵渲写蟮娜綆?kù)單獨(dú)處理,可以使用ar -x命令,將庫(kù)解壓縮成o文件的集合,然后按照大小排序,再按照上述方式進(jìn)行處理。

ar -x  靜態(tài)庫(kù)

對(duì)代碼的處理我們只做到這個(gè)粒度,如果時(shí)間允許,可以進(jìn)行更小粒度的處理,例如找到無(wú)用的方法、重復(fù)的方法進(jìn)行屏蔽。當(dāng)然更小粒度意味著對(duì)代碼處理的時(shí)間更多,收益沒(méi)有文件級(jí)別大。微信使用了下面的方法:

1. 查找無(wú)用selector
結(jié)合LinkMap文件的__TEXT.__text,通過(guò)正則表達(dá)式([+|-][.+\s(.+)]),我們可以提取當(dāng)前可執(zhí)行文件里所有objc類(lèi)方法和實(shí)例方法(SelectorsAll)。再使用otool命令otool -v -s __DATA __objc_selrefs逆向__DATA.__objc_selrefs段,提取可執(zhí)行文件里引用到的方法名(UsedSelectorsAll),我們可以大致分析出SelectorsAll里哪些方法是沒(méi)有被引用的(SelectorsAll-UsedSelectorsAll)。注意,系統(tǒng)API的Protocol可能被列入無(wú)用方法名單里,如UITableViewDelegate的方法,我們只需要對(duì)這些Protocol里的方法加入白名單過(guò)濾即可。

2. 查找無(wú)用oc類(lèi)
通過(guò)otool命令逆向__DATA.__objc_classlist段和__DATA.__objc_classrefs段來(lái)獲取當(dāng)前所有oc類(lèi)和被引用的oc類(lèi),兩個(gè)集合相減就是無(wú)用oc類(lèi)。

總結(jié):方法很多,最重要是根據(jù)自己的需求、項(xiàng)目緊迫程度,選擇合適的方法來(lái)進(jìn)行,而不是盲目扎到某一方面。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容