本文已授權(quán)微信公眾號「玉剛說」獨(dú)家發(fā)布。
大家好,你現(xiàn)在看到的是「Java 混淆那些事」系列文章的第一篇,通過這個系列我想帶大家重新認(rèn)識一下 ProGuard 到底能干什么?最終領(lǐng)悟怎么才能寫好混淆規(guī)則。所以說這個系列文章的重點(diǎn)將會放到書寫 keep 規(guī)則上面。我會最大程度用大白話寫明白。
首先我們了解一下 ProGuard 到底是什么能干什么?
ProGuard 是可以對 Java 類文件進(jìn)行壓縮、優(yōu)化、混淆和預(yù)驗(yàn)證的工具。
簡單解釋一下 ProGuard 的功能
- 壓縮 (Shrinker):刪除無效的類、字段、方法等。
- 優(yōu)化 (Optimizer):優(yōu)化字節(jié)碼,合并方法,刪除無用字段等。
- 混淆 (Obfuscator):將類名、屬性名、方法名以及字段名混淆為難以讀懂的字母,比如a, b, c等。
- 預(yù)校驗(yàn) (Preverifier):對 class 文件進(jìn)行預(yù)檢驗(yàn),確保虛擬機(jī)加載的 class 文件是安全并且可以執(zhí)行的。
我們再來看下一個問題 ProGuard 是以什么樣的流程進(jìn)行工作的。
壓縮階段
ProGuard 會從「代碼入口點(diǎn)」開始遞歸查找,把用到的類或變量等留下來,沒用到的全都刪掉。優(yōu)化階段
ProGuard 會優(yōu)化經(jīng)過壓縮階段留下來的類,比如將外部沒有調(diào)用的非代碼入口點(diǎn)的方法或類改為私有的,又或者把一部分方法改為 final 的,相應(yīng)的字段改為 static、final,或者把幾個方法合并成一個,刪除沒有用到的參數(shù)等等的優(yōu)化操作。混淆階段
將代碼入口點(diǎn)調(diào)用到的類和方法(非代碼入口點(diǎn)方法),給他改個名字,比如簡短的或者復(fù)雜的,這個過程中重命名的字典可以自定義。改完名字后還能保證程序的正常運(yùn)行邏輯。預(yù)校驗(yàn)階段
在編譯版本為 Java ME 或 1.6 以及更高版本時是默認(rèn)開啟的。但編譯成 Android版本時,預(yù)校驗(yàn)是不必須的。
那么代碼入口點(diǎn)到底是什么呢?
好的現(xiàn)在就忘記以上這些廢話,我們重點(diǎn)來看第一個知識點(diǎn)「代碼入口點(diǎn)」。我們剛才應(yīng)該也看到了 ProGuard 壓縮階段是從代碼入口點(diǎn)開始遞歸查找用到的代碼的。
舉個例子:比如你寫了一個很方便的下載類,假設(shè)需要使用的就這一個方法 new DowonloadClien("url").start() 那么這個方法就應(yīng)該指定為代碼入口點(diǎn)。
ProGuard 怎么知道哪里是代碼入口點(diǎn)的呢?
沒錯這個代碼入口點(diǎn)如果我們不告訴 ProGuard,他是不會知道的。那么怎么告訴他呢?我們通過 keep 規(guī)則就可以告訴 ProGuard 了,具體用法我們以后文章中具體說,這里就了解一下。
舉個例子
下面我們寫一個通俗的小例子,配合代碼理解一下??纯磯嚎s、優(yōu)化、混淆這些功能。
//測試代碼,如下代碼純屬為了測試,除此之外沒有任何合理性。
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();
}
//我們先不添加任何混淆參數(shù),混淆之后的結(jié)果
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 包內(nèi)容
看到幾個很顯然的效果
- 沒有被代碼入口點(diǎn)調(diào)用到的類、方法都刪除了。
- 定義的多個變量也都合并到一起了,甚至完全消失不見了。
- 很多方法也進(jìn)行了合并。
- 除了代碼入口點(diǎn)之外,留下來方法名和變量名全都改變了。
- 優(yōu)化了代碼,可以看到上面 public static Main sMain = null; 混淆完自動給改成了 private。
- 他還會自動把一部分方法優(yōu)化為 final 的。
為什么 Main 這個類以及 main 方法沒有被混淆呢?
在 ProGuard 默認(rèn)生成的配置文件下有個條匹配規(guī)則
-keepclasseswithmembers public class * {
public static void main(java.lang.String[]);
}
解釋一下:匹配每個類里面的 main 方法為代碼入口點(diǎn),如果沒有任何一個類有 main 方法。那么我們的上面的例子就是空的文件了,因?yàn)樵趬嚎s階段就已經(jīng)把所有代碼全都刪了。
main 方法是 Java 應(yīng)用程序的入口方法,程序運(yùn)行執(zhí)行的第一個方法。
小結(jié)
經(jīng)過這個小例子,除了預(yù)校驗(yàn)之外,其他特性我們都已經(jīng)明顯的看到了。概念也大概的懂了。恭喜你打怪升級成功,快去看看下一篇吧。