前言
代碼混淆對於每個入門的 Android 工程師來說都不會太陌生,因爲在編譯正式版本時,這是一個必不可少的過程。而且使用代碼混淆也相當簡單,簡單到只需要配置一句minifyEnabled true
。但是你是否理解混淆的原理,如果問你代碼混淆到底做了什麼,你會怎麼說?
1、混淆編譯器
如果以混淆編譯器來劃分的話,Android 代碼混淆可以分爲以下兩個時期:
- ProGuard:一個通用的 Java 字節碼優化工具,由比利時團隊 GuardSquare 開發
- R8:ProGuard 的繼承者,專爲 Android 設計,在編譯性能上更優秀
下圖梳理了它們隨着 Android Gradle Plugin 版本迭代相應做出的變更:
Android Gradle Plugin 版本迭代
其中,混淆編譯器的變更:
其中:DEX編譯器的變更:
如果需要修正 Android Gradle Plugin 的默認行爲,可以在gradle.properties
中添加配置:
- 啓用與禁用 R8
# 顯式啓用 R8 android.enableR8 = true
# 1. 只對 Android Library module 停用 R8 編譯器 android.enableR8.libraries = false # 2. 對所有 module 停用 R8 編譯器 android.enableR8 = false
- 啓用與禁用 D8
# 顯式啓用 D8 android.enableD8 = true
# 顯式禁用 D8 android.enableD8 = false
另外,如果在應用模塊的 build.gradle
文件中設置useProguard = false
,也會使用 R8 編譯器代替 ProGuard。
2、四大功能
ProGuard 與它的繼承者 R8 都提供了壓縮(shrinker)、優化(optimizer)、混淆(obfuscator)、預校驗(preverifier)四大功能:
- 壓縮(也稱爲搖樹優化,tree shaking):從應用及依賴項中移除未使用的類、方法和字段,有助於規避64方法數的瓶頸
- 優化:通過代碼分析移除更多未使用的代碼,甚至重寫代碼
- 混淆:使用無意義的簡短名稱重命名類、方法和字段,增加逆向難度
- 預校驗:對於面向 Java 6 或者 Java 7 JVM 的 class 文件,編譯時可以把預校驗信息添加到類文件中(StackMap 和 StackMapTable屬性),從而加快類加載效率。預校驗對於 Java 7 JVM 來說是必須的,但是對於 Android 平臺無效
使用 ProGuard 時,部分編譯流程如下圖所示:
- ProGuard 對 .class 文件執行代碼壓縮、優化與混淆
- D8 編譯器執行脫糖,並將 .class 文件轉換爲 .dex文件
使用ProGuard的部分編譯流程
使用 R8 時,部分編譯流程如下圖所示:
- R8 將脫糖(Desugar)、壓縮、優化、混淆和 dex(D8 編譯器)整合到一個步驟
- R8 對 .class 文件執行代碼壓縮、優化與混淆
- D8 編譯器執行脫糖,並將 .class 文件轉換爲 .dex文件
使用R8的編譯流程
對比以下 ProGuard 與 R8 :
- 共同點:
1、開源
2、R8 支持所有現有 ProGuard 規則文件
3、都提供了四大功能:壓縮、優化、混淆、預校驗
- 不同點:
1、ProGuard 可用於 Java 項目,而 R8 專爲 Android 項目設計
2、R8 將脫糖(Desugar)、壓縮、優化、混淆和 dex(D8 編譯器)整合到一個步驟中,顯着提高了編譯性能
關於 D8 編譯器
將 .class 字節碼轉化爲 .dex 字節碼的過程被稱爲 DEX 編譯,最初是由DX 編譯器完成。與 DX 編譯器相比,新的 D8 編譯器的編譯速度更快,輸出的 .dex 文件更小,卻能保持相同乃至更出色的應用運行時性能
3、 使用示例
無論使用R8還是ProGuard,默認不會啓用壓縮、優化和混淆功能。這個設計主要是出於兩方面考慮:一方面是因爲這些編譯時任務會增加編譯時間,另一方面是因爲如果沒有充分定義混淆保留規則,還可能會引入運行時錯誤。因此,最好只在應用的測試版本和發佈版本中啓用這些編譯時任務,參考使用示例:
// build.gradle
...
android {
buildTypes {
// 測試版本
preview {
// 啓用代碼壓縮、優化和混淆(由R8或者ProGuard執行)
minifyEnabled true
// 啓用資源壓縮(由Android Gradle plugin執行)
shrinkResources true
// 指定混淆保留規則文件
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
// 發佈版本
release {
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
// 開發版本
debug{
minifyEnabled false
}
}
...
}
minifyEnabled
:(默認情況下)啓用代碼壓縮、優化、混淆與預校驗shrinkResources
:啓用資源壓縮proguardFiles
、proguardFile
:指定 ProGuard 規則文件,前者可以指定多個參數。下面兩段配置的作用是一樣的。// 方式一: proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' // 方式二: proguardFile getDefaultProguardFile('proguard-android-optimize.txt') proguardFile 'proguard-rules.pro'
前面提到了:無論使用R8還是ProGuard,壓縮、優化和混淆功能都是默認關閉的。通過以下配置可以靈活控制:
- 整體關閉
minifyEnabled false
// 這行就沒有效果了
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
- 關閉壓縮
-dontshrink
- 關閉優化(R8 無效)
-dontoptimize
注意:R8 不能關閉優化,也不允許修改優化的行爲,事實上,R8 會忽略修改默認優化行爲的規則。例如設置 -optimizations
和 -optimizationpasses
後會得到編譯時警告:
AGPBI: {"kind":"warning","text":"Ignoring option: -optimizations","sources":[{"file":"省略..."}],"tool":"D8"}
AGPBI: {"kind":"warning","text":"Ignoring option: -optimizationpasses","sources":"省略..."}],"tool":"D8"}
- 關閉混淆(建議在開發版本關閉混淆)
-dontobfuscate
- 關閉預校驗(對 Android 平臺無效,建議關閉)
-dontpreverify
4、ProGuard 規則文件
R8 延續了 ProGuard 使用規則文件修改默認行爲的做法。在很多時候,規則文件也被稱爲混淆保留規則文件,這是因爲該文件內定義的絕大多數規則都是和代碼混淆相關的。事實上,文件內還可以定義代碼壓縮、優化和預校驗規則,因此稱爲 ProGuard 規則文件比較嚴謹。
在上一節裏,我們提到了使用proguardFiles
和proguardFile
指定 ProGuard 規則文件。對於任何一個項目,它的 ProGuard 規則文件有以下三種來源:
- 1、Android Gradle 插件
在編譯時,Android Gradle 插件會生成 proguard-android-optimize.txt
、 proguard-android.txt
,位置在<module-dir>/build/intermediates/proguard-files/
。這兩個文件中除了註釋之外,唯一的區別是前者啓用瞭如下代碼壓縮,而後者關閉了代碼壓縮,如下所示:
# proguard-android-optimize.txt
-optimizations !code/simplification/arithmetic,!code/simplification/cast,!field/*,!class/merging/*
-optimizationpasses 5
-allowaccessmodification
相同部分省略...
# proguard-android.txt
-dontoptimize
相同部分省略...
其中相同的那部分混淆規則中,下面這一部分是比較特殊的:
-keep class android.support.annotation.Keep
-keep class androidx.annotation.Keep
// 保留@Keep註解的類,保留...TODO
-keep @android.support.annotation.Keep class * {*;}
-keep @androidx.annotation.Keep class * {*;}
// 保留@Keep修飾的方法
-keepclasseswithmembers class * {
@android.support.annotation.Keep <methods>;
}
-keepclasseswithmembers class * {
@androidx.annotation.Keep <methods>;
}
// 保留@Keep修飾的字段
-keepclasseswithmembers class * {
@android.support.annotation.Keep <fields>;
}
-keepclasseswithmembers class * {
@androidx.annotation.Keep <fields>;
}
// 保留@Keep修飾的構造方法
-keepclasseswithmembers class * {
@android.support.annotation.Keep <init>(...);
}
-keepclasseswithmembers class * {
@androidx.annotation.Keep <init>(...);
}
它指定了與@Keep
註解相關的所有保留規則,這裏就解釋了爲什麼使用@Keep
修飾的成員不會被混淆了吧?
- 2、Android Asset Package Tool 2 (AAPT2)
在編譯時,AAPT2 會根據對 Manifest 中的類、佈局及其他應用資源的引用來生成aapt_rules.txt
,位置在<module-dir>/build/intermediates/proguard-rules/debug/aapt_rules.txt
。
例如,AAPT2 會爲 Manifest 中註冊的每個組件添加保留規則:
Referenced at [項目路徑]/app/build/intermediates/merged_manifests/release/AndroidManifest.xml:19
-keep class com.have.a.good.time.MainActivity { <init>(); }
省略...
在這裏,AAPT2 生成了MainActivity
的保留規則,同時它還指出了引用出處:AndroidManifest.xml:19
。這是因爲 啓動 Activity 的過程中,需要使用反射的方式實例化具體的每一個 Activity ,有興趣可以看下 ActivityThread#performLaunchActivity()
-> Instrumentation#newActivity()
- 3、Module
創建新 Module 時,IDE 會在該模塊的根目錄中創建一個 proguard-rules.pro
文件。當然,除了這個自動生成的文件,還可以按需創建額外的規則文件。例如,下面的配置對 release 添加了額外的規則文件:
...
android {
...
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
productFlavors {
dev{
...
}
release{
proguardFile 'release-rules.pro'
}
}
}
...
小結一下:
規則文件來源 | 描述 |
---|---|
Android Gradle 插件 | 在編譯時,由 Android Gradle 插件生成 |
AAPT2 | 在編譯時,AAPT2 根據對應用清單中的類、佈局及其他應用資源的引用來生成保留規則 |
Module | 創建新 Module 時,由 IDE 創建,或者另外按需創建 |
如果將 minifyEnabled
屬性設爲 true
,ProGuard 或 R8 會將來自上面列出的所有可用來源的規則組合在一起。爲了看到完整的規則文件,可以在proguard-rules.pro
中添加以下配置,輸出編譯項目時應用的所有規則的完整報告:
-printconfiguration build/intermediates/proguard-files/full-config.txt
5、組件化混淆
在組件化的項目中,需要注意應用 Module 和 Library Module 的行爲差異和組件化的資源匯合規則,總結爲以下幾個重點:
- 編譯時會依次對各層 Library Module進行編譯,最底層的 Base Module 會最先被編譯爲 aar 文件,然後上一層編譯時會將依賴 Module 輸出的 aar 文件/ jar 文件解壓到模塊的 build 中相應的文件夾中
- App Module 這一層彙總了全部的 aar 文件後,才真正開始編譯操作
- 後編譯的 Module 會覆蓋之前編譯的 Module 中的同名資源
組件化資源彙總
Lib Module 彙總到 App Module
使用較高版本的 Android Gradle Plugin,不會將彙總的資源放置在 exploded-aar
文件夾。即便如此,Lib Module 的資源彙總到 App Module 的規則是一樣的。
我們通過一個簡單示例測試不同配置下的混淆結果:
配置一 | 配置二 | 配置三 | 配置四 | |
---|---|---|---|---|
App Module 開啓混淆 | X | X | √ | √ |
Base Module 開啓混淆 | X | √ | X | √ |
示例程序:App Module 依賴了 Base Module
將構建的 apk 包拖到 Android Studio 面板上即可分析 Base 類混淆結果,例如配置一的結果:
使用配置一時,Base 類沒有被混淆
全部測試結果如下:
配置一 | 配置二 | 配置三 | 配置四 | |
---|---|---|---|---|
App Module 開啓混淆 | X | X | √ | √ |
Base Module 開啓混淆 | X | √ | X | √ |
(結果)Base 類是否被混淆 | X | X | √ | √ |
可以看到,混淆開啓由 App Module 決定, 與Lib Module 無關。
現在我們分別在 Lib Module 和 App Module 的 proguard-rules.pro
中添加 Base 類的混淆保留規則,並在 build.gradle
中添加配置文件,測試 Base 類是否能保留:
-keep class com.rui.base.Base
測試結果如下:
配置位置 | Lib Module | App Module |
---|---|---|
(結果)Base 類是否保留 | X | √ |
可以看到:(默認情況)混淆規則以 App Module 中的混淆規則文件爲準。
這裏就引入兩種主流的組件化混淆方案:
- 在 App Module 中設置混淆規則
這種方案將混淆規則都放置到 App Module 的proguard-rules.pro
中,最簡單也最直觀,缺點是移除 Lib Module 時,需要從 App Module 中移除相應的混淆規則。儘管多餘的混淆規則並不會造成編譯錯誤或者運行錯誤,但還是會影響編譯效率。
很多的第三方 SDK,就是採用了這種組件化混淆方案。在 App Module 中添加依賴的同時,也需要在proguard-rules.pro
中添加專屬的混淆規則,這樣才能保證release
版本正常運行。
- 在 App Module 中設置公共混淆規則,在 Lib Module 中設置專屬混淆規則
這種方案將專屬的混淆規則設置到 Lib Module 的proguard-rules.pro
,但是根據前面的測試,在 Lib Module 中設置的混淆規則是不生效的。爲了讓規則生效,還需要在 Lib Module 的build.gradle
中添加以下配置:
...
android{
defaultConfig{
consumerProguardFiles 'consumer-rules.pro'
}
}
其中consumer-rules.pro
文件:
-keep class com.rui.base.Base
測試結果表明,Base 類已經被保留了。這種使用consumerProguardFiles
的方式有以下幾個特點:
consumerProguardFiles
只對 Lib Module 生效,對 App Module 無效consumerProguardFiles
會將混淆規則輸出爲proguard.txt
文件,並打包進 aar 文件- App Module 會使用 aar 文件中的
proguard.txt
彙總爲最終的混淆規則,這一點可以通過前面提到的-printconfiguration
證明
最後
如果你看到了這裏,覺得文章寫得不錯就給個讚唄!歡迎大家評論討論!如果你覺得那裏值得改進的,請給我留言。一定會認真查詢,修正不足,定期免費分享技術乾貨。感興趣的小夥伴可以點一下關注哦。謝謝!