Java 混淆那些事(三):了解 ProGuard Keep 規(guī)則

本文已授權(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è)效果很明顯:

  1. keep 指定的類和類成員都沒有被移除你踩,并且沒有被混淆(構(gòu)造方法和 start() 方法))邑蒋。
  2. 指定的字段也沒有被混淆医吊。
  3. keep 指定的方法被作為了代碼入口點(diǎn),調(diào)用到的相關(guān)類和方法也沒有被移除束莫,但是被混淆了。

還有幾種情況策严,大家自己試一下饿敲。

  1. 如果規(guī)則不寫任何類成員怀各,就只會留下一個(gè)空的類文件
  2. 如果規(guī)則寫了不存在的類成員,也不會有什么效果寿酌。
  3. 可以讓字段名不被混淆醇疼。但是字段類型被混淆了法焰。
  4. 可以讓方法名不被混淆壶栋。但是方法返回值類型被混淆了。
-keepclasseswithmembers

這個(gè)效果和 keep 完全一樣琉兜,但是稍微有點(diǎn)不同毙玻,大家也可以自己試一試桑滩。

  1. 如果不寫任何類成員运准,混淆后就只會留下一個(gè)空的類文件,但是 ProGuard 會給出提示將規(guī)則改變?yōu)?-keep该互。
  2. 如果寫了不存在的類成員韭畸,那么當(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 的效果

  1. 作用的類必須在壓縮階段被保留 -keepclassmembers 才可以生效。
  2. 可以讓類的成員不被混淆基括。但是字段類型被混淆了风皿。
  3. 可以讓方法名不被混淆匠璧。但是方法返回值類型被混淆了夷恍。
  4. -keepclassmembers 同樣可以起到指定代碼入口點(diǎn)的工作,雖然 a.java 是空的遏暴,但這因?yàn)槭谴a優(yōu)化的作用朋凉。

還是幾種情況袋励,大家自己試一下。

  1. 如果寫的 -keepclassmembers 規(guī)則沒有寫類成員盖灸,ProGuard 會給出提示改變?yōu)?-keep赁炎。
  2. 如果寫的某個(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í)際效果

  1. -keep 所指定的字段名稱不被混淆损肛,但是類型還是被混淆的治拿。

動手試一試

  1. 只能做用于字段笆焰,否則報(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í)際效果

  1. keep 所指定的字段名稱不被混淆叠国,而且字段類型也沒有混淆了粟焊。
  2. 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í)際效果

  1. -keep 指令是保留相關(guān)的類,但是 DownloadManager 并沒有保留下來合瓢,就是因?yàn)?allowshrinking 的作用透典,如果這個(gè)類和方法不是必須的那么有可能會在壓縮階段被刪除。

動手試一試

  1. 如果其他代碼入口點(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í)際效果

  1. 我們雖然指定了代碼入口點(diǎn)遗增,但是我們并沒有用到款青,所以全都優(yōu)化階段刪除了抡草。

動手試一試

  1. 如果我們指定一個(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í)際效果

  1. 雖然代碼入口點(diǎn)的代碼保留了敲长,但是名稱全部都混淆了祈噪。

自己動手

  1. 如果指定沒有用到的代碼辑鲤,那么他也會保留并且同樣是被混淆的杠茬。

簡述 class_specification

官方描述的類規(guī)范模板,看著 Java 代碼很像舀透。

  1. [] 標(biāo)識可選礁击。
  2. | 表示 ‘或’ 的意思只能取一個(gè)哆窿。
  3. ...表示可以有多個(gè),簡單舉個(gè)例子 [[!]public|private|protected|static ... ] 可以包含 public static 這兩個(gè)强衡,從 Java 的角度理解也不難漩勤。
  4. {} 大括號是實(shí)實(shí)在在的大括號缩搅。
  5. () 就是實(shí)實(shí)在在的括號硼瓣,沒有什么其他意思堂鲤。
  6. ! 表示 ’否‘ ,例如:!class 規(guī)則匹配表示不能是這個(gè) class
  7. * 葵擎、 <fields> 半哟、 <init> 酬滤、 <methods> 都是通配符,我們下一篇再描述镜沽。

