
前言
隨著iphone13p最大記憶體放大到了1T,大記憶體手機的時代悄然降臨,在android里面,三星也有,羅老師幾年前說:如果我告訴你們我們在做1T的手機,你們可能以為我瘋了,
看看現在,估計未來會有更多手機有1T版,大家開始真香了,
但是,如果現在有人說:要做一個1T大小的app,那他可能是真瘋了,至少未來十年不可能,因為手機記憶體是越大越好,你一個app當然是能小就小呀
Android app的檔案格式為apk,本文就是探討對于一個android apk,有哪些方法可以減小體積
Apk組成
要想減小體積,首先我們需要了解apk的構成

-
我們寫的.java檔案會被編譯為.class檔案,再由dx工具編譯為Classes.dex檔案,由于android限制,每個dex檔案最多65535個方法,所以多出來的方法就生成Classes2.dex , Classes3.dex~ClassesN.dex
-
Resource(res)與Assets比較像,區別是res目錄下會生成資源ID,并在.R檔案中記錄,可以直接使用,這里平常我們用得很多,而assets不會有ID,而是通過AssetManager介面獲取;
所以res類似于我們的桌面,一般放我們要操縱的控制元件資源,而assets類似于桌下的抽屜,放諸如資料庫,html這類資源
-
Native Libraries平時打交道少,優化空間也很有限
上面是抽象的apk結構,下面我們看一個實際的
將qq.apk拖入android studio

可以看到最大的R檔案夾,點進去,都是一些圖片,第二大的是assets,里面是一些表情包以及插件圖片
其他的我們剛剛也說過,值得注意的是,里面多了一個META-INF
他存放了應用的簽名資訊,其中
-
.MF: 每一個資源都有一個SHA1簽名,存放在這里
-
.SF: 檔案存放.MF經過base64編碼后的簽名
-
.RSA: 對.SF檔案使用SHA1演算法生成數字摘要(注意:.MF中是對每一個資源進行SHA1,這里是對檔案),然后進行RSA加密,再用開發者私鑰進行簽名,安裝時使用公鑰解密
這樣子,一個app安裝在手機時,解密這一數字摘要,然后與內部的.MF檔案比對,如果相符,證明資源內容沒有被修改
Dex檔案
在APK組成中我們可以看到,占用記憶體最大的是res,assets與classs.dex檔案,這也是我們的優化方向,接下來,我們看看如何優化dex
首先我們看看dex的結構

更詳細的版本在官網,這里如果對這些結構的作用有興趣,可以看下圖的詳細版本

ProGuadrd
dex是代碼編譯而來,而對于代碼檔案,最重要的優化就是混淆了,將方法名,屬性名等變為又短又無意義的名字,不僅能縮小體積還能避免反編譯被人破解
在IDE中,我們可以看到qq里面的類都是小寫字母,里面的變數和方法都按字母順序排列了,從a開始

