混淆是 Android 打包過(guò)程中最重要的流程之一,基本上所有 app 都應(yīng)該開(kāi)啟混淆,增加app的安全性?;煜鋵?shí)是包括了代碼壓縮、代碼混淆以及資源壓縮等的優(yōu)化過(guò)程。谷歌官方文檔Shrink Your Code and Resource,依靠 ProGuard,混淆流程將主項(xiàng)目以及依賴(lài)庫(kù)中未被使用的類(lèi)、類(lèi)成員、方法、屬性移除,這有助于規(guī)避64K方法數(shù)的瓶頸;同時(shí),將類(lèi)、類(lèi)成員、方法重命名為無(wú)意義的簡(jiǎn)短名稱(chēng),增加了逆向工程的難度。而依靠 Gradle 的 Android 插件,我們將移除未被使用的資源,可以有效減小 apk 安裝包大小。
配置方法
在 app module 目錄中的 build.gradle文件中的 android 節(jié)點(diǎn)下配置如下:
buildTypes {
release {
shrinkResources true
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
因?yàn)榛煜龝?huì)大大減慢編譯的速度,所以 debug 模式下不應(yīng)該開(kāi)啟混淆,minifyEnabled 設(shè)置為 true 表示開(kāi)啟混淆,shrinkResources 設(shè)置為 true 表示開(kāi)啟壓縮資源,proguardFiles 表示指定給項(xiàng)目的 ProGuard 文件,默認(rèn)情況下是指定了兩個(gè) Proguard rules 文件,一個(gè)是 Android 系統(tǒng)自帶的 proguard-android.txt,另外一個(gè)就是在 app module目錄下的 proguard-rules.pro,proguard-android.txt 我們可以在 Android SDK 的目錄中搜索即可找到該文件,完整路徑是 SDK 目錄下的 <sdk-root>/tools/proguard/。我們先看看 proguard-rules.pro 這個(gè)文件,它是留給開(kāi)發(fā)者來(lái)自定義混淆規(guī)則的,一般開(kāi)發(fā)者的混淆都寫(xiě)在這個(gè)文件中。
常見(jiàn)的混淆命令及含義
- optimizationpasses 指定壓縮級(jí)別,默認(rèn)是5
- dontoptimize 混淆時(shí)不優(yōu)化類(lèi)文件
- dontusemixedcaseclassnames 不使用大小寫(xiě)混合類(lèi)名
- dontskipnonpubliclibraryclasses 混淆jars中的非public classes
- dontpreverify 混淆時(shí)不做預(yù)校驗(yàn)
- dontwarn 不提示警告,避免打包時(shí)某些警告出現(xiàn)
- ignorewarnings 忽略警告,避免打包時(shí)某些警告出現(xiàn)
- verbose 混淆時(shí)記錄日志
- assumenosideeffects 優(yōu)化時(shí)允許訪問(wèn)并修改類(lèi)和成員的訪問(wèn)修飾符,可能作用域會(huì)變大。
- repackageclasses 可以把你的代碼以及所使用到的各種第三方庫(kù)代碼統(tǒng)統(tǒng)移動(dòng)到同一個(gè)包下。
- obfuscationdictionary 后面加一個(gè)純文本文件路徑,它的作用是指定一個(gè)字典文件作為混淆字典。默認(rèn)情況下我們的代碼命名會(huì)被混淆成 abcdefg... 字母組合的內(nèi)容,需要修改可以使用這個(gè)配置項(xiàng)將字典修改成亂碼或中文內(nèi)容。
- keep 防止類(lèi)和成員被移除或重命名
- keepnames 防止類(lèi)和成員被重命名
- keepclassmembers 防止成員被移除或重命名
- keepclassmembernames 防止成員被重命名
- keepclasseswithmembers 防止擁有該成員的類(lèi)被移除或重命名
- keepclasseswithmembernames 防止擁有該成員的類(lèi)被重命名
- keepattributes 當(dāng)“Annotation、Exceptions, Signature, Deprecated, SourceFile, SourceDir, LineNumberTable,InnerClasses”這些東西可能被移除時(shí)如果想保留,就使用該屬性keep住
混淆的格式一般是下面這兩種:
[混淆命令] [類(lèi)]
和
[混淆命令] [類(lèi)] {
[成員];
}
其中"類(lèi)"是代表類(lèi)相關(guān)的限制條件,它最終會(huì)指定到某些符合限定條件的類(lèi),它可以是下列內(nèi)容:
- 具體的類(lèi)
- 類(lèi)訪問(wèn)修飾符(public private protected)
- 通配符*,匹配任意長(zhǎng)度字符,但不含包名分隔符(.)
- 通配符**,匹配任意長(zhǎng)度字符,并且包含包名分隔符(.)
- extends,即可以指定類(lèi)的基類(lèi)
- implement,匹配實(shí)現(xiàn)了某接口的類(lèi)
- $,內(nèi)部類(lèi)
"成員"代表類(lèi)成員相關(guān)的限定條件,它將最終定位到某些符合該限定條件的類(lèi)成員。它的內(nèi)容可以使用:
- <init> 匹配所有構(gòu)造器
- <fields> 匹配所有域
- <methods> 匹配所有方法
- 通配符*,匹配任意長(zhǎng)度字符,但不含包名分隔符(.)
- 通配符**,匹配任意長(zhǎng)度字符,并且包含包名分隔符(.)
- 通配符***,匹配任意參數(shù)類(lèi)型
- ...,匹配任意長(zhǎng)度的任意類(lèi)型參數(shù)。比如void test(…)就能匹配任意 void test(String a) 或者是 void test(int a, String b) 這些方法。
- 訪問(wèn)修飾符(public、protected、private)
常用的自定義混淆規(guī)則
- 不混淆某個(gè)類(lèi)
-keep public class com.shenhuniurou.DemoActivity { *; }
- 不混淆某個(gè)包所有的類(lèi)
-keep class com.shenhuniurou.demo.** { *; }
- 不混淆某個(gè)類(lèi)的子類(lèi)
-keep public class * extends com.shenhuniurou.activity.BaseActivity { *; }
- 不混淆所有類(lèi)名中包含了“model”的類(lèi)及其成員
-keep public class **.*model*.** {*;}
- 不混淆某個(gè)接口的實(shí)現(xiàn)
-keep class * implements com.shenhuniurou.demo.DemoInterface { *; }
- 不混淆某個(gè)類(lèi)的構(gòu)造方法
-keepclassmembers class com.shenhuniurou.demo.DemoClass {
public <init>();
}
- 不混淆某個(gè)類(lèi)的特定的方法
-keepclassmembers class com.shenhuniurou.demo.DemoClass {
public void test(java.lang.String);
}
下面我們?cè)倏纯碨DK自帶的默認(rèn)混淆文件中定義了哪些混淆規(guī)則
# 不使用大小寫(xiě)混合類(lèi)名
-dontusemixedcaseclassnames
# 混淆jars中的非public classes
-dontskipnonpubliclibraryclasses
# 混淆時(shí)記錄日志
-verbose
# 混淆時(shí)不優(yōu)化類(lèi)文件
-dontoptimize
# 混淆時(shí)進(jìn)行預(yù)校驗(yàn)
-dontpreverify
# 不混淆注解
-keepattributes *Annotation*
-keep public class com.google.vending.licensing.ILicensingService
-keep public class com.android.vending.licensing.ILicensingService
# 保持 native 方法不被混淆
-keepclasseswithmembernames class * {
native <methods>;
}
# 保持自定義View的getter/setter不被混淆
-keepclassmembers public class * extends android.view.View {
void set*(***);
*** get*();
}
# 保持Activity和其子類(lèi)的View作為參數(shù)的方法不被混淆
-keepclassmembers class * extends android.app.Activity {
public void *(android.view.View);
}
# 保持枚舉不被混淆
-keepclassmembers enum * {
public static **[] values();
public static ** valueOf(java.lang.String);
}
# 保持通過(guò)Parcelable序列化的類(lèi)不被混淆
-keepclassmembers class * implements android.os.Parcelable {
public static final android.os.Parcelable$Creator CREATOR;
}
# 保持R文件不被混淆
-keepclassmembers class **.R$* {
public static <fields>;
}
-dontwarn android.support.**
# Keep注解的相關(guān)的類(lèi)和方法字段以及構(gòu)造方法
-keep class android.support.annotation.Keep
-keep @android.support.annotation.Keep class * {*;}
-keepclasseswithmembers class * {
@android.support.annotation.Keep <methods>;
}
-keepclasseswithmembers class * {
@android.support.annotation.Keep <fields>;
}
-keepclasseswithmembers class * {
@android.support.annotation.Keep <init>(...);
}
有了這些默認(rèn)的混淆規(guī)則之后,在開(kāi)發(fā)者自定義的混淆文件中就不需要再寫(xiě)了,開(kāi)發(fā)者一般需要關(guān)注的混淆規(guī)則是引用了第三方庫(kù)后或者使用了jni的類(lèi)、webview中調(diào)用了js方法,這些都需要開(kāi)發(fā)者來(lái)定義混淆規(guī)則。第三方庫(kù)的接入文檔一般都已經(jīng)寫(xiě)好了混淆規(guī)則,開(kāi)發(fā)者只需要拷貝至proguard-rules.pro文件中即可。
完成混淆后,混淆過(guò)的包必須進(jìn)行檢查,避免因混淆引入的bug。
在使用上面的配置進(jìn)行混淆打包后在 <module-name>/build/outputs/mapping/release/ 目錄下會(huì)輸出以下文件:
- dump.txt 描述APK文件中所有類(lèi)的內(nèi)部結(jié)構(gòu)
- mapping.txt 提供混淆前后類(lèi)、方法、類(lèi)成員等的對(duì)照表
- seeds.txt 列出沒(méi)有被混淆的類(lèi)和成員
- usage.txt 列出被移除的代碼
如果有使用渠道打包的話,那么輸出目錄是 <module-name>/build/outputs/mapping/channel/release/。我們可以根據(jù) seeds.txt 文件檢查未被混淆的類(lèi)和成員中是否已包含所有期望保留的,再根據(jù) usage.txt 文件查看是否有被誤移除的代碼。另外還需要從測(cè)試方面檢查。將混淆過(guò)的包進(jìn)行全方面測(cè)試,檢查是否有 bug 產(chǎn)生。
混淆資源文件
一般使用上述兩個(gè)混淆文件的配置混淆出來(lái)的apk,只是混淆了代碼,而對(duì)于項(xiàng)目中的資源文件名卻沒(méi)有混淆的,所以一般別人反編譯你的apk后,是可以清楚的看到你的資源文件的(包括命名和內(nèi)容)。微信團(tuán)隊(duì)開(kāi)源了一個(gè)能混淆資源文件的庫(kù)叫AndResGuard,AndResGuard是一個(gè)幫助你縮小APK大小的工具,他的原理類(lèi)似Java Proguard,但是只針對(duì)資源。他會(huì)將原本冗長(zhǎng)的資源路徑變短,例如將res/drawable/wechat變?yōu)閞/d/a下面是微信apk的資源目錄結(jié)構(gòu):

解碼混淆過(guò)的堆疊追蹤
混淆后的類(lèi)、方法名等等難以閱讀,這固然會(huì)增加逆向工程的難度,但對(duì)追蹤線上 crash 也造成了阻礙。我們拿到 crash 的堆棧信息后會(huì)發(fā)現(xiàn)很難定位,這時(shí)需要將混淆反解。
在 <sdk-root>/tools/proguard/bin 路徑下有附帶的的反解工具(Window 系統(tǒng)為 proguardgui.bat,Mac 或 Linux 系統(tǒng)為 proguardgui.sh)。
雙擊運(yùn)行 proguardgui.bat 后,可以看到左側(cè)的一行菜單。點(diǎn)擊 ReTrace,選擇該混淆包對(duì)應(yīng)的 mapping 文件(混淆后在<module-name>/build/outputs/mapping/release/ 路徑下會(huì)生成 mapping.txt 文件,它的作用是提供混淆前后類(lèi)、方法、類(lèi)成員等的對(duì)照表),再將 crash 的 stack trace 黏貼進(jìn)輸入框中,點(diǎn)擊右下角的 ReTrace ,混淆后的堆棧信息就顯示出來(lái)了。如下圖:
