天天看點

關于proguard,你需要知道的全部proguard分為4個步驟:一、4個步驟中的常量配置二、keep配置三、其它常用配置四、proguard配置示例五、關于反射六、關于proguard配置的一些建議七、參考文檔:

proguard流程

proguard分為4個步驟:

  • 壓縮(shrink)

    移除未使用的類、方法、字段等;

  • 優化(optimize)

    優化位元組碼、簡化代碼等操作;

  • 混淆(obfuscate)

    使用簡短的、無意義的名稱重全名類名、方法名、字段等;

  • 預校驗(preverify)

    為class添加預校驗資訊。

一、4個步驟中的常量配置

1. 壓縮(shrink)

-dontshrink

聲明不進行壓縮操作,預設情況下,除了-keep配置(下詳)的類及其直接或間接引用到的類,都會被移除。

2. 優化(optimize)

-dontoptimize

不對class進行優化,預設開啟優化。

注意:由于優化會進行類合并、内聯等多種優化,-applymapping可能無法完全應用,需使用熱修複的應用,建議使用此配置關閉優化。

-optimizationpassesn

執行優化的次數,預設1次,多次能達到更好的優化效果。

-optimizationsoptimization_filter

優化配置,可進行字段優化、内聯、類合并、代碼簡化、算法指令精簡等操作。

#隻進行 移除未使用的局部變量、算法指令精簡
-optimizations code/removal/variable,code/simplification/arithmetic