除了修改變數名,ProGuadrd還可以在功能等價的基礎上重寫代碼,比如把多個函式呼叫寫到一個函式里面去,更加增大了閱讀理解難度(雖然初學者一般已經這樣做了),以及打亂格式,增加空格等
主要步驟如下
-
壓縮(Shrink): 檢測和洗掉沒有使用的類,欄位,方法和特性,
-
優化(Optimize) : 分析和優化Java位元組碼,
-
混淆(Obfuscate): 使用簡短的無意義的名稱,對類,欄位和方法進行重命名,
-
預檢(Preveirfy): 用來對Java class進行預驗證(預驗證主要是針對JME開發來說的,Android中沒有預驗證程序,默認是關閉),
D8 與R8優化
這兩平時接觸不多,他們主要是在位元組碼處做優化的,開發時感知不強(感覺就是用來面試的)
D8主要是在編譯位元組碼時重排序,將占用空間變得更小,比如對于greetingType方法,正常編譯后的結果是
[000584] Main.greetingType:(LGreeting;)Ljava/lang/String;
0000: sget-object v0, LMain$1;.$SwitchMap$Greeting:[I
0002: invoke-virtual {v2}, LGreeting;.ordinal:()I
0005: move-result v1
0006: aget v0, v0, v1
0008: packed-switch v0, 00000017 // 這里
如果使用D8優化,編譯后的結果
[0005f0] Main.greetingType:(LGreeting;)Ljava/lang/String;
0000: sget-object v0, LMain$1;.$SwitchMap$Greeting:[I
0002: invoke-virtual {v1}, LGreeting;.ordinal:()I
0005: move-result v1
0006: aget v0, v0, v1
-0008: packed-switch v0, 00000017 // 這里
+0008: const/4 v1, #int 1
+0009: if-eq v0, v1, 0014
+000b: const/4 v1, #int 2
+000c: if-eq v0, v1, 0017
可以看到 0008處后的幾條指令有變化,多了幾個if,對于不同的case做創建不同的變數,可以節省空間
R8也類似,只是策略有些不一樣
更詳細的了解可以參考 D8 Optimizations
總之,他們的作用是就是,在不改變功能的情況下,重寫部分class指令,減小空間占用,但是有可能會增加指令數量
Redex優化
Redex是Facebook推出的一個優化Dex檔案的工具,和D8R8一樣,也是對位元組碼的處理,有以下效果
- 行內函式,減少呼叫
- 洗掉無用代碼
- 將只有一個實作類的介面或者父類用實作類代替
- 字串混淆所見
……
不過這個我沒用過,但是感覺Proguard與D8R8都多多少少能做到,可能是他在細節上用了更好的演算法
但是不管多少框架,對dex檔案的優化說來說去也就這些
移除多余的庫與代碼
最后是移除第三方庫和冗余代碼,屬于業務邏輯上的原因
-
多余的庫
對于自己的小專案,還好,對于多人參與的大型專案,很有可能對同一個功能,不同的人用了不同的輪子,手Q里面就有,比如要寫單測,之前使用Powermock,后來用JMock,再后來改為Mockk,一個專案,三個單測框架
由于不同的單測框架已經寫了不少單測,短時間移除是不太可能的,但是可以慢慢轉為同一種單測框架
-
多余代碼
Android studio會自己檢測,沒有用過的會置位灰色提醒,但是會漏掉很多,通過插件Lint可以檢測,
資源清理
上面都是在代碼層面減小dex,apk的另一個空間占用大戶,是資源,尤其是其中的圖片,
圖片,你可知道,多少OOM因你而起?多少app因你閃退?
圖片壓縮與更換格式
我們先看看圖片為什么那么大
圖片的顯示,有ARGB 4個通道,其中默認的顯示模式是ARGB8888,ARGB8888表示每個通道的顏色區間為[0,255],也就是兩個16進制數表示,也就是8bit -> 1位元組
所以ARGB8888模式下,一個像素4個通道下占用4位元組,一張1024*1024的手機圖片圖片,就是
2
10
?
2
10
?
2
2
=
2
22
=
4
M
2^{10} * 2^{10} * 2^2 = 2^{22} = 4M
210?210?22=222=4M
一張圖4M,太離譜了!
上面是打開后在運存的占用,我們可以修改顏色通道,不然ARGB565來減小單個像素所占用運存,不過有點跑題,本篇我們講的是app的大小,也就是所占用手機的記憶體(我們約定 手機運存 = 電腦記憶體,手機記憶體 = 電腦硬碟)
記憶體與運存中的圖片存在形式是不一樣的,壓縮方法也不一樣,很多人容易弄混
回到記憶體,記憶體中,圖片是以png,jpg等格式存盤
我之前開發的時候都是先將png圖片,往tinypng網站中壓縮一下再放入,所以可以壓縮圖片,一般能壓個三分之一~三分之二,
也可以更換圖片格式,比如webp,svg可以更小,android studio也提供了對應的支持,但是沒有最好的格式,只是適用場景不同
👇

這里多提一下webp,因為這是google推出的,大家在谷歌瀏覽器下載圖片的時候,一般默認下載下來就是webp格式,所謂更小的記憶體占用,本質上是對圖片進行了壓縮,webp的壓縮演算法是VP8視頻編碼,核心邏輯就是將圖片分割成更小的子塊,然后預測周圍像素值,預測越準,周圍的像素值就可以刪去,再在圖片打開時算出刪掉的像素
圖片網路化
在微信或者qq聊天中,對方發來一張圖片,我們在聊天視窗往往先看到一張很模糊的縮略圖,當點擊時才會加載出高清圖,
這個思路也可以用在apk中,很多入口較深的高清大圖,或者需要經常更新的圖片,也許用戶根本不看,就沒有必要內置在apk中,看時加載即可,如果需要提前占位置,可以用縮略圖代替
至于哪些圖網路化,需要根據業務與用戶體驗來權衡了
比如淘寶,在斷網情況下打開時,只有icon內置了

其他策略
無論是對Dex還是對資源進行優化,雖然安全有效,但是本質上是將原來有的東西變得更小,對apk的瘦身程度是有限的,還有一些”七傷拳“,優化率極高,但是對apk的影響也很大,需要謹慎使用,
插件化
所謂插件化,就是將apk中的非主要功能弄成獨立的apk,原主apk稱為宿主,
比如支付寶里面,就是搞支付的,那么他里面的什么口碑,基金,天貓一堆亂七八糟,同時功能獨立的東西就非常適合做成插件,用戶用到的時候再從網路加載進來,這樣極大的減少了apk占用,
但是這里涉及到比較多的技術問題:
- 用戶現在只有宿主apk,如何讓宿主加載到插件apk里面的代碼?
- android四大組件都需要到manifest中注冊,插件里面的組件顯然不可能提前注冊到宿主的manifest中(不然注冊了,插件沒加載進來,會找不到類),所以如何讓系統認為下載下來的插件有注冊?
- 宿主與插件資源能否正確互相參考?
一般來說,通過的是代理和反射來處理,騰訊有一個shadow框架可以大致實作”零反射“,
- 復用獨立安裝App的原始碼:
- 零反射無Hack實作插件技術:
- 全動態插件框架:
- 宿主增量極小:
- Kotlin實作:
不過插件化技術不在今天的討論范圍,有興趣可以研究下tencent-shadow
當使用了插件化后,專案基本是要重構了,相比起改改Dex和圖片,這個工程量極大,但是收益也會很高
webview
這里類似于圖片網路化,相對于圖片,直接將整個界面都變成url,
我們手機app中的小程式一般都是url顯示在webview中
相關技術可以使用jsBridge與Hybird,本質上就是通過bridge連接h5與android iOS,實作通信

不過代價就是,加載速度慢于原生,還要注意防止網址篡改等
小結
本文我們討論的是apk的瘦身方案,首先先明確了apk的主要組成部分為dex檔案與資源檔案
-
對于dex檔案,我們可以進行混淆,位元組碼重排序,移除多余庫與代碼
-
對于資源檔案,我們可以替換格式,壓縮圖片,網路化
除了這些常規操作,我們還可以使用插件化與Webview方法極致減少體積,但是這兩個技術工程量大,而且有性能代價,需要謹慎使用,
我是小松,專注于計算機以及android開發,如有興趣,可以關注一波【小松漫步】公眾號,搜集了不少電子書免費分享,感謝支持!
參考資料
深入探索 Android 包體積優化(匠心制作-上)
Android 專案中資源檔案 – asset 目錄和 res 目錄
頂象App加固技術決議:DEX檔案格式的詳解
D8 Optimizations
Android 開發應該掌握的 Proguard 技巧
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/333832.html
標籤:其他
