本文已授權(quán)微信公眾號「玉剛說」獨(dú)家發(fā)布才沧。
這篇文章是「Java 混淆那些事」的第三篇温圆,我們來真槍真刀的干一下子孩革,用實(shí)際行動驗(yàn)證了解一下 ProGuard 的 Keep 語法,這篇代碼偏多锅移,希望大家好好理解饱搏。
閱讀提示:上半部分純屬個(gè)人總結(jié)窍帝,不明白請看下半部分的例子诽偷,讀完了根據(jù)自己的理解實(shí)踐一下。
簡介 Keep 語法
那么 keep 語法有什么用呢压怠?如果我們對外提供了一套 Library 飞苇,如果不指定代碼入口點(diǎn)恐怕是所有代碼都要被刪掉了,所以我們要指定「代碼入口點(diǎn)」雨让,并且告訴 ProGuard 那些類名絕對不能變動忿等,那些方法名不能變動等等贸街。
Keep 有以下幾種用法
- -keep [,modifier,…] class_specification 匹配類名以及指定的方法或字段,為代碼入口點(diǎn)捐川」帕ぃ可以單獨(dú)匹配類或者類和類成員冷溶。匹配到的類不會被混淆和刪除,匹配到的類成員不會被混淆和刪除纯衍,方法被當(dāng)作代碼入口點(diǎn)襟诸。
- -keepclassmembers [,modifier,…] class_specification 匹配類名以及規(guī)則指定的方法或字段基协,為代碼入口點(diǎn)澜驮。但是有個(gè)前提:就是必須在壓縮階段被保留的類才可以。
- -keepclasseswithmembers [,modifier,…] class_specification 它和 -keep 的作用基本一致悍缠,但是規(guī)則必須完全匹配類名以及類成員才能匹配成功,寫錯(cuò)類成員名稱或?qū)懖淮嬖陬惓蓡T名稱都會導(dǎo)致整條規(guī)則失效滤港。
其中 modifier 為可選配置趴拧,可以指定一個(gè)或多個(gè)著榴。 class_specification 是類和成員的模板
modifier 共有一下幾個(gè)可選值,當(dāng)然匹配范圍和限制還是要服從 keep 規(guī)則的缝龄。
- includecode 保證所指定的字段名稱不被混淆叔壤,而類型將被混淆口叙。只能用于字段妄田,否則報(bào)錯(cuò)。
- includedescriptorclasses 指定有返回值的方法或者字段脚曾,他們返回值的類型以及字段的類型不會被混淆启具,相關(guān)的類名以及包名也不會被混淆鲁冯。
- allowshrinking 縮減 -keep 的匹配范圍,如果這個(gè)類和方法不是必須的那么有可能會在壓縮階段被刪除撞芍。
- allowoptimization -keep 選項(xiàng)中指定的代碼入口點(diǎn)可以在優(yōu)化步驟中被優(yōu)化改變序无,但是它們可能會在優(yōu)化階段被刪除。
- allowobfuscation -keep 選項(xiàng)中指定的代碼入口點(diǎn)可以會被混淆改名米罚,但是它們不會被刪除丈探。
還有三種和上面的用法是相對應(yīng)的用法
- **-keepnames class_specification ** 就是 -keep,allowshrinking class_specification 的簡寫
- **-keepclassmembernames class_specification ** 就是 -keepclassmembers,allowshrinking class_specification 的簡寫
- **-keepclasseswithmembernames class_specification ** 就是 -keepclasseswithmembers,allowshrinking class_specification 的簡寫
實(shí)踐語法
這部分是實(shí)踐大家也可以跳過碗降,根據(jù)自己的理解親自動手操作即可讼渊,如果我有什么重要的或錯(cuò)誤結(jié)論歡迎指正尊剔。
在 ProGuard GUI 把混淆的配置文件保存须误,然后使用文件編輯器直接在配置文件下面添加即可。然后在 ProGuard 讀取并執(zhí)行奶甘。
/*
* 測試代碼結(jié)構(gòu)
* src
* -> DownloadClient.java
* -> DownloadManager.java
* -> http
* -> HttpDownload.java
* -> HttpRequest.java
*/
// 各個(gè)文件的具體代碼
// DownloadClient.java
public int status = 0;
public String url;
private HttpDownload httpDownload;
public DownloadClient(String url) {
this.url = url;
httpDownload = new HttpDownload();
}
public void start() {
status = 1;
httpDownload.start();
}
public void stop() {
status = 2;
httpDownload.stop();
}
// DownloadManager.java
HttpRequest httpRequest = new HttpRequest();
public HttpRequest getDownloadUrl() {
System.out.println(httpRequest.get());
return httpRequest;
}
// HttpDownload.java
private int i=0;
public void start(){
System.out.println("開始下載");
i++;
}
public void stop(){
System.out.println("停止下載");
i--;
}
// HttpRequest.java
public String get() {
return "請求成功";
}
實(shí)踐 keep 規(guī)則
這個(gè)例子我們不使用 main 方法臭家,只把這個(gè)小例子當(dāng)做一個(gè) SDK钉赁。
-keep 命令
//混淆腳本
-keep class DownloadClient {
public java.lang.String url;
public <init>(java.lang.String);
public void start();
}
處理效果
/*
* 代碼結(jié)構(gòu)
* a
* -> a.java
* defpackage
* -> DownloadClient.java
*/
// 各個(gè)文件的具體代碼
// a.java
private int a = 0;
public final void a() {
System.out.println("開始下載");
this.a++;
}
// DownloadClient.java
private int a = 0;
private a b;
public String url;
public DownloadClient(String str) {
this.url = str;
this.b = new a();
}
public void start() {
this.a = 1;
this.b.a();
}
這個(gè)效果很明顯:
- keep 指定的類和類成員都沒有被移除你踩,并且沒有被混淆(構(gòu)造方法和 start() 方法))邑蒋。
- 指定的字段也沒有被混淆医吊。
- keep 指定的方法被作為了代碼入口點(diǎn),調(diào)用到的相關(guān)類和方法也沒有被移除束莫,但是被混淆了。
還有幾種情況策严,大家自己試一下饿敲。
- 如果規(guī)則不寫任何類成員怀各,就只會留下一個(gè)空的類文件
- 如果規(guī)則寫了不存在的類成員,也不會有什么效果寿酌。
- 可以讓字段名不被混淆醇疼。但是字段類型被混淆了法焰。
- 可以讓方法名不被混淆壶栋。但是方法返回值類型被混淆了。
-keepclasseswithmembers
這個(gè)效果和 keep 完全一樣琉兜,但是稍微有點(diǎn)不同毙玻,大家也可以自己試一試桑滩。
- 如果不寫任何類成員运准,混淆后就只會留下一個(gè)空的類文件,但是 ProGuard 會給出提示將規(guī)則改變?yōu)?-keep该互。
- 如果寫了不存在的類成員韭畸,那么當(dāng)前這條 -keepclasseswithmembers 規(guī)則沒有任何效果。它就沒有 -keep 那么佛系了喂分。
-keepclassmembers
// 混淆腳本
-keep class DownloadClient
-keepclassmembers class DownloadClient {
private http.HttpDownload httpDownload;
public <init>(java.lang.String);
public void start();
}
處理效果
/*
* 代碼結(jié)構(gòu)
* a
* -> a.java
* defpackage
* -> DownloadManager.java
*/
// 各個(gè)文件的具體代碼
// a.java
null
// DownloadManager.java
a httpRequest = new a();
public a getDownloadUrl() {
System.out.println("請求成功");
return this.httpRequest;
}
大家看到我這次寫了一條 -keep 混淆規(guī)則蒲祈,為什么呢蜒车?
因?yàn)樵趬嚎s階段能留下來的類上 -keepclassmembers 才能有效果酿愧,否則沒有效果邀泉。所以要先把相關(guān)類留下來汇恤。
我們總結(jié)一下 -keepclassmembers 的效果
- 作用的類必須在壓縮階段被保留 -keepclassmembers 才可以生效。
- 可以讓類的成員不被混淆基括。但是字段類型被混淆了风皿。
- 可以讓方法名不被混淆匠璧。但是方法返回值類型被混淆了夷恍。
- -keepclassmembers 同樣可以起到指定代碼入口點(diǎn)的工作,雖然 a.java 是空的遏暴,但這因?yàn)槭谴a優(yōu)化的作用朋凉。
還是幾種情況袋励,大家自己試一下。
- 如果寫的 -keepclassmembers 規(guī)則沒有寫類成員盖灸,ProGuard 會給出提示改變?yōu)?-keep赁炎。
- 如果寫的某個(gè)類成員沒有匹配到就不會生效,但是其余規(guī)則的匹配到的還是會生效的讥裤,并不會像 -keepclasseswithmembers 那么霸道己英。
實(shí)踐 modifier 規(guī)則
includecode
//混淆腳本
-keep class DownloadClient{
public <init>(java.lang.String);
public void start();
}
-keep,includecode class DownloadClient {
private http.HttpDownload httpDownload;
}
混淆效果
/*
* 代碼結(jié)構(gòu)
* a
* -> a.java
* defpackage
* -> DownloadClient.java
*/
// 各個(gè)文件的具體代碼
// a.java
private int a = 0;
public final void a() {
System.out.println("開始下載");
this.a++;
}
// DownloadClient.java
private int a = 0;
private String b;
private a httpDownload;
public DownloadClient(String str) {
this.b = str;
this.httpDownload = new a();
}
public void start() {
this.a = 1;
this.httpDownload.a();
}
實(shí)際效果
- -keep 所指定的字段名稱不被混淆损肛,但是類型還是被混淆的治拿。
動手試一試
- 只能做用于字段笆焰,否則報(bào)錯(cuò)嚷掠。
includedescriptorclasses
//混淆腳本
-keep class DownloadClient {
public <init>(java.lang.String);
public void start();
}
-keep,includedescriptorclasses class DownloadManager {
public http.HttpRequest getDownloadUrl();
}
-keep,includedescriptorclasses class DownloadClient {
private http.HttpDownload httpDownload;
}
混淆效果
/*
* 代碼結(jié)構(gòu)
* http
* -> HttpRequest.java
* -> HttpDownload.java
* defpackage
* -> DownloadClient.java
* -> DownloadManager.java
*/
// 各個(gè)文件的具體代碼
// HttpRequest.java
public static String a() {
return "請求成功";
}
// HttpDownload.java
private int a = 0;
public final void a() {
System.out.println("開始下載");
this.a++;
}
// DownloadClient.java
private int a = 0;
private String b;
private HttpDownload httpDownload;
public DownloadClient(String str) {
this.b = str;
this.httpDownload = new HttpDownload();
}
public void start() {
this.a = 1;
this.httpDownload.a();
}
// DownloadManager.java
private HttpRequest a = new HttpRequest();
public HttpRequest getDownloadUrl() {
System.out.println(HttpRequest.a());
return this.a;
}
實(shí)際效果
- keep 所指定的字段名稱不被混淆叠国,而且字段類型也沒有混淆了粟焊。
- keep 一個(gè)帶返回類型的方法,返回值的類型也不會被混淆
allowshrinking
//混淆腳本
-keep class DownloadClient {
public <init>(java.lang.String);
public void start();
}
-keep,allowshrinking class DownloadManager {
public http.HttpRequest getDownloadUrl();
}
混淆效果
/*
* 代碼結(jié)構(gòu)
* a
* -> a.java
* defpackage
* -> DownloadClient.java
*/
// 各個(gè)文件的具體代碼
// a.java
private int a = 0;
public final void a() {
System.out.println("開始下載");
this.a++;
}
// DownloadClient.java
private int a = 0;
private String b;
private a c;
public DownloadClient(String str) {
this.b = str;
this.c = new a();
}
public void start() {
this.a = 1;
this.c.a();
}
實(shí)際效果
- -keep 指令是保留相關(guān)的類,但是 DownloadManager 并沒有保留下來合瓢,就是因?yàn)?allowshrinking 的作用透典,如果這個(gè)類和方法不是必須的那么有可能會在壓縮階段被刪除。
動手試一試
- 如果其他代碼入口點(diǎn)調(diào)用了該方法税弃,才會保留,效果跟去掉 allowshrinking modifier 的效果一致幔翰。
allowoptimization
//混淆腳本
-keep,allowoptimization class DownloadClient {
public <init>(java.lang.String);
public void start();
}
混淆效果
/*
* 代碼結(jié)構(gòu)
* defpackage
* -> DownloadClient.java
*/
// 各個(gè)文件的具體代碼
// DownloadClient.java
//空的
實(shí)際效果
- 我們雖然指定了代碼入口點(diǎn)遗增,但是我們并沒有用到款青,所以全都優(yōu)化階段刪除了抡草。
動手試一試
- 如果我們指定一個(gè)其他代碼入口點(diǎn)渠牲,并且調(diào)用了 DownloadClient 的 start() 方法步悠,那么他就會跟沒有 allowoptimization modifier 的效果一致了鼎兽。例如我們的例子,如果被調(diào)用就和如下腳本效果一致鹦付。
-keep class DownloadClient {
public <init>(java.lang.String);
public void start();
}
allowobfuscation
//混淆腳本
-keep,allowobfuscation class DownloadClient {
public <init>(java.lang.String);
public void start();
}
混淆效果
/*
* 代碼結(jié)構(gòu)
* a
* -> a.java
* defpackage
* -> a.java
*/
// 各個(gè)文件的具體代碼
// a/a.java
private int a = 0;
public final void a() {
System.out.println("開始下載");
this.a++;
}
// defpackage/a.java
private int a = 0;
private String b;
private a.a c;
public a(String str) {
this.b = str;
this.c = new a.a();
}
public void a() {
this.a = 1;
this.c.a();
}
實(shí)際效果
- 雖然代碼入口點(diǎn)的代碼保留了敲长,但是名稱全部都混淆了祈噪。
自己動手
- 如果指定沒有用到的代碼辑鲤,那么他也會保留并且同樣是被混淆的杠茬。
簡述 class_specification
官方描述的類規(guī)范模板,看著 Java 代碼很像舀透。
- [] 標(biāo)識可選礁击。
- | 表示 ‘或’ 的意思只能取一個(gè)哆窿。
- ...表示可以有多個(gè),簡單舉個(gè)例子 [[!]public|private|protected|static ... ] 可以包含 public static 這兩個(gè)强衡,從 Java 的角度理解也不難漩勤。
- {} 大括號是實(shí)實(shí)在在的大括號缩搅。
- () 就是實(shí)實(shí)在在的括號硼瓣,沒有什么其他意思堂鲤。
- ! 表示 ’否‘ ,例如:!class 規(guī)則匹配表示不能是這個(gè) class
- * 葵擎、 <fields> 半哟、 <init> 酬滤、 <methods> 都是通配符,我們下一篇再描述镜沽。
再放一張格式化過的比較好理解的圖
我對官方的規(guī)則進(jìn)行格式化了一下敏晤,這樣看是不是就好理解多了,如果不考慮通配符缅茉,其實(shí)就是跟 Java 正常的寫法是一致的嘴脾。比如我們上面例子中混淆的寫法,除了有個(gè) <init> 之外,其他都是普通的 Java 語法译打。
再說幾條上面沒有體現(xiàn)的規(guī)則
- 寫類名包名必須要寫全耗拓,比如 String 要寫 java.lang.String奏司。
- Builder```,
小結(jié)
到此為止 ProGuard 的 Keep 規(guī)則我們也簡單的聊了一下,希望大家自己多嘗試搪缨,然后總結(jié)一下每條命令的用處食拜,方便日后使用。