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

 

 本文由用戶 RalParamor 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!