APK 包瘦身:追上那個胖子
引子
APK 大家肯定都很熟悉了,安卓應用安裝包文件。而APK的尺寸對于每個產品來說都是一個非常重要的指標。對于如何減小這個數字,有無數的前人總結的或全面、或零散的經驗,許多團隊也對此做過各種各樣的努力,說實話也是一塊嚼爛了的口香糖。
如何在此基礎上再咀嚼出一絲甜味、再翻滾出新的厚度呢,這個是筆者一直在苦苦思索的問題。
仔細閱讀很多其他團隊總結出的APK瘦身相關的文章,大體都是講一個APK包已經胖成那( nèi )樣了我們如何讓它瘦下來,這是一種促成式的結果導向的思維方式。這種哪胖減哪的方式存在什么問題呢?相信很多同學都與我有同樣的遭遇:一次壓制、后續報復性反彈。
道家言:一生二、二生三、三生萬物,知一、方能知二知三。因此筆者還是想要從根源上去解釋APK尺寸這個問題: 一個APK包從根本上如何長到這么胖的,我們如何能在如此頻繁的項目迭代中保持它的身材呢 ? 本文將從這個角度來說明我們APK各部分增大到底是因為什么,以及我們對于APK尺寸的影響因素都有哪些誤解,繼而得出作為開發者的我們怎樣才能 從過程上去避免 APK尺寸過分增大的問題。
這大體是一次面向過程的勘探。一些拙見,希望能夠提供些新的思路、也歡迎大家指出錯誤、互相交流、提出建議。
項目初始:APK誕生之初
首先提出一問題: 一個最小的HelloWorld應用APK尺寸可以有多小? 帶著這個問題思考,我做了幾組循環對照試驗,以便于我們對APK尺寸有一個直觀的初始認識。在這里我也同時測試了一下很多人關心的,常常作為依賴庫引入的support-v4、support-v7、以及design包,對于APK尺寸的最小影響。
Android Build Tools版本:25.0.2
構建工具:gradle_2.2.0
support-v4版本:25.3.1
support-v7版本:25.3.1
app包含內容:ic_launcher.png(3kb)、helloworld代碼及必要資源
(備注:以上混淆指的是代碼混淆,資源混淆在后文中討論,需要引入第三方庫)
通過上面的實驗,我們對于簽名、proguard以及第三方庫引用對于apk尺寸產生的影響應該有了一個更加直觀的認識。
精簡APK包的包內結構析
圖1:精簡APK包結構分析圖
通過反編譯神器 Jadx-gui 對剛剛生成的最小簽名包進行反編譯,可以發現APK包結構主要有以上5部分構成。
那么這五個必要部分、每個部分的到底各自包含一些什么信息呢?
APK包內成分詳解:你真的了解它們嗎
1. META-INF
META-INF文件夾是做什么的,在jar文件中,我們常常能看見它的身影。要論它的年齡,比Android要大的多。在Android還沒有出生的時候,META-INF就已經被廣泛用于jar包中存放各種發布、包安全、構建等輔助信息。在Android的APK中,它也承擔了類似的職能,主要用于簽名驗證相關信息的存放。
Android APK 中META-INF的結構:
圖2:META-INF結構分析圖
META-INF文件夾內一般會包含三個信息:MANIFEST.MF、 .SF文件及 .RSA文件。其中MANIFEST.MF是常駐居民,而.SF文件和.RSA文件在簽名時才會生成。
MANIFEST.MF 文件
圖3:MENIFEST.MF 文件內容
如果未簽名,MANIFEST.MF文件中只會保留最最基本的構建信息。簽名后,文件中會增加APK包內所有文件名及對應的SHA1-Digest的數據指紋,每個三行、排列整齊。
.SF 文件
圖4:.SF 文件內容
可以看見,.SF文件的結構與MANIFEST.MF文件類似。.SF文件中,包含了MANIFEST.MF文件的SHA1-Digest后的數據指紋,同時包含MANIFEST.MF中每個資源[名稱 - 指紋]鍵值對字符串、SHA1-Digest后的數據指紋。 也正因此,.SF文件的尺寸一般來說與MANIFEST.MF文件的尺寸差不多。
:圖5:MANIFEST.MF 文件中的一條資源 [名稱 - 指紋]鍵值對
.SF文件對這三行、包含換行符進行SHA1-Digest
.RSA 文件
.RSA文件是一個特殊格式的文件,特定的數據存放在特定的偏移位置,不能直接用文本編輯器打開,可以用keytool、openssl等命令解析其中的相關參數。
.RSA文件內部包含了簽名公鑰、.RSA文件本身一些信息的(SHA1、SHA256、MD5) + 私鑰加密指紋、以及.SF文件的私鑰加密指紋。
.RSA文件大小基本固定在1k左右,不會隨新增資源、新增代碼而增大。
以上三個文件環環相扣,用于安裝app時校驗包的完整性、是否被第三方篡改。
2. AndroidManifest.xml
AndroidManifest.xml這個文件應該都比較清楚了, 包含四大組件的定義、權限的定義等等,本身內容沒有被加密,為純文本文件,因此通常不會超過100kb大小。需要注意的:如果引入第三方庫中同時定義了AndroidManifest.xml,在打包最后會合并成一個大的AndroidManifest.xml文件,這也是一塊需要留意的尺寸增量。
3. classes.dex文件
classes.dex包含了代碼類的class,當方法數很多開啟Multidex的時候,可能會見到classes1.dex、classes2.dex等多個dex的存在。如果代碼中,引用了第三方庫,那么第三方庫中的類和方法也會被打入dex內。
4. res文件夾
res文件夾里有什么呢?想當然地很多人會認為所有的資源文件都會打包至res文件夾內。其實不然,res中會存放所有的文件資源,例如png文件、xml定義的drawable文件等等。而color、dimen、string等逐條定義在xml文件中的資源,并不會被包含在這里,而是被存放在resources.arsc中。
5. resources.arsc文件
resources.arsc文件夾里包含了所有有id的資源的[索引-類型.名稱-路徑](可以參照R文件)、以及color、dimen、string等資源的[索引-類型.名稱-取值]。
需要注意的,該文件是一種特殊格式的文件,特定的數值存在于特定的偏移位置,不能直接用普通的文本編輯器打開。想要查看它的內容,需要用特定的工具,如下圖為反編譯神奇Jadx-gui_0.6.1的解析結果。(需要注意的是Jadx-gui_0.6.0版本對resources.arsc的解析有bug,不能顯示)。
圖6:resources.arsc文件內容示例
6. lib文件夾
這次簡單的實驗中并沒有lib文件夾的生成,然而在實際的項目中,我們可能會有一些.so庫的直接或間接依賴。.so文件占用的尺寸也不容小覷。百度瀏覽器的apk成分中,so占用的空間就是最大的。
7. 總結
通過以上分析,可以得出以下結論:
APK SIZE 守衛實戰
通過前面的實驗和總結,APK包的各部分結構及相關的增長原因的應該比較清晰了。實際一輪又一輪密集的版本迭代中,我們如何去守住APK的尺寸呢?
視覺需求:圖片資源增加
1. 最好的圖片格式是什么
首先對比一下PNG、JPEG、WEBP三種熱門圖片格式的優劣:
webp壓縮率實測
很多人都好奇webp到底有多小,筆者在這里進行了一個小小的實驗,實際測試下png無損壓縮為webp格式后尺寸的變化:
圖7:轉化前png圖片:5.7kb
圖8:轉化后webp圖片:4.4kb
這張圖片的壓縮率在 23% 左右。大量實驗時,也存在個別png轉化為webp后尺寸反而增大的現象。 總體壓縮率在20+% 。Google官方給出的數據為26% 左右。
2. 管中窺豹:PNG壓縮算法
雖然webp近年來很受歡迎、使用量有增長之勢,但對于Android應用而言,PNG還是主流的圖片格式。PNG的精妙之處就在于它的壓縮算法。如果想要初步了解PNG壓縮算法,筆者在翻博客的時候發現一個很有意思的小腳本: pngthermal ,可以幫助我們去理解png的壓縮算法是怎么計算的。
如下圖9,我們可以看到正如傳統的熱力圖所示,png壓縮度高的地方對應呈現藍色、png壓縮度低的地方對應呈現綠色、黃色甚至紅色。
通過圖一對比我們首先得出一個直觀又簡單的推斷:① 純色部分壓縮度高、顏色變化復雜處壓縮度低
圖9:圖片對比一
再對比一下官方給出的圖片,我們可以再次得出以下推斷:② 重復的部分會被壓縮掉 、③ 線性漸變部分壓縮度高(接近與純色的壓縮度)、非線性顏色變化部分壓縮度低
圖10:圖片對比二
對于簡單理解png尺寸的影響因素,以上三個推斷已經比較充足。如果有同學對png壓縮算法有進一步的興趣。請參閱以下國外大牛寫的文章,其中詳細列舉了png的優化算法: https://medium.com/@duhroach/how-png-works-f1174e3cc7b7 。
3. 圖片資源增加應對方案
首先根據前文的分析,我們得出增加一個圖片會影響apk的哪些部分:
a) res文件夾中會增加該圖片
b) 會增加META-INF中兩個簽名相關文件的大小
c) 會增加resources.arsc文件的大小
圖片是否必要?能否復用已有圖片?
對于只有色彩變化的圖片,可以用ColorFilter一張圖輕松實現。
圖11:ColorFilter實現示例
對于只有簡單旋轉、位移、裁切、形變的圖片,可以一張圖Matrix輕松實現。
對于幀動畫,如果是簡單的旋轉、位移、裁切、形變(如下圖11),可以用代碼實現的盡量用代碼實現。
圖12:代碼實現示例
大圖是否能切割成小圖?能否去除留白?
對于大圖而言,有效信息往往只有幾塊,剩余的是大量的留白,甚至是不規則紋理型留白。對于png圖片來說,留白部分也是會占用空間的。而在我們的實際項目中,視覺同學往往會為了留白的質感,給留白添加紋理。不規則紋理型留白,會災難性地增加png尺寸(從png壓縮章節我們就可以看出)。
是否能變換成.9圖?
對于圓角icon、聊天框、等圖片,可以做成.9圖,一張圖片到處復用。
圖13:.9格式圖片
是否能提供位深8bit的圖片?
對于應用于手機上的圖片,位深32bit的圖片與位深8bit的圖片,區別在于支持的顏色多少,在高質顯示屏上可以看出細微差別,而對于用戶肉眼感知而言相差無幾,前者的尺寸是后者的好幾倍。
圖片是否能夠壓縮?
圖片壓縮基本可以分成兩種思路:在當前格式的基礎上進行有損壓縮、或轉換圖片格式。
有很多png壓縮工具或線上png壓縮網站(tinypng.com)支持圖片的有損壓縮。他們的原理大同小異。
原理:首先會將高位深色值轉化為近似的低位深(8bit)色值。其次會根據png壓縮的特性,將一些不規則的躍遷點去掉或者使之趨于線性分布,以保證較高的壓縮率。最后會去掉一些沒有用的metadata。
百度瀏覽器歷史版本中對png進行有損壓縮后,圖片總體積減少了27.5%
4. 資源壓縮的一些坑、一些黑科技和一些TRICKY的點
a) AAPT: aapt有個選項要關閉,在build.gradle中需要設置cruncherEnabled = false不然資源會被再壓一次,aapt可能會『幫助』你把已經壓好的資源壓縮得更大。
b) 資源混淆: 這個Android官方并沒有給出靠譜方案,然而存在一些好用的第三方庫。例如Github上微信團隊提出的一個資源混淆方案: https://github.com/shwenzhang/AndResGuard
原理上,資源混淆是將資源的路徑名稱縮短,繼而減少resources.arsc的大小、 同時減少META-INF中簽名文件的大小。
實測開啟AndResGuard資源混淆后, APK尺寸可以減少1MB左右 。
c) shrinkResources:gradle2.0.0版本后增加了這個看起來很牛的屬性,需要配合minifyEnabled屬性(也就是新版的混淆屬性)一起用。它的作用是會幫助你把混淆期間被標記到的、沒有被引用的資源變得成很小的默認格式。實際使用中則有些尷尬,這個屬性可能會影響一些非直接引用到的資源文件,導致不可預期的bug。
d) 圖標可以換成矢量圖資源,變成字體文件。例如阿里巴巴給出的iconfont方案,就是以矢量圖的字體文件來替換圖像資源的思路。使用iconfont方案替換一些簡單圖標后,百度瀏覽器中所有簡單圖標的尺寸可以降低近50%。
e) .9圖片不宜直接使用工具或在線網站壓縮,因為很多壓縮算法會去除.9相關的metadata,導致.9圖片失效
功能需求:代碼及依賴庫增加
1. ProGuard:dex的瘦身小助手
ProGuard很多人都比較熟悉了。Proguard是android提供的一個免費的工具,它能夠移除工程中一些沒有引用到的代碼,或者使用更短的包名和名稱來重命名代碼中的類、字段和函數等,達到壓縮、優化和混淆代碼的功能。
ProGuard為什么能減少APK尺寸呢?
a) ProGuard會縮短包名類名法名,減少名稱導致的包空間消耗。
b) ProGuard會檢查每個類、每個方法的可用性、是否被引用、是否可以到達,因此如果引入第三方庫,ProGuard可以幫助我們過濾去大部分不用的方法和類。
然而proguard并不是萬能的。 ProGuard相關的常見誤區:
Q:ProGuard能夠刪掉所有不用的方法嗎?
A :有些代碼是需要-keep的,被-keep的類不會刪除其不用的部分。并且有些庫是必須-keep的,在精簡APK尺寸的時候,需要考慮到這個潛在問題。
Q :很多方法,方法體為空,ProGuard能夠刪掉空方法嗎?
A :ProGuard不能刪除空方法。空方法是占空間、占用方法數的,平時開發過程中需要注意到空方法的隱藏開銷。
2. 代碼及依賴庫增加應對方案
增加代碼、依賴庫會影響apk中哪些部分的尺寸呢?
a) 增加代碼:classes.dex中會增加相應代碼,及代碼引用的類和方法,及代碼引用的jar和其他庫中的類和方法。
b) 新增.so文件:
① lib下會增加相應so文件
② 會增加resources.arsc的大小
③ 會增加META-INF中兩個簽名相關文件的大小
c) 新增java依賴庫:需要檢查是否有無用資源同時引入,增加res的大小。上文中的對照實驗可以看出,引入design包后,包大小有明顯的增長,除了因為degisn包依賴于support-v7包有很多標記@Keep的代碼以外,還由于design自身帶有很多資源文件。
應對TIPS:
適當地控制ProGuard,盡量不keep大的依賴庫
謹慎引入過大的.so庫。適當刪減so庫。
適當刪減依賴庫中的資源和代碼。
定期清除低頻功能、廢棄代碼。
3. 代碼壓縮的一些坑、一些黑科技和一些TRICKY的點
a) lib中的so可以只保留arm相關目錄下的so,去除x86目錄下的so。因為目前市場上主流的架構是arm架構,并且大部分x86架構兼容arm的so。所以x86的so不必特地保留。如果實在需要支持部分廠商,可考慮特定渠道包打入x86的so。 去除x86的so后,我們的APK尺寸減少了200k左右。
b) 有些同學會以為,xml布局文件中定義的view需要-keep,不讓它被混淆掉。而事實是Android已經在你之前想到了這個問題,這些view無需聲明-keep。
c) 一些啟動無需用到的so,可以通過 7zip壓縮,在安裝后解壓使用。百度瀏覽器中,我們使用7zip工具的lzma算法,可以將原本26MB大小的so,壓縮至11MB, 壓縮率高達58% 。而使用zip,只能壓縮至14MB,壓縮率為46%
圖14:zip和7zip的壓縮對比
4. proguard配置中有這么一項,用來保留代碼行號,方便定位問題。
在發版時可以去掉,實測可以減少2.8%左右的包尺寸。
-keepattributes SourceFile,LineNumberTable
定期體檢
隨著項目迭代的深入、項目參與人數的增長,APK的尺寸膨脹自然會變得不可控制,那么對于APK尺寸的科學監控,就顯得尤為重要。 幸好,已經有很多現成的好工具可以利用:
1. APK成分監控
Apk成分監控很多網站都提供了在線檢測的功能,在這里就不贅述,舉一個可以免費試試的栗子:NimbleDroid: https://nimbledroid.com/。這是一個國外的項目,上傳apk包,就能輕松解析包內成分,讓APK中的脂肪無處遁形。
圖15:百度瀏覽器apk尺寸分析
2. 代碼監控 Android Studio->Analyze-> Inspect Code
這個工具可以幫助我們快速分析出冗余類、冗余方法,幫我們定位棄用的功能點,從而從根本上減少dex的大小。
圖16:Inspect Code分析示例
3. 廢棄資源梳理
隨著項目迭代的腳步不斷向前,自然而然會產生許多不用的資源,我們可以通過自動化腳本跑出這部分資源,定期進行刪除。
例如,百度瀏覽器在近6個版本的迭代后,各模塊可以跑出來這么多的歷史遺留垃圾:
圖17:廢棄資源分析示例
這些資源清理之后,可以省出一大筆資源本身占用的空間、減少resources.arsc的大小。
4. 用圖片相似算法找出視覺同學給的重復切圖
項目迭代中,我們往往會重復引入一些過去就已經添加過的資源。完全相同的資源可以通過比較md5來找出。不過除了相同資源以外,項目中大量存在的是重復、有細微差別的切圖,這個就可以根據圖片相似性算法跑出來。
圖18:相似圖片分析(圖片來源于網絡)
還想再瘦一些:瘦到極致
1. 獨立低頻業務模塊插件化,后下載。歷史版本中,我們將圖片搜索、語音識別、日夜間主題等獨立功能轉化為插件后, APK包尺寸減少了4.4MB。
2. 獨立大資源后下載。
3. 嘗試非死book的Redex字節碼優化方案。
結束語
以上就是我們目前在APK瘦身方面做的一些嘗試和積累。其實,對于APK瘦身,其實是一件持續長久的事情,如何在密集的版本迭代中、不斷新增新需求的同時,能夠不粘連、無殘留地刪除舊的廢棄需求,如何搭建項目結構、實現低耦合可插拔式的子模塊功能,這些也是值得我們深思的問題。
希望本文能給致力于減小APK尺寸、致力于打磨產品的程序員工匠們一些啟發和借鑒意義。
來自:http://mp.weixin.qq.com/s/w-JnlBRLiSbRRi_btwFDgA