首頁 > 書法

Android使用R8壓縮APK體積

作者:由 這是懷疑論 發表于 書法日期:2022-07-04

android的壓縮包能刪嗎

此讀書筆記完全基於公開的Google官方文件

壓縮程式碼

如果設定minifyEnabled true, 那麼會啟用R8程式碼壓縮,搖樹最佳化。

android {

。。。

buildTypes {

release {

minifyEnabled true

}

}

}

我覺得類似於JVM的垃圾回收,使用引用樹檢查來確定哪些物件需要清理。

Android使用R8壓縮APK體積

R8構建一張圖來確定執行不到的程式碼

未被引用的就會被移除,以節省資源,提高程式效率。

看到這裡,我立刻就想到,透過引用檢查來移除無用類/方法/成員變數。那麼只通過反射呼叫的類、方法、變數豈不是會出問題?

帶著問題我接著往下看:

自定義要保留的程式碼

在某些情況下,R8 很難做出正確分析,因此可能會移除您的應用實際上需要的程式碼。下面列舉了幾個例子,說明了它在什麼情況下可能會錯誤地移除程式碼:

當您的應用透過 Java 原生介面 (JNI) 呼叫方法時

當您的應用在執行時查詢程式碼時(如使用反射)

果然,設計者肯定知道這個問題,所以留下了解決辦法:

要修復錯誤並強制 R8 保留某些程式碼,請在 ProGuard 規則檔案中新增

-keep

程式碼行。例如:

-keep public class MyClass

壓縮資源

android {

。。。

buildTypes {

release {

shrinkResources true

minifyEnabled true

proguardFiles getDefaultProguardFile(‘proguard-android。txt’),

‘proguard-rules。pro’

}

}

}

如上所示,使用shrinkResources true 來啟用資源壓縮。

而與壓縮程式碼同理,我們會有可能使用Resources。getIdentifier()來訪問資源,那麼這些執行時動態使用的資源該怎麼辦?

R8會預設採取比較安全的防禦策略,將所有具有匹配名稱格式的資源標記為可能已使用,不移除。

例如:

String name = String。format(“img_%1d”, angle + 1);

res = getResources()。getIdentifier(name, “drawable”, getPackageName());

以上程式碼會使系統將所有帶img_字首的資源標記為已使用並保留下來。

PS:資源壓縮器還會瀏覽程式碼以及各種

res/raw/

資源中的所有字串常量,查詢格式類似於

file:///android_res/drawable//ic_plus_anim_016。png

的資源網址。如果它找到這樣的字串,或發現一些其他字串看似可用來構建這樣的網址,就不會將它們移除。

那對於APK體積極其敏感的人表示,想讓資源壓縮和程式碼壓縮一樣,預設刪除無靜態引用的資源,手動保留會動態引用的資源該怎麼辦呢?

答案是啟用嚴格引用檢查,具體做法是在keep。xml檔案中將shrinkMode設為strict,如下所示:

<?xml version=“1。0” encoding=“utf-8”?>

tools:shrinkMode=“strict” />

同時,需要使用tools:keep屬性來手動保留想保留的資源:

<?xml version=“1。0” encoding=“utf-8”?>

tools:keep=“@layout/l_used*_c,@layout/l_used_a,@layout/l_used_b*”

tools:discard=“@layout/unused2” />

其中

tools:keep

屬性中指定要保留的每個資源,在

tools:discard

屬性中指定要捨棄的每個資源。

解碼混淆過的堆疊軌跡

在使用騰訊bugly的時候,我沒弄懂它怎麼解碼被混淆過的程式碼、識別出Exception的正確行數和正確方法名,又為啥要這麼做。

首先我們要知道,程式碼混淆就是類似於短網址邏輯,使用較短的類名、方法名、變數名來全域性替換所有的。這樣的一個好處是可以顯著縮小程式碼體積,另一個好處是即使class檔案被反編譯,混淆過的無意義的程式碼也難以看懂,可以保護程式碼。

我們注意到,在此過程中,方法名類名都會變,所以反射呼叫肯定會完蛋。另一個是行號也會變化了,縮短程式碼後,方法、表示式所在行可能會有變動(參考程式碼最佳化),所以Exception的日誌會完蛋,不僅類名方法名一臉懵逼,連行號也找不到了。

所以生產環境的bug,產生的日誌要怎麼看?文章中有了解答:

R8 每次執行時都會建立一個

mapping。txt

檔案,其中列出了混淆過的類、方法和欄位名稱與原始名稱的對映關係。此對映檔案還包含用於將行號映射回原始原始檔行號的資訊。R8 將此檔案儲存在

/build/outputs/mapping/

目錄中。

原來如此,這個mapping。txt就相當於密碼本了,bugly讓每個版本都上傳一份對映檔案,原來如此。

顯然隨著每個版本程式碼迭代,mapping檔案肯定會變化,所以一個apk對應一個mapping檔案是自然的。

文章也提示了我們:

注意

:您每次編譯專案時都會覆蓋 R8 生成的

mapping。txt

檔案,因此您每次釋出新版本時都必須小心地儲存一個副本。透過為每個釋出版本保留一個

mapping。txt

檔案副本,如果使用者提交了來自舊版應用的混淆過的堆疊軌跡,您將能夠除錯相關問題。

仍然存在的疑問

梯子掛了,所以訪問很多原文不便。

我繼續讀文章的過程中注意到,上面說的關於 “混淆過的程式碼透過反射呼叫肯定會完蛋”是武斷的。文章下面提到:

預設情況下,R8 假設您打算在執行時檢查和操縱該類的物件(即使您的程式碼實際上並不這樣做),因此它會自動保留該類及其靜態初始化程式。

通俗地說,就是如果開發者透過反射呼叫了class A,即使A沒有被其他任何地方使用到,那麼R8會保留A和A的靜態初始化方法,保證你還能正常反射呼叫。

而我們可以透過在專案的

gradle。properties

檔案中新增

android。enableR8。fullMode=true

來啟用更積極的最佳化,也就是預設不保留反射呼叫的類A的。

我的疑惑就是,R8如何分析得知我反射呼叫了class A呢,大機率還是類似於上面的防禦性地保護資原始檔,對編譯時確定或者半確定的class進行積極的保留。對於執行時地動態獲取需要反射呼叫的類名時,束手無策。