title: ProGuard 初探
date: 2019-01-28
博客地址:ProGuard 初探
0x00 環(huán)境
0x01 ProGuard 總覽
ProGuard 是 java 字節(jié)碼優(yōu)化工具揭厚, 廣泛運(yùn)用到 Java 和 Android 項目中√可以有效的減少程序的大小提岔,提高運(yùn)行效率碱蒙,提高逆向分析的成本振亮。
ProGuard 優(yōu)化主要分為四個階段:
Shrink , Optimize, Obfuscate , Preverify 四個階段
- Shrink: 刪除沒有被使用的類和方法。
- Optimize: 對代碼指令進(jìn)行優(yōu)化褒搔。
- Obfuscate: 對代碼名稱進(jìn)行混淆星瘾。
- Preverify: 對 class 進(jìn)行預(yù)校驗,校驗 StackMap /StackMapTable 屬性念逞。
四個階段可以獨立運(yùn)行的,默認(rèn)全部開啟硕盹,可以通過配置 -dontshrink
,-dontoptimize
垛贤,-dontobfuscate
南吮,-dontpreverify
關(guān)閉對應(yīng)的階段.
注: ProGuard 處理 class 部凑。class 文件可以由 jikes 或 javac 或 Kotlin 生成, ProGuard 會根據(jù) javac 和 jikes 特性做針對性優(yōu)化比勉。
0x02 ProGuard 處理過程
1. Configuration Parse
1.1 過程
將 ProguardFile 文件編寫的規(guī)則解析成 Configuration 配置.
1.2 常用的參數(shù)
keepattributes
-keepattributes [attribute_filter]
保留類或方法或字段中 Attributes 屬性. Attributes 存在多種類型. 類型如下:
ProGuard 支持 Java 1-10 定義的所有的 Attributes。
為了保證程序能正常運(yùn)行需要保留了部分屬性:
ConstantValue
衣洁,Code
, BootstrapMethods
坊夫。其余屬性均可被刪除.ConstantValue
: 用于 final 修飾的 基本類型 或 String 類型字段环凿,指向字段的初始值。Code
:指向當(dāng)前方法的代碼指令瞭稼。BootstrapMethods
:和 invokedynamic
指令相配合實現(xiàn)動態(tài)調(diào)用方法。 例如 java 8 的 lambda。
ProGuard 的 Attributes 是在 Obfuscate
階段執(zhí)行悔雹。如果想該配置生效需要開啟 Obfuscate
.
Keep Option
Keep Option 會應(yīng)用在所有優(yōu)化階段,主要分為三種情況益涧。其余情況均是這三種情況的衍生闲询。
-keep [,modifier,...] class_specification
Keep 類限定下的類。 同時 Keep 該類下 成員限定 的方法或字段鸽捻。
-keepclassmembers [,modifier,...] class_specification
Keep 類限定 下的 成員限定 的方法或字段 御蒲。不 Keep 類限定 的類。
-keepclasseswithmembers [,modifier,...] class_specification
如果 類限定 和 成員限定 都存在痰滋, 那么 Keep 類 和 成員限定敲街。
Keep 在不同階段的含義不同: 在Shrink
階段為成員和類不被刪除, 在Optimize
階段為類和成員內(nèi)部的指令不被優(yōu)化. Obfuscate
階段為類和成員的名稱不被混淆.
class_specification
[@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 ... ] *;
...
}]
簡化
類限定{
成員限定[]
}
- 類限定: 通過 注解,accessFlags峻黍,包名挽拂,類名亏栈, 父類,簽名察署,接口等信息指定規(guī)則脐往。
- 成員限定:成員有兩種 字段和方法钙勃。
字段通過 注解辖源,accessFlags ,字段名矾湃,描述符,簽名等信息拍屑。
方法通過 注解僵驰,accessFlags, 方法名顽腾,參數(shù)崔泵,描述符,簽名等信息入篮。
默認(rèn)情況下 Keep Option 將應(yīng)用到 Shrink
陈瘦,Optimize
,Obfuscate
三個階段潮售。ProGuard 支持更為細(xì)致的控制痊项。通過 modifier
來控制。
keep | modifier | 作用 |
---|---|---|
allowshrinking | 該 keep 選項不在 Shrink 階段生效 |
|
allowoptimization | 該 keep 選項不在 Optimize 階段生效 |
|
allowobfuscation | 該 keep 選項不在 Obfuscate 階段生效 |
KeepNames Option
keepnames 等價于 keep,allowshrinking.
這里通過比較 keep 和 keepnames 來理解二者的區(qū)別.
keep option | 描述 |
---|---|
-keep class_specification |
類限定 和成員限制 不被刪除.同時 類限定 和成員限制 名稱不被混淆 |
-keepnames class_specification |
類限定 和 限定成員 名稱不被混淆, 不保證類限定 和成員限制 是否被刪除 |
-keepclassmembers class_specification |
類限定 和成員限制 不被刪除.同時 成員限制 的名稱不被混淆 |
-keepclassmembernames class_specification |
成員限制 的名稱不被混淆, 不保證他們是否被刪除 |
-keepclasseswithmembers class_specification | 如果 類限定 和成員限制 都存在, 那么類限定 和成員限制 不被刪除. 同時他們的名稱不被混淆 |
-keepclasseswithmembernames class_specification | 如果類限定 和成員限制 都存在, 那么類限定 和成員限制 名稱不被混淆, 不保證他們是否被刪除 |
2. Read Inputs
通過 -injars
和 -libraryjars
來聲明 input
. injars
描述程序運(yùn)行的代碼鞍泉。后續(xù)將對程序代碼做優(yōu)化训枢。 libraryjars
描述程序運(yùn)行中需要用的環(huán)境, 主要是為后面優(yōu)化階段提供信息分析。libraryjars
一般情況為 JRE
下的 rt.ja
r 和一些特定平臺類型的 jar 。
可以添加參數(shù)修改 Library 的默認(rèn)解析行為。
-skipnonpubliclibraryclasses
:解析 library 過程中跳過所有非 public 的類郁轻。
-dontskipnonpubliclibraryclasses
:解析 library 過程中解析所有類骑篙。
-dontskipnonpubliclibraryclassmembers
:解析 library 過程中解析所有字段和方法。
理論上如果 Library 中的類或成員是非 public 說明開發(fā)者并不希望被訪問或使用。 我們可以使用參數(shù)關(guān)閉须喂,關(guān)閉以后會加快運(yùn)行是己。
最終得到兩個 ClassPool 乘盼,ProgramClass 和 LibraryClass.
3. Initialize
ProGuard 基于兩個 ClassPool 對所有的 Class 進(jìn)行連接.
- 連接包括所有的類的層級關(guān)系 ( 父類,子類,interface )。
- 連接注解中 enum 常量潜索。
- 連接 code 字節(jié)碼相關(guān)字段和相關(guān)類。
method 的操作:關(guān)聯(lián)對應(yīng)的 class 和 method。
field 的 操作:關(guān)聯(lián)對應(yīng)的 class 和 field。 - 連接反射信息
反射是根據(jù)類名或方法名或字段名進(jìn)行操作的。當(dāng)我們將反射使用的字符串跟對應(yīng)的類或方法或字段連接上. 當(dāng)對應(yīng)的類或方法或字段混淆的時候同步變更,那么反射依舊生效, 之所以出現(xiàn)了 NoSuchMethodException, NoSuchFieldException,ClassNotFoundException 等問題,就是因為不同步更改信息. 同步更改需要在Initialize
階段將反射信息連接上對應(yīng)的類字段方法. 這里的連接并不是沒有缺陷的.但是會處理以下幾種情況
Class.forName("SomeClass");
Class.forName("SomeClass").newInstance().
AtomicIntegerFieldUpdater.newUpdater(A.class, "someField")
AtomicLongFieldUpdater.newUpdater(A.class, "someField")
AtomicReferenceFieldUpdater.newUpdater(A.class, B.class癌瘾,"someField")
AtomicIntegerFieldUpdater.newUpdater(..., "someField")
AtomicLongFieldUpdater.newUpdater(..., "someField")
AtomicReferenceFieldUpdater.newUpdater(..., "someField")
SomeClass.class.getMethod("someMethod",...)
SomeClass.class.getDeclaredMethod("someMethod",...)
SomeClass.class.getField("someMethod",...)
SomeClass.class.getDeclaredFields("someMethod",...)
SomeClass.class.getConstructor("someMethod",...)
SomeClass.class.getDeclaredConstructor("someMethod",...)
這里情況,反射信息能被正確連接.
- Q: 既然 Proguard 會為反射連接信息冠句。 那么我們還要編寫針對混淆的規(guī)則嗎罕扎?
A: 需要。這里的連接是基于模板匹配崖蜜。并沒有做更多的嘗試氏堤。 當(dāng)你的代碼不滿足上面模板的話星著。不能被正確配置。例子如下:
Class cls = Class.forName("SomeClass"); // SomeClass 可以被正確設(shè)置
Method ss = cls.getMethod("someMethod"); // someMethod 不能被正確設(shè)置茎刚, 因為不滿足任何模板
上面的情況如果要被正確模式初狰。 需要進(jìn)行靜態(tài)分析腥光。 這將會是一個相對耗時的操作。ProGuard 的靜態(tài)分析只出現(xiàn)在 Optimize
.
Note And Warn
在連接的過程中夫偶, ProGuard 會提供一些信息说铃,用于我們定位和發(fā)現(xiàn)問題。
信息主要分為兩部分。note 和 warn。
Note
- configuration 配置問題低滩。
- 重復(fù)的類。
- 反射潛在的問題。
Warn
- Library 中使用了程序中的類。
- 類晃痴,方法,字段 連接不到。 (即缺失相對應(yīng)的類稠屠,方法龙屉,字段)
通過參數(shù)關(guān)閉對應(yīng)類或?qū)?yīng)類下的警告信息
-dontnote [class_filter]
-dontwarn [class_filter]
Initialize 階段是后面所有優(yōu)化的基礎(chǔ)。
注:Note 和 Warn 相當(dāng)有用。通過 Note 信息我們可以知道可能潛在的混淆問題。Warn 可以幫助我們檢查 API 兼容凉夯。這非常有用。
4.Shrink
4.1 Shrink 優(yōu)化
ProGuard 會根據(jù) Configuration Roots 開始標(biāo)記, 同時根據(jù) Roots 為入口開始發(fā)散 . 標(biāo)記完成以后, 刪除未被標(biāo)記的類或成員. 最終得到的是精簡的 ClassPool 寝衫。
4.2 Roots
Roots 包括 類汹胃,方法,字段, 方法指令, 來源主要有 2 種。
- 通過 keep 同時 allowshrinking 不為 true 枝冀。計算 class_specification 中
類限定
和限定成員
- 通過 keepclasseswithmembers 關(guān)鍵字 allowshrinking 不為 true 。如果
類限定
和成員限定
都存在鸵钝。計算 class_specification 中 類限定 和 成員限定
4.3 標(biāo)記流程
通過開始標(biāo)記 Roots 發(fā)散到所有的代碼.
- 類:標(biāo)記類和父類。
- 方法:標(biāo)記方法. 如果是虛方法, 往上標(biāo)記對應(yīng)的虛方法.
- 字段:標(biāo)記字段和字段的相關(guān) Class损拢。
- 方法指令: 方法調(diào)用指令標(biāo)記相關(guān)類和方法, 字段操作指令標(biāo)記相關(guān)類和字段
注: 標(biāo)記過程中主要是使用 Initialize
階段的連接信息.
4.3 保留規(guī)則
- 一個類或方法或字段在 Roots 中將會保留哗讥。
- 一個類或方法或字段被使用將會保留。
- 一個類被 keep 保留, 那么它的構(gòu)造方法(<init>)员魏,非空靜態(tài)初始化(<cinit>)也將被保留厦章。
- 一個類被保留,那么它從 library 中繼承的方法也將被保留下來。
- 一個類被保留抵栈,那么它的父類也會被保留疤剑。
- 一個虛方法被保留,那么它父類對應(yīng)方法也將被保留。
- 一個類被保留葫隙,interface 被保留怀喉。 interface 方法被保留,該類實現(xiàn) interface 方法也被保留史侣。
- 內(nèi)部類或注解如果沒有使用將不會被保留税朴。注解如果在 ClassPool 中找不到那么會被保留瘾杭。
- 方法被保留。 它的參數(shù),行號也將被保留醇坝。
- Q: 如果 A 的復(fù)寫了 toString() 方法 。沒有被調(diào)用摊腋。 toString() 會被移除嗎?
A: 不會 toString() 是從 rt.jar java.lang.Object 類中繼承過來的坚踩。如果 A 被保留,那么從 LIbrary 中的繼承的方法將被無條件保留下來. 即使是一個空方法.
4.4 總結(jié)
Shrink 只會刪除沒有用的類和成員,并不會裁切方法。對于沒有使用的空方法或者沒有修改的虛方法. 這些方法我們是可以刪除的. 但是這些操作涉及到 code 指令的修改. ProGuard 在這階段并沒有做這么重的操作, 不過部分空方法會在 Optimize
階段被刪除,
5. Optimize
5.1 Optimize 優(yōu)化
Optimize 是四個階段最為復(fù)雜的地方从诲。也是耗時最長的階段。
Optimize 會在該階段通過對 代碼指令杭煎、 堆棧, 局部變量以及數(shù)據(jù)流分析.來模擬程序運(yùn)行中盡可能出現(xiàn)的情況來優(yōu)化和簡化代碼. 為了數(shù)據(jù)流分析的需要 Optimize 會多次遍歷所有字節(jié)碼雷恃。ProGuard 會開啟多線程來加快速度。
5.2 優(yōu)化選項
ProGuard 定義了 33 優(yōu)化項, 包含 class
节猿,field
魁蒜,method
弧轧,code
四個緯度从祝。
private static final String CLASS_MARKING_FINAL = "class/marking/final";
private static final String CLASS_UNBOXING_ENUM = "class/unboxing/enum";
private static final String CLASS_MERGING_VERTICAL = "class/merging/vertical";
private static final String CLASS_MERGING_HORIZONTAL = "class/merging/horizontal";
private static final String CLASS_MERGING_WRAPPER = "class/merging/wrapper";
private static final String FIELD_REMOVAL_WRITEONLY = "field/removal/writeonly";
private static final String FIELD_MARKING_PRIVATE = "field/marking/private";
private static final String FIELD_PROPAGATION_VALUE = "field/propagation/value";
private static final String METHOD_MARKING_PRIVATE = "method/marking/private";
private static final String METHOD_MARKING_STATIC = "method/marking/static";
private static final String METHOD_MARKING_FINAL = "method/marking/final";
private static final String METHOD_MARKING_SYNCHRONIZED = "method/marking/synchronized";
private static final String METHOD_REMOVAL_PARAMETER = "method/removal/parameter";
private static final String METHOD_PROPAGATION_PARAMETER = "method/propagation/parameter";
private static final String METHOD_PROPAGATION_RETURNVALUE = "method/propagation/returnvalue";
private static final String METHOD_INLINING_SHORT = "method/inlining/short";
private static final String METHOD_INLINING_UNIQUE = "method/inlining/unique";
private static final String METHOD_INLINING_TAILRECURSION = "method/inlining/tailrecursion";
private static final String CODE_MERGING = "code/merging";
private static final String CODE_SIMPLIFICATION_VARIABLE = "code/simplification/variable";
private static final String CODE_SIMPLIFICATION_ARITHMETIC = "code/simplification/arithmetic";
private static final String CODE_SIMPLIFICATION_CAST = "code/simplification/cast";
private static final String CODE_SIMPLIFICATION_FIELD = "code/simplification/field";
private static final String CODE_SIMPLIFICATION_BRANCH = "code/simplification/branch";
private static final String CODE_SIMPLIFICATION_OBJECT = "code/simplification/object";
private static final String CODE_SIMPLIFICATION_STRING = "code/simplification/string";
private static final String CODE_SIMPLIFICATION_MATH = "code/simplification/math";
private static final String CODE_SIMPLIFICATION_ADVANCED = "code/simplification/advanced";
private static final String CODE_REMOVAL_ADVANCED = "code/removal/advanced";
private static final String CODE_REMOVAL_SIMPLE = "code/removal/simple";
private static final String CODE_REMOVAL_VARIABLE = "code/removal/variable";
private static final String CODE_REMOVAL_EXCEPTION = "code/removal/exception";
private static final String CODE_ALLOCATION_VARIABLE = "code/allocation/variable";
5.2.1 Class 緯度
class/marking/final
沒有派生的類使用 final 修飾银伟。
class/unboxing/enum
將枚舉的使用轉(zhuǎn)換成常量 int 的使用蒿褂。
當(dāng)枚舉出現(xiàn)如下情況不對其優(yōu)化
- 枚舉實現(xiàn)了自定義接口齿坷。并且被調(diào)用李滴。
- 代碼中使用了不同簽名來存儲枚舉闲先。
- 使用 instanceof 指令判斷。
- 在枚舉加鎖操作毒租。
- 對枚舉強(qiáng)轉(zhuǎn)灾梦。
- 在代碼中調(diào)用靜態(tài)方法 valueOf 方法鲫忍。
- 定義可以外部訪問的方法鸦泳。
優(yōu)勢:更小的占用內(nèi)存,更快的執(zhí)行效率实撒。但條件較為苛刻其做。
class/merging/wrapper
將只有一個 targetClass 字段類型的 wrapper class 嘗試合并到 targetClass class 审残。 即時 targetClass 將擁有 wrapper 的所有方法.。同時將 wrapper 指令調(diào)用的轉(zhuǎn)成 targetClass 的指令調(diào)用当悔。
wrapper 和 targetClass 滿足如下條件:
- wrapper 構(gòu)造函數(shù)只有一個參數(shù), 參數(shù)類型為 targetClass。
- wrapper 只有一個字段且非靜態(tài), 類型為 targetClass
- wrapper 和 targetClass 父類是 java/lang/Object 。
- wrapper 沒有注解
- 兩個類擁有互相訪問權(quán)限括眠。
- wrapper 和 targetClass 沒有繼承關(guān)系
- wrapper 沒有 instantof 指令和強(qiáng)轉(zhuǎn)的使用
- 兩個沒有使用反射實例化
- 兩個沒有存在相同的方法。
- wrapper 沒有子類钞护。
注: class/merging/wrapper
該項優(yōu)化只會 外部類 merge 非靜態(tài)內(nèi)部類沾歪。 ProGuard 會匹配 wrapper 的構(gòu)造函數(shù)。
this.x = arg0;
super.<init>;
return;
匹配 javac 為內(nèi)部類自動生成的一參構(gòu)造函數(shù)烫沙。 對于非內(nèi)部類的構(gòu)造函數(shù) super.<init>;
是第一個指令。后續(xù)才是字段的賦值的指令肢扯。
class/merging/vertical
滿足以下情況合并子類的方法和字段
- 子類沒有注解
- 兩個類擁有互相訪問權(quán)限。
- 子類沒有 instantof 指令和強(qiáng)轉(zhuǎn)的使用
- 子類和父類沒有使用反射實例化
- 子類沒有靜態(tài)字段
- 子類沒有內(nèi)部類只怎,不是他人的內(nèi)部類
- 兩個沒有存在相同的方法赴精。
class/merging/horizontal
滿足以下情況合并兄弟類(同一個父類)的方法和字段
- 兄弟類沒有注解
- 兩個類擁有互相訪問權(quán)限。
- 兄弟類沒有 instantof 指令和強(qiáng)轉(zhuǎn)的使用
- 兩個沒有使用反射實例化
- 兄弟類沒有靜態(tài)字段
- 兄弟類沒有內(nèi)部類,不是他人的內(nèi)部類
- 兩個沒有存在相同的方法畏浆。
- 雙方派生類沒有和對方私有的方法相同的簽名扎谎。(主要保證合并以后不會出現(xiàn)方法沖突)
- 雙方派生類不能擁有對方可見的字段凤瘦。(主要保證合并以后不會出現(xiàn)字段沖突)
5.2.2 Field 緯度
field/removal/writeonly
刪除只有寫沒有讀的字段壁酬。同時刪除寫的指令鳞上。反射的字段屬于即讀又寫。
field/marking/private
將只在申明類中使用,沒有被反射方式使用,使用 private 修飾
field/propagation/value
優(yōu)化固定值字段的調(diào)用
字段滿足如下
- 字段類型為 int,char,long,double,boolean,float,byte踩验,short
- 字段是恒固定值咬腋。
通過下面例子理解一下
public class Constant {
public static final int C_1 = 12;
}
//優(yōu)化前
fun1(Constant.C_1);
//優(yōu)化后
fun1(12);
注: 如果字段被 final 修飾,ProGuard 認(rèn)為它是一個固定的值宾肺。 對于非 final 修飾。盡管在后續(xù)的操作中沒有被修改,但 ProGuard 認(rèn)為字段存在一個初始值氢烘。這或許是對的。通過下面例子理解一下饲漾。
public final int field1 = 12; // 固定值 12
public int field2 = 12;// 初始值為0 财剖,在 init 方法中被賦值為12 具壮。
5.2.3 Method 緯度
method/marking/private
只在申明類中使用批旺,且沒有被反射調(diào)用方法使用 private 修飾
method/marking/static
嘗試將方法使用 static 修飾
滿足以下條件:
- 該方法非靜態(tài)
- 方法沒有使用 this 參數(shù)心例;虛方法要保證在整個繼承樹中都沒有使用 this 參數(shù)望众。
method/marking/final
為方法添加 final 修飾截驮。
需滿足如下任一個條件:
- 類使用 final 修飾笑陈,方法非空非私有非抽象
- 沒有了派生類,
- 該方法沒有派生類重載葵袭。
method/marking/synchronized
對 synchronized 修飾方法進(jìn)行去鎖涵妥。
需要滿足如下條件
- 非靜態(tài)方法
- 該方法未被使用。
method/removal/parameter
方法參數(shù)在方法中沒被使用到坡锡,虛方法要保證在整個繼承樹中都沒有使用參數(shù)蓬网。將會被裁切。同時會對方法名稱進(jìn)行重命名原先方法加+方法hashcode鹉勒。
注:這里對方法重命名并沒有檢查是否存在相同簽名的方法帆锋。但是出現(xiàn)該情況的比例比較小
method/propagation/parameter
只支持 int,char贸弥,long窟坐,double海渊,boolean绵疲,float,byte臣疑,short 類型入?yún)?br>
當(dāng)入?yún)⑹枪潭ㄖ档臅r盔憨,對入?yún)⑦M(jìn)行優(yōu)化。
通過如下例子感受一下:
// 優(yōu)化前
public int main() {
int value = 99;
....
value ++讯沈;
call( value ); //
}
// 優(yōu)化后
public int main() {
int value = 99;
....
// 通過分析郁岩, 這里返回的入?yún)⒖偸?00
call(100); //
}
該項優(yōu)化次數(shù),在 6.0.3 版本統(tǒng)計存在問題缺狠,原因是在統(tǒng)計的時候缺少靜態(tài)方法或非靜態(tài)方法的判斷问慎。具體可看 #mr6
method/propagation/returnvalue
優(yōu)化只支持 int,char挤茄,long如叼,double,boolean穷劈,float笼恰,byte,short 這些類型的作為方法返回值歇终。當(dāng)返回值是固定值社证, 那么對其進(jìn)行優(yōu)化。
method/inlining/short
method/inlining/unique
嘗試內(nèi)聯(lián)方法评凝。
該方法滿足如下條件
- unique 方法只被調(diào)用一次追葡。short 方法字節(jié)碼數(shù)足夠小,android 項目默認(rèn)小于32. 可通過System.setProperty( “maximum.inlined.code.length” ,60)修改
- 方法 私有 或 靜態(tài) 或 final 類型的方法.
- 方法不存在遞歸.
- 方法不存在加鎖的
- 方法不存在 try catch
- 方法沒有返回值
- 方法不能是構(gòu)造方法
- 不同類, 不能有調(diào)用 super 的方法或 invokedynamic 指令
- 沒有回向分支
注: 內(nèi)聯(lián)會導(dǎo)致方法行號進(jìn)行偏移.
method/inlining/tailrecursion
尾遞歸優(yōu)化(略)
5.2.4 code 緯度
code/merging
合并不同分支下的代碼(略)
code/simplification/variable
詳情查看 InstructionSequenceConstants
- 優(yōu)化變量讀纫巳狻:
eg : iload/iload = iload/dup - 刪除多余變量的操作:
eg: iload/pop = nothing
eg: lload/pop2 = nothing
code/simplification/arithmetic
詳情查看 InstructionSequenceConstants
優(yōu)化指令中的運(yùn)算疾渣。
- 乘法指令轉(zhuǎn)成左移指令:
eg: * 8 = ... << 3 - 簡化指令的個數(shù):i=i+1 = i++
- ...
code/simplification/cast
詳情查看 InstructionSequenceConstants
- 優(yōu)化多個連續(xù)的 cast 指令
code/simplification/field
詳情查看 InstructionSequenceConstants
刪除無用字段操作指令操作,
eg: getfield/putfield = nothing
優(yōu)化字段操作指令崖飘。
eg: getstatic/getstatic = getstatic/dup
code/simplification/branch
詳情查看 InstructionSequenceConstants
刪除一些無用的分支榴捡,
簡化分支判斷指令
code/simplification/object
詳情查看 InstructionSequenceConstants
- 簡化代碼中多余的 equals 判斷。
eg: object.equals(object) = true - 對包裝器類型實例化 轉(zhuǎn)成 包裝器類型的工廠方法朱浴。
eg: new Integer(v) = Integer.valueof(v)
code/simplification/string
詳情查看 InstructionSequenceConstants
優(yōu)化 String 的使用吊圾。合并多個靜態(tài)字符串
- 優(yōu)化 String equals 部分情況:
eg:"abc".equals("abc") = true - 優(yōu)化 String 靜態(tài)方法 valueOf 和 concat :
eg:String.valueOf(12) = "12"
eg: "a".concat("b") = "ab" - 優(yōu)化 StringBuilder StringBuffer 的init,append() 翰蠢,toString() 方法
eg:new StringBuffer().append("abc") = new StringBuffer("abc")
eg:new StringBuffer("a").append("bc"") = new StringBuffer("abc")
eg:StringBuffer#append("ab").append("c") = StringBuffer#append("abc")
eg:StringBuffer#append("") = StringBuffer#
eg:new StringBuffer("a").append(12).toString() = "a".concat(String.valueOf(12))
code/simplification/math
詳情查看 InstructionSequenceConstants
- java.lang.Math 的方法進(jìn)行優(yōu)化.
eg:(float)Math.abs((double)...) = Math.abs(float) - 對于android 項目還會進(jìn)行優(yōu)化
將所有的android.util.FloatMath的調(diào)用轉(zhuǎn)換成java.lang.Math 因為高版本的FloatMath 已經(jīng)被廢棄了项乒。
code/removal/simple
去除不會到達(dá)的代碼塊。
code/removal/variable
去除方法沒有用到局部變量
code/removal/exception
try catch 里面的代碼指令不會發(fā)生異常梁沧, 移除 try catch 語句檀何。
code/allocation/variable
優(yōu)化局部變量的使用
// 優(yōu)化前
String ss = "99";
System.out.println(ss);
String ss1 = "99";
System.out.println(ss1);
// 優(yōu)化后
String ss1 = "99";
System.out.println(ss);
ss1 = "99";
System.out.println(ss1);
code/removal/advanced
ProGuard 允許刪除一些沒有副作用的方法指令調(diào)用。
實現(xiàn)過程
ProGuard 標(biāo)記有副作用的指令和方法, 然后向上回溯標(biāo)記該指令上的對象,參數(shù),方法. 沒有被標(biāo)記的指令則可以被移除廷支。
通過下面例子理解一下:
public void func(){
/...
Object o = obj.funcA(a);
/...
}
方法 funcA 滿足以下幾點可以被移除
- funcA 的參數(shù)a不會逃逸.
逃逸:
經(jīng)過 funcA 參數(shù)被其他的對象持有了频鉴。 - funcA 沒有外部副作用.
外部副作用:
調(diào)用了一個 native 方法或修改了一個靜態(tài)對象等等. 這些操作副作用的范圍已經(jīng)超過 obj 范圍. - 參數(shù) a 在 funcA 沒有被修改。 或參數(shù) a 是一個可忽略的對象恋拍。
- obj 在 funcA 沒有被修改, 或 obj 是一個可忽略的對象垛孔。
修改:
對象的字段經(jīng)過 funcA 發(fā)生了變化。
可忽略對象:
對象賦值是可忽略的施敢。沒有成為有副作用方法的參數(shù). - 返回值 o 沒有成為有副作用方法的參數(shù).
- 返回值 o 沒有成為 func 的返回值
- 返回值 0 沒有被 thow 拋出.
ProGuard 對于 Library 中的方法做最壞的打算, 參數(shù)會發(fā)生逃逸周荐。方法有外部副作用。對象和參數(shù)會被修改僵娃。返回值是一個外部引用概作。不滿足條件 1,2,3,4. ProGuard 提供聲明
來修改它們的副作用影響范圍。 對于 Library 來說默怨, ProGuard 不會分析其內(nèi)部代碼指令讯榕。直接按照聲明確定他們的副作用影響。 對于程序中的方法先壕。會對方法內(nèi)部指令進(jìn)行分析計算副作用影響瘩扼。開發(fā)者可以根據(jù)需要使用聲明
修改它的副作用影響。
聲明方式如下:
聲明方式 | 內(nèi)部標(biāo)識 | 描述 |
---|---|---|
-assumenosideeffects | hasNoSideEffects ,hasNoExternalSideEffects hasNoEscapingParameters | 沒有外部影響, 沒有參數(shù)逃逸,沒有參數(shù)和對象被修改 |
-assumenoexternalsideeffects | hasNoExternalSideEffects hasNoEscapingParameters | 沒有外部影響,沒有參數(shù)逃逸,沒有參數(shù)被修改 |
-assumenoescapingparameters | hasNoEscapingParameters | 沒有參數(shù)逃逸 |
-assumenoexternalreturnvalues | hasNoExternalReturnValues | 返回值是參數(shù)或新對象 |
-assumenosideeffects
聲明:
被聲明的方法將滿足條件1,2,3,4. 當(dāng)返回值滿足條件 5,6,7, 那么該方法調(diào)用指令將被刪除.
assumenoexternalsideeffects
聲明:
被聲明的方法將滿足條件1,2,3垃僚。
-assumenoexternalreturnvalues
聲明
方法返回值有三種情況:
- 返回值的是入?yún)ⅰ?/li>
- 返回值一個新對象實例集绰。該對象在方法內(nèi)被實例化。
- 返回值是的外部引用谆棺。 一般為堆上某個引用的字段實例栽燕。
這三種情況罕袋, 第三種返回值是一個不可被忽略的對象。
assumenoexternalreturnvalues 聲明表示返回值是一個新對象實例或者參數(shù)碍岔。 是一個可以被忽略的對象浴讯, 后續(xù)中如果該返回值滿足 567 , 那么該對象為不可忽略的對象蔼啦。
通過 ProGuard 的例子榆纽,理解一下聲明的作用。
例子1
聲明
-assumenoexternalsideeffects class java.lang.StringBuilder {
public java.lang.StringBuilder();
public java.la·ng.StringBuilder append(java.lang.String);
}
方法塊1:
new StringBuilder().append("dd")
方法塊2:
new StringBuilder().append("dd").append("ddd");
結(jié)果:
方法塊1 被刪除
因為使用 assumenoexternalsideeffects 聲明了兩個方法 StringBuilder() 和 append() 方法捏肢。
new StringBuilder() 返回的是一個可忽略的對象奈籽。append() 滿足以上條件,所以調(diào)用也是一個沒有副作用的操作鸵赫。
方法塊2 被保留
因為在第二個 append 方法的時候衣屏, 調(diào)用者是由第一個 append 方法返回的一個外部引用。滿足123辩棒, 不滿足4狼忱,所以 append 方法調(diào)用存在副作用,第二個 append 方法被保留一睁。 ProGuard 向上回溯標(biāo)記相關(guān)參數(shù)對象和方法钻弄。最終 方法塊2 整體被保留了。
例子2
聲明
-assumenoexternalsideeffects class java.lang.StringBuilder {
public java.lang.StringBuilder();
public java.lang.StringBuilder append(java.lang.String);
}
-assumenoexternalreturnvalues class java.lang.StringBuilder {
public java.lang.StringBuilder append(java.lang.String);
}
結(jié)果:
方法塊2 被刪除
assumenoexternalreturnvalues
將 append 返回值聲明為非外部引用卖局。將滿足條件1234567斧蜕。調(diào)用不存在副作用。
例子3
配置
-assumenosideeffects class java.lang.StringBuilder {
public java.lang.StringBuilder();
public java.lang.StringBuilder append(java.lang.String);
}
結(jié)果:
方法塊2 被刪除
assumenosideeffects
聲明屏蔽了 StringBuilder 自身的修改砚偶。 滿足了條件4,同時滿足條件 123567洒闸。 調(diào)用不存在副作用染坯。
5.3 優(yōu)化副作用
- 反編譯問題: 優(yōu)化中會使用上 pop,pop2 丘逸,swap单鹿,等指令, 這個將會導(dǎo)致反編譯不能編譯出相對應(yīng)語義的 代碼深纲。
- 定位問題:部分優(yōu)化帶來行號偏移的問題和 SourceFile 丟失仲锄。
注: 以上總結(jié)均基于 ProGuard 6.0.3 的源碼。省略了部分條件和情況, 因為太過于復(fù)雜.以及描述不清
5.4 總結(jié)
Optimize 階段是 ProGuard 幾個階段中著墨最多的湃鹊, 代碼量也是最多最為復(fù)雜的儒喊。 整體耗時也是最長, 即使其他幾個階段的耗時加起來也比不上 Optimize 的耗時的一半币呵, 但這階段卻也是最容易被忽略的階段怀愧。
6. Obfuscate
6.1 Obfuscate 處理過程
將類,字段,方法的名稱簡化成短名字, 簡化需要依據(jù) java 的規(guī)范, 方法名應(yīng)符合定義沒有非法字符. 虛方法在 class 繼承中方法名稱保持一致. 同個范圍內(nèi)字段或方法描述符,簽名相同的時候名稱唯一, 相同包下 class 名稱唯一. 從 library 中繼承的方法名稱不變等等。
6.2 Obfuscate 參數(shù)
-applymapping
應(yīng)用映射規(guī)則。
-useuniqueclassmembernames
混淆時候為類成員生成全局唯一的名稱芯义。
相同的 字段描述符 的字段 擁有全局唯一的名稱哈垢。
相同的 方法描述符 的方法 擁有全局唯一的名稱。
-overloadaggressively
該選項是一個更為激進(jìn)的選項扛拨, 他允許在同一個類中耘分,一個不同類型的字段擁有相同的名字。相同入?yún)⒉煌祷仡愋蛽碛邢嗤Q绑警。 這個選項可以讓 class 的大小更小陶贼。但是對于反編譯是一個災(zāi)難。
-keepparameternames
在保留本地變量表基礎(chǔ)上待秃。 只保留參數(shù)的變量表拜秧。
-repackageclasses
x
-defaultpackage
x
將混淆的類的包名替換為x。 加大逆向分析的成本
-flattenpackagehierarchy
x
將混淆的類的包名以x 為前綴扁平化章郁。 加大逆向分析的成本枉氮。
-packageobfuscationdictionary
混淆包名字典
-classobfuscationdictionary
混淆類和成員字典
-renamesourcefileattribute
x
SourceFile 屬性值重置為 x
7. Preverify
對 java code 進(jìn)行預(yù)校驗。 主要校驗 StackMap /StackMapTable 屬性暖庄。android 虛擬機(jī)字節(jié)碼校驗不基于StackMap /StackMapTable聊替。
0x02 ProGuard 在 Android 上運(yùn)用:
1. ProGuard Rule
Android 開啟 ProGuard
release {
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
- minifyEnabled: 開啟代碼收斂, 默認(rèn)使用 ProGuard 方式培廓。
- proguardFiles:定義 ProGuard rule惹悄。
ProGuard rules 的來源主要分為 4 類:
- 預(yù)置 rules:默認(rèn)有三種 proguard-android.txt, proguard-android-optimize.txt,proguard-defaults.txt, 在 Gradle 在編譯的時候通過任務(wù)
extractProguardFiles
將預(yù)置在依賴com.android.tools.build:gradle-core
java resource 的 rules 解壓到根項目 build/intermediates/proguard-files 文件下肩钠。
proguard-android.txt
泣港。 該項關(guān)閉了 Optimize。如果想開啟Optimize 可以引用proguard-android-optimize.txt
或者不使用預(yù)置的 rules 价匠。 - project rules:定義在主工程的 rules
- aar rules:每個 library 攜帶關(guān)于自身的一份 rules当纱。
- aapt_rules:aapt 在為資源時候生成。
2. 應(yīng)用
2.1 R 文件內(nèi)聯(lián):
Android 中 R 文件是標(biāo)識資源 ID踩窖, Resource 可以根據(jù)標(biāo)識資源 ID查找對應(yīng)的資源坡氯。 R 文件分為兩種,
- 主工程的 R 文件
字段使用 static final 修飾洋腮。javac 編譯的時候箫柳,將源碼中的 id 引用替換成對應(yīng)資源常量。 - Library的 R 文件
Library 生成 aar 的時候啥供。資源的 id 并不確7定悯恍。 同時避免 javac 做類似主工程的優(yōu)化界逛。R 文件是 static 非 final 诉濒。 R 文件也不會一起打包到aar 中蒸健。
我們可以通過刪除 R 文件來減小包大小。 主工程的 R 文件可以直接刪除元媚。 對于Library 中的 R 文件需要先內(nèi)聯(lián)禁添。然后再刪除熟丸。
方案:
- 通過自定義 Android Gradle Transform Api 來實現(xiàn)刺洒。內(nèi)聯(lián)和刪除 R 文件。
- 使用 ProGuard 來做內(nèi)聯(lián)和刪除的優(yōu)化篙梢。通過優(yōu)化項
field/propagation/value
來實現(xiàn)顷帖。 ProGuard 這獲取是一個更為優(yōu)雅的選擇。代價是Optimize
的耗時渤滞。
2.2 API 檢查
在上次文章 Gradle Configuration 分析的中可以發(fā)現(xiàn) Gradle 對依賴版本的判斷是不可靠的贬墩。我們需要在最后階段進(jìn)行 API 檢查。 防止出現(xiàn) NoSuchMethodException, NoSuchFieldException,ClassNotFoundException 等異常妄呕。
方案一
結(jié)合 -dontwarn 參數(shù)陶舞,記錄 Initialize 階段連接中出現(xiàn)缺失的類和字段或者方法。但是 Initialize 的時候绪励。程序的 ClassPool 的部分類和方法會在 Shrink 階段被刪除肿孵。 對于它們的檢查是多余的。他們的錯誤也是可以被忽略的.方案二
Shrink 階段后疏魏。重新連接 ClassPool 停做。 記錄其中的缺失的類和字段或者方法。相對于方案一, 方案二需要基于ProGuard 源碼進(jìn)行擴(kuò)展大莫。
2.3 瘦身
ProGuard 應(yīng)該是 APK 瘦身第一大利器蛉腌。主要是在四方面。
- 類和方法只厘,字段的刪除烙丛。(Shirk)
- 字節(jié)碼的優(yōu)化。(Optimize)
- 字節(jié)碼 中 Attributes 屬性的刪除懈凹。(Obfuscate)
- 名稱的簡化蜀变。(Obfuscate)
ProGuard 是在 rule 規(guī)則上做優(yōu)化。 rule 的范圍越窄介评,那么優(yōu)化的效果就越明顯。我們盡可能的優(yōu)化 rule 來達(dá)到最大化的優(yōu)化的結(jié)果爬舰。除了在定義的時候特別注意范圍们陆。 同時可以優(yōu)化 aapt_rule 來做更為極致的優(yōu)化。aapt_rule 是由 aapt 工具在生成 arsc 資源時候生成 rule情屹。 該 rule 是一個較為保守的方案坪仇。 它涵蓋了所有 資源中可能出現(xiàn)的情況。 因為有些資源是在代碼中永遠(yuǎn)不會被使用到垃你。所以根據(jù)沒有用到的資源生成的 rule 也是一個冗余的 rule 椅文。通過以下情況了解一下具體情況喂很。
情況1:只有 app 代碼。 通過 ProGuard 之后 jar 的大小 3 KB
情況2:有 app 代碼皆刺,引入了
appcompat-v7:28.0.0
依賴少辣。 但是沒有使用 v7 的代碼或者資源, 通過 ProGuard 之后 jar 大小為 612 KB羡蛾。情況3:有 app 代碼和
appcompat-v7:28.0.0
依賴漓帅,沒有使用 v7 的代碼或資源。 收斂了 aapt_rules 痴怨。 ProGuard 之后 jar 大小為 29 KB忙干。之所以沒有辦法達(dá)到情況1 中 3 KB原因在于引入了 v7 的同時引入了 v7 的 aar_rules.
注: aapt_rules 收斂以后瘦身的效果還受到其他因素的影響。
0x03 ProGuard rule 優(yōu)化建議
- 盡可能使用 keepnames 替代 keep
- 不使用 -ignorewarnings
- rule 范圍盡可能小
- 使用 Optimize 浪藻, 但避免出現(xiàn)行號偏移捐迫。
- 反射使用遵循模板。
- aar 攜帶自身的rules
- 使用注解 keep
- 使用 -overloadaggressively 提高瘦身效果
- 使用 -skipnonpubliclibraryclasses 加快混淆速度
- 四大組件和 View 交給 aapt 生成爱葵。
- 去除多余的 Attributes(RuntimeInvisibleAnnotations施戴,LocalVariableTypeTable...)
0x04 尾巴
我們往往使用 混淆 來代表 ProGuard, 這有失偏頗钧惧。 混淆只是 ProGuard 的其中一功能暇韧。遠(yuǎn)遠(yuǎn)不能來代表 ProGuard ∨ǖ桑總體來說 ProGuard 是一個特別優(yōu)秀的框架懈玻。擁有完整的 Java 1-10 字節(jié)碼解析。完整的字節(jié)碼操作模擬乾颁。但是較為復(fù)雜的 Optimize 代碼還不穩(wěn)定涂乌。耗時較長。部分優(yōu)化實現(xiàn)相對保留英岭。通過對 ProGuard 的理解和學(xué)習(xí)會對于以往使用運(yùn)氣編程情況有所改善湾盒。
0x05 其他
-whyareyoukeeping
: 可以通過該選項在 Debug 的時候。 定位類被保留的原因诅妹。 正常情況下不建議開啟罚勾。會延長 ProGuard 時長。
-printconfiguration
: 聚合 ProGuard 的所有rules 輸出到具體文件上吭狡。
-addconfigurationdebugging
: 有效的定位反射導(dǎo)致的問題尖殃。
推薦保留屬性:
-keepattributes LineNumberTable,Signature,RuntimeVisibleAnnotations,RuntimeVisibleParameterAnnotations,AnnotationDefault
推薦保留優(yōu)化:
-optimizations field/propagation/value
-optimizations method/removal/parameter
-optimizations method/propagation/parameter
-optimizations method/propagation/returnvalue
-optimizations code/simplification/variable
-optimizations code/simplification/field
-optimizations code/simplification/branch
-optimizations code/simplification/object
-optimizations code/simplification/string
-optimizations code/simplification/math
-optimizations code/simplification/advanced
-optimizations code/removal/advanced
-optimizations code/removal/simple
-optimizations code/removal/variable
-optimizations code/removal/exception
-optimizations code/allocation/variable