#進行除 算法指令精簡、字段、類合并外的所有優化
-optimizations !code/simplification/arithmetic,!field/*,!class/merging/*
           

3. 混淆(obfuscate)

-dontobfuscate

不進行混淆,預設開啟混淆。除-keep指定的類及成員外,都會被替換成簡短、随機的名稱,以達到混淆的目的。

-applymappingfilename

根據指定的mapping映射檔案進行混淆。

-obfuscationdictionaryfilename

指定字段、方法名的混淆字典,預設情況下使用abc等字母組合,比如根據自己的喜好指定中文、特殊字元進行混淆命名。

-classobfuscationdictionaryfilename

指定類名混淆字典。

-packageobfuscationdictionaryfilename

指定包名混淆字典。

-useuniqueclassmembernames

指定相同的混淆名稱對應不同類的相同成員,不同的混淆名稱對應不同的類成員。在沒有指定這個選項時,不同類的不同方法都可能映射到a,b,c。

有一種情況,比如兩個不同的接口,擁有相同的方法簽名,在沒有指定這個選項時,這兩個接口的方法可能混淆成不同的名稱。但如果新增一個類同時實作這兩個接口,并且利用-applymapping指定之前的mapping映射檔案時,這兩個接口的方法必須混淆成相同的名稱,這時就和之前的mapping沖突了。

在某此熱修複場景下需要指定此選項。

-dontusemixedcaseclassnames

指定不使用大小寫混用的類名,預設情況下混淆後的類名可能同時包含大寫小字母。這在某些對大小寫不敏感的系統(如windowns)上解壓時,可能存在檔案被互相覆寫的情況。

-keeppackagenames[package_filter]

指定不混淆指定的包名,多個包名可以用逗号分隔,可以使用? * **通配符,并且可以使用否定符(!)。

-keepattributes[attribute_filter]

指定保留屬性,多個屬性可以用多個-keepattributes配置,也可以用逗号分隔,可以使用? * **通配符,并且可以使用否定符(!)。

比如,在混淆ibrary庫時,應該至少keep Exceptions, InnerClasses, Signature;如果在追蹤代碼,還需要keep符号表;使用到注解時也需要keep。

-keepattributes Exceptions,InnerClasses,Signature
-keepattributes SourceFile,LineNumberTable
-keepattributes *Annotation*
           

-keepparameternames

指定keep已經被keep的方法的參數類型和參數名稱,在混淆library庫時非常有用,可供IDE幫助使用者進行資訊提示和代碼自動填充。

4. 預校驗(preverify)

-dontpreverify

指定不對class進行預校驗,預設情況下,在編譯版本為micro或者1.6或更高版本時是開啟的。但編譯成Android版本時,預校驗是不必須的,配置這個選項可以節省一點編譯時間。

(Android會把class編譯成dex,并對dex檔案進行校驗,對class進行預校驗是多餘的。)

二、keep配置

-keep[,modifier,...] class_specification

指定類及類成員作為代碼入口,保護其不被proguard,如:

-keep class com.rush.Test
-keep interface com.rush.InterfaceTest
-keep class com.rush.** {
    <init>;
    public <fields>;
    public <methods>;
    public *** get*();
    void set*(***);
}
           
  • class表示keep類或接口
  • interface僅表示keep接口

類名 通配符如下:

| 通配符 | 含義 |

| --- |

| ? | 比對單個字元,包名分隔符(.)除外 |

| * | 比對除(.)外的任意字元 |

| ** | 比對任意字元(包含.),如com.rush.**比對com.rush包下的所有類及其所有子包的類。 |

字段和方法 通配符如下:

| 通配符 | 含義 |

| --- |

| <init> | 比對所有構造方法 |

| <fields> | 比對所有字段 |

| <methods> | 比對所有方法 |

| ? | 比對單個字元,包名分隔符(.)除外 |

| * | 比對除(.)外的任意字元 |

類型 通配符如下:

| 通配符 | 含義 |

| --- |

| % | 比對原始類型,如int, boolean等 |

| ? | 比對任意單個字元 |

| * | 比對除包名分隔符(.)外的任意字元 |

| ** | 比對任意字元,包括包名分隔符(.) |

| *** | 比對任意類型(原始類型、非原始類型、數組或非數組類型)|

| ... | 比對任意參數個數,任意參數類型 |

其中類配置完整定義如下,其中[]表示可選:

[@annotationtype] [[!]public|final|abstract|@ ...] [!]interface|class|enum classname
    [extends|implements [@annotationtype] classname]
[{
    [@annotationtype] [[!]public|private|protected|static|volatile|transient ...] <fields> |
                                                                      (fieldtype fieldname);
    [@annotationtype] [[!]public|private|protected|static|synchronized|native|abstract|strictfp ...] <methods> |
                                                                                           <init>(argumenttype,...) |
                                                                                           classname(argumenttype,...) |
                                                                                           (returntype methodname(argumenttype,...));
    [@annotationtype] [[!]public|private|protected|static ... ] *;
    ...
}]
           

keep過于簡單粗暴,proguard提供了6種不同的配置:

| 保留 | 防止被移除或重命名 | 防止被重命名(未使用的會被移除) |

| --- | --- |

| 類和類成員 | -keep | -keepnames |

| 僅類成員 | -keepclassmembers | -keepclassmembernames |

| 如類含有某成員,保留類及其成員 | -keepclasseswithmembers | -keepclasseswithmembernames |

三、其它常用配置

-verbose

指定在混淆過程中輸出更多資訊,配置這個選項後,在遇到異常時,将輸出完整的堆棧,而不僅僅是異常消息。

-dontnote[class_filter]

指定不輸出潛在的錯誤或者遺漏,比如拼寫錯誤或者缺失了有用的資訊。class_filter是一個正規表達式,比對到類将不輸出這些資訊。

-dontwarn[class_filter]

指定一組類,不警告這些類中找不到引用或其它重要的問題。這個選項是很危險的,比如,找不到引用的錯誤可能導緻代碼不能正常work。

(在引用一些存在警告的jar包,這個選項比較有用。)

-ignorewarnings

指定輸出是以警告資訊,但繼續進行混淆。同上一選項,慎用。

-printconfiguration[filename]

指定輸出整個過程中的所有配置,輸出到标準輸出流或者指定檔案中。這有時候在排程配置時有用。

-dump[filename]

指定在任一處理過程後,輸出class檔案的結構,可以輸出到标準輸出流或者指定檔案中。

四、proguard配置示例

4.1 Android預設推薦配置

在IDE自動生成的project.properties檔案中,有這樣一行:

#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt
           

Android Studio預設生成的build.gradle檔案有如下配置:

buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
}
           

其中getDefaultProguardFile('proguard-android.txt')擷取的也是tools/proguard/proguard-android.txt。下面看一下這個檔案的配置:

# 不使用大小寫混合類名
-dontusemixedcaseclassnames
# 不路過引用庫中的非public類
-dontskipnonpubliclibraryclasses
# 輸出更多資訊
-verbose

# 不進行優化
-dontoptimize
# 不進行預校驗
-dontpreverify

# keep注解
-keepattributes *Annotation*
#keep google license服務接口
-keep public class com.google.vending.licensing.ILicensingService
-keep public class com.android.vending.licensing.ILicensingService

# keep native方法及其所屬類
-keepclasseswithmembernames class * {
    native <methods>;
}

# keep自定義view的get/set方法
-keepclassmembers public class * extends android.view.View {
   void set*(***);
   *** get*();
}

# keep繼續自Activity中所有包含public void *(android.view.View)簽名的方法,如onClick
-keepclassmembers class * extends android.app.Activity {
   public void *(android.view.View);
}

# keep枚舉中的values和valueOf方法
-keepclassmembers enum * {
    public static **[] values();
    public static ** valueOf(java.lang.String);
}

# keep Parcelable的CREATOR成員
-keepclassmembers class * implements android.os.Parcelable {
  public static final android.os.Parcelable$Creator CREATOR;
}

# keep R檔案的靜态字段
-keepclassmembers class **.R$* {
    public static <fields>;
}

# 不輸出support包中的警告
-dontwarn android.support.**
           

4.2 一個典型library庫的配置

示例引用自官方文檔samples

# 儲存mapping映射檔案到out.map
-printmapping out.map 

# keep已keep方法的參數類型及參數名稱
-keepparameternames 
# 這個配置未弄清楚,待測試
-renamesourcefileattribute SourceFile 
-keepattributes Exceptions,InnerClasses,Signature,Deprecated,
                SourceFile,LineNumberTable,*Annotation*,EnclosingMethod 

# keep所有類的protected成員
-keep public class * { 
      public protected *; 
} 

# keep在jdk 1.2中編譯器插入的代碼
-keepclassmembernames class * { 
    java.lang.Class class$(java.lang.String); 
    java.lang.Class class$(java.lang.String, boolean); 
} 

# keep native方法
-keepclasseswithmembernames,includedescriptorclasses class * { 
    native <methods>; 
} 

# keep枚舉中的values和valueOf方法
-keepclassmembers,allowoptimization enum * { 
    public static **[] values(); 
    public static ** valueOf(java.lang.String); 
} 

# keep系列化相關方法
-keepclassmembers class * implements java.io.Serializable { 
    static final long serialVersionUID; 
    private static final java.io.ObjectStreamField[] serialPersistentFields; 
    private void writeObject(java.io.ObjectOutputStream); 
    private void readObject(java.io.ObjectInputStream); 
    java.lang.Object writeReplace(); 
    java.lang.Object readResolve(); 
} 
           

4.3 一個典型Android App的配置

示例引用自官方文檔samples

-dontpreverify 
-repackageclasses '' 
-allowaccessmodification 
# 不優化算法指令
-optimizations !code/simplification/arithmetic 
-keepattributes *Annotation* 

# keep繼承自系統元件的類
-keep public class * extends android.app.Activity 
-keep public class * extends android.app.Application 
-keep public class * extends android.app.Service 
-keep public class * extends android.content.BroadcastReceiver 
-keep public class * extends android.content.ContentProvider
 
# keep自定義view及其構造方法、set方法
-keep public class * extends android.view.View { 
      public <init>(android.content.Context); 
      public <init>(android.content.Context, android.util.AttributeSet); 
      public <init>(android.content.Context, android.util.AttributeSet, int); 
      public void set*(...); 
} 

-keepclasseswithmembers class * { 
    public <init>(android.content.Context, android.util.AttributeSet); 
} 

-keepclasseswithmembers class * { 
    public <init>(android.content.Context, android.util.AttributeSet, int); 
} 

-keepclassmembers class * extends android.content.Context { 
    public void *(android.view.View); 
    public void *(android.view.MenuItem); 
} 

-keepclassmembers class * implements android.os.Parcelable { 
    static ** CREATOR; 
} 

-keepclassmembers class **.R$* { 
    public static <fields>; 
} 

# keep javascript注釋的方法,使用到webview js回調方法的需要添加此配置
-keepclassmembers class * { 
    @android.webkit.JavascriptInterface <methods>; 
} 
           

五、關于反射

并不是所有會被反射引用的類都必須keep,在progurad過程中能直接分析到引用的類會被proguard做相應的處理:

# Class.forName的類名"SomeClass"被混淆後自動替換
Class.forName("SomeClass")
SomeClass.class
# 以下字段和方法名都會在被混淆後自動替換
SomeClass.class.getField("someField")
SomeClass.class.getDeclaredField("someField")
SomeClass.class.getMethod("someMethod", new Class[] {})
SomeClass.class.getMethod("someMethod", new Class[] { A.class })
SomeClass.class.getMethod("someMethod", new Class[] { A.class, B.class })
SomeClass.class.getDeclaredMethod("someMethod", new Class[] {})
SomeClass.class.getDeclaredMethod("someMethod", new Class[] { A.class })
SomeClass.class.getDeclaredMethod("someMethod", new Class[] { A.class, B.class })
AtomicIntegerFieldUpdater.newUpdater(SomeClass.class, "someField")
AtomicLongFieldUpdater.newUpdater(SomeClass.class, "someField")
AtomicReferenceFieldUpdater.newUpdater(SomeClass.class, SomeType.class, "someField")
           

寫個demo驗證下:

Class<?> clazz = Class.forName("com.rush.test.SimpleClass1");
clazz.getDeclaredMethod("Test1");
SimpleClass2.class.getDeclaredField("mTestField");
SimpleClass2.class.getDeclaredMethod("Test2");
           

對以上代碼編譯并proguard,結果如下:

Class.forName("com.rush.a.a").getDeclaredMethod("Test1", new Class[0]);
b.class.getDeclaredField("a");
b.class.getDeclaredMethod("a", new Class[0]);
           
  • 通過Class.forName反射的class com.rush.test.SimpleClass1"被自動替換成了"com.rush.a.a";
  • 但通過Class.forName擷取的class再去反射方法沒有正确處理;
  • 通過完整class.getDeclaredField或者getDeclaredMethod反射時能夠把字段名和方法名自動替換掉。

從結果看,反射并不是大家想像的那樣必須keep,proguard能自動分析到引用的情況都能正确處理。但有些類是在配置檔案裡配置,或者動态拼接類名反射的,這些情況需要做好keep。

為了問題追蹤的友善,建議所有會被反射引用的代碼和library public接口都做好keep。

六、關于proguard配置的一些建議

  • 所有會被反射引用的類都做好keep(建議,雖然有些反射能被正确處理)。

    如native方法,四大元件,接口model,枚舉,序列化類等。

  • 隻keep必須保留的内容,不要過度keep
  • 使用熱修複的App,添加-dontoptimize配置

七、參考文檔:

  • https://www.guardsquare.com/en/proguard/manual/introduction
  • https://www.guardsquare.com/en/proguard/manual/usage
  • https://www.guardsquare.com/en/proguard/manual/examples

作者:rushjs

連結:https://www.jianshu.com/p/d768f6d1d93b

來源:簡書

簡書著作權歸作者所有,任何形式的轉載都請聯系作者獲得授權并注明出處。

轉載于:https://my.oschina.net/JiangTun/blog/1839302