再放一張格式化過的比較好理解的圖

我對官方的規(guī)則進(jìn)行格式化了一下敏晤,這樣看是不是就好理解多了,如果不考慮通配符缅茉,其實(shí)就是跟 Java 正常的寫法是一致的嘴脾。比如我們上面例子中混淆的寫法,除了有個(gè) <init> 之外,其他都是普通的 Java 語法译打。

再說幾條上面沒有體現(xiàn)的規(guī)則

  1. 寫類名包名必須要寫全耗拓,比如 String 要寫 java.lang.String奏司。
  2. 連接內(nèi)部類乔询,比如 com.XXX 中有內(nèi)部類 Builder,這種構(gòu)建器的寫法很常見吧韵洋,混淆規(guī)則怎么寫呢竿刁?簡單舉個(gè)例子: ```-keep class com.XXXBuilder```,

小結(jié)

到此為止 ProGuard 的 Keep 規(guī)則我們也簡單的聊了一下,希望大家自己多嘗試搪缨,然后總結(jié)一下每條命令的用處食拜,方便日后使用。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末副编,一起剝皮案震驚了整個(gè)濱河市负甸,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌痹届,老刑警劉巖呻待,帶你破解...
    沈念sama閱讀 206,378評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異队腐,居然都是意外死亡蚕捉,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評論 2 382
  • 文/潘曉璐 我一進(jìn)店門柴淘,熙熙樓的掌柜王于貴愁眉苦臉地迎上來鱼冀,“玉大人,你說我怎么就攤上這事悠就。” “怎么了充易?”我有些...
    開封第一講書人閱讀 152,702評論 0 342
  • 文/不壞的土叔 我叫張陵梗脾,是天一觀的道長。 經(jīng)常有香客問我盹靴,道長炸茧,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,259評論 1 279
  • 正文 為了忘掉前任稿静,我火速辦了婚禮梭冠,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘改备。我一直安慰自己控漠,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,263評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著盐捷,像睡著了一般偶翅。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上碉渡,一...
    開封第一講書人閱讀 49,036評論 1 285
  • 那天聚谁,我揣著相機(jī)與錄音,去河邊找鬼滞诺。 笑死形导,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的习霹。 我是一名探鬼主播朵耕,決...
    沈念sama閱讀 38,349評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼序愚!你這毒婦竟也來了憔披?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,979評論 0 259
  • 序言:老撾萬榮一對情侶失蹤爸吮,失蹤者是張志新(化名)和其女友劉穎芬膝,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體形娇,經(jīng)...
    沈念sama閱讀 43,469評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡锰霜,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,938評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了桐早。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片癣缅。...
    茶點(diǎn)故事閱讀 38,059評論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖哄酝,靈堂內(nèi)的尸體忽然破棺而出友存,到底是詐尸還是另有隱情,我是刑警寧澤陶衅,帶...
    沈念sama閱讀 33,703評論 4 323
  • 正文 年R本政府宣布屡立,位于F島的核電站,受9級特大地震影響搀军,放射性物質(zhì)發(fā)生泄漏膨俐。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,257評論 3 307
  • 文/蒙蒙 一罩句、第九天 我趴在偏房一處隱蔽的房頂上張望焚刺。 院中可真熱鬧,春花似錦门烂、人聲如沸乳愉。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽匾委。三九已至拖叙,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間赂乐,已是汗流浹背薯鳍。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留挨措,地道東北人挖滤。 一個(gè)月前我還...
    沈念sama閱讀 45,501評論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像浅役,于是被迫代替她去往敵國和親斩松。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,792評論 2 345

推薦閱讀更多精彩內(nèi)容