本文已授權微信公眾號「玉剛說」獨家釋出。
大家好,你現在看到的是「Java 混淆那些事」系列文章的第一篇,通過這個系列我想帶大家重新認識一下 ProGuard 到底能幹什麼?最終領悟怎麼才能寫好混淆規則。所以說這個系列文章的重點將會放到書寫 keep 規則上面。我會最大程度用大白話寫明白。
首先我們瞭解一下 ProGuard 到底是什麼能幹什麼?
ProGuard 是可以對 Java 類檔案進行壓縮、優化、混淆和預驗證的工具。
簡單解釋一下 ProGuard 的功能
- 壓縮 (Shrinker):刪除無效的類、欄位、方法等。
- 優化 (Optimizer):優化位元組碼,合併方法,刪除無用欄位等。
- 混淆 (Obfuscator):將類名、屬性名、方法名以及欄位名混淆為難以讀懂的字母,比如a, b, c等。
- 預校驗 (Preverifier):對 class 檔案進行預檢驗,確保虛擬機器載入的 class 檔案是安全並且可以執行的。
我們再來看下一個問題 ProGuard 是以什麼樣的流程進行工作的。
-
壓縮階段 ProGuard 會從「程式碼入口點」開始遞迴查詢,把用到的類或變數等留下來,沒用到的全都刪掉。
-
優化階段 ProGuard 會優化經過壓縮階段留下來的類,比如將外部沒有呼叫的非程式碼入口點的方法或類改為私有的,又或者把一部分方法改為 final 的,相應的欄位改為 static、final,或者把幾個方法合併成一個,刪除沒有用到的引數等等的優化操作。
-
混淆階段 將程式碼入口點呼叫到的類和方法(非程式碼入口點方法),給他改個名字,比如簡短的或者複雜的,這個過程中重新命名的字典可以自定義。改完名字後還能保證程式的正常執行邏輯。
-
預校驗階段 在編譯版本為 Java ME 或 1.6 以及更高版本時是預設開啟的。但編譯成 Android版本時,預校驗是不必須的。
那麼程式碼入口點到底是什麼呢?
好的現在就忘記以上這些廢話,我們重點來看第一個知識點「程式碼入口點」。我們剛才應該也看到了 ProGuard 壓縮階段是從程式碼入口點開始遞迴查詢用到的程式碼的。
舉個例子:比如你寫了一個很方便的下載類,假設需要使用的就這一個方法 new DowonloadClien("url").start()
那麼這個方法就應該指定為程式碼入口點。
ProGuard 怎麼知道哪裡是程式碼入口點的呢? 沒錯這個程式碼入口點如果我們不告訴 ProGuard,他是不會知道的。那麼怎麼告訴他呢?我們通過 keep 規則就可以告訴 ProGuard 了,具體用法我們以後文章中具體說,這裡就瞭解一下。
舉個例子
下面我們寫一個通俗的小例子,配合程式碼理解一下。看看壓縮、優化、混淆這些功能。
//測試程式碼,如下程式碼純屬為了測試,除此之外沒有任何合理性。
src
-> model
-> ModelA.java
int testA = 2;
public void modelA(int age) {
int a = 1 + age;
int b = testA + age;
System.out.println("print " + b);
}
public void modelB(String name) {
System.out.println("print " + name);
}
-> ModelB.java
public void modelA(String name) {
System.out.println("print " + name);
}
public void modelB(String name) {
System.out.println("print " + name);
}
-> utils
-> UtilsA.java
private static final String UtilA = "utila";
public static void printA() {
System.out.println("print " + UtilA);
}
public static void printB() {
System.out.println("print B");
}
-> UtilsB.java
public static void printA(){
System.out.println("print A");
}
public static void printB(){
System.out.println("print B");
}
Main.java
public static Main sMain = null;
public static void main(String[] args) {
sMain = new Main();
sMain.run();
}
private void run() {
ModelA modelA = new ModelA();
modelA.modelA(5);
UtilsA.printA();
}
//我們先不新增任何混淆引數,混淆之後的結果
src
-> a
-> a.java
private int a = 2;
public final void a(int i) {
System.out.println("print " + (this.a + 5));
}
-> defpackage
-> Main.java
private static Main a = null;
public static void main(String[] strArr) {
a = new Main();
new a().a(5);
System.out.println("print utila");
}
複製程式碼
對比一下混淆前和混淆後的 Jar 包內容
看到幾個很顯然的效果
- 沒有被程式碼入口點呼叫到的類、方法都刪除了。
- 定義的多個變數也都合併到一起了,甚至完全消失不見了。
- 很多方法也進行了合併。
- 除了程式碼入口點之外,留下來方法名和變數名全都改變了。
- 優化了程式碼,可以看到上面 public static Main sMain = null; 混淆完自動給改成了 private。
- 他還會自動把一部分方法優化為 final 的。
為什麼 Main 這個類以及 main 方法沒有被混淆呢?
在 ProGuard 預設生成的配置檔案下有個條匹配規則
-keepclasseswithmembers public class * {
public static void main(java.lang.String[]);
}
複製程式碼
解釋一下:匹配每個類裡面的 main 方法為程式碼入口點,如果沒有任何一個類有 main 方法。那麼我們的上面的例子就是空的檔案了,因為在壓縮階段就已經把所有程式碼全都刪了。
main 方法是 Java 應用程式的入口方法,程式執行執行的第一個方法。
小結
經過這個小例子,除了預校驗之外,其他特性我們都已經明顯的看到了。概念也大概的懂了。恭喜你打怪升級成功,快去看看下一篇吧。