毫無疑問著隆,混淆是打包過程中最重要的流程之一哼蛆,在沒有特殊原因的情況下躏碳,所有 app 都應(yīng)該開啟混淆双揪。
首先箕别,這里說的的混淆其實(shí)是包括了代碼壓縮居兆、代碼混淆以及資源壓縮等的優(yōu)化過程搂橙。依靠 ProGuard往弓,混淆流程將主項(xiàng)目以及依賴庫(kù)中未被使用的類旦装、類成員页衙、方法、屬性移除,這有助于規(guī)避64K方法數(shù)的瓶頸店乐;同時(shí)艰躺,將類、類成員眨八、方法重命名為無意義的簡(jiǎn)短名稱腺兴,增加了逆向工程的難度。而依靠 Gradle 的 Android
插件廉侧,我們將移除未被使用的資源页响,可以有效減小 apk 安裝包大小。
本文由兩部分構(gòu)成伏穆,第一部分給出混淆的最佳實(shí)踐拘泞,力求讓零基礎(chǔ)的新手都可以直接使用混淆;第二部分會(huì)介紹一下混淆的整體枕扫、自定義混淆規(guī)則的語(yǔ)法與實(shí)踐陪腌、自定義資源保持的規(guī)則等。
一烟瞧、Android混淆最佳實(shí)踐
- 混淆配置
一般情況下诗鸭,app module 的
build.gradle
文件默認(rèn)會(huì)有如下結(jié)構(gòu):
android {
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
</br>
因?yàn)殚_啟混淆會(huì)使編譯時(shí)間變長(zhǎng),所以debug模式下不應(yīng)該開啟参滴。我們需要做的是:
將release下minifyEnabled的值改為true强岸,打開混淆;
加上shrinkResources true砾赔,打開資源壓縮蝌箍。
修改后文件內(nèi)容如下:
android {
buildTypes {
release {
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
- 自定義混淆規(guī)則
在
app module 下默認(rèn)生成了項(xiàng)目的自定義混淆規(guī)則文件
proguard-rules.pro,多方調(diào)研后暴心,一份適用于大部分項(xiàng)目的混淆規(guī)則最佳實(shí)踐如下:
指定壓縮級(jí)別
-optimizationpasses 5
不跳過非公共的庫(kù)的類成員
-dontskipnonpubliclibraryclassmembers
混淆時(shí)采用的算法
-optimizations !code/simplification/arithmetic,!field/,!class/merging/
把混淆類中的方法名也混淆了
-useuniqueclassmembernames
優(yōu)化時(shí)允許訪問并修改有修飾符的類和類的成員
-allowaccessmodification
將文件來源重命名為“SourceFile”字符串
-renamesourcefileattribute SourceFile
保留行號(hào)
-keepattributes SourceFile,LineNumberTable
保持所有實(shí)現(xiàn) Serializable 接口的類成員
-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();
}
Fragment不需要在AndroidManifest.xml中注冊(cè)妓盲,需要額外保護(hù)下
-keep public class * extends android.support.v4.app.Fragment
-keep public class * extends android.app.Fragment
保持測(cè)試相關(guān)的代碼
-dontnote junit.framework.**
-dontnote junit.runner.**
-dontwarn android.test.**
-dontwarn android.support.test.**
-dontwarn org.junit.**
真正通用的、需要添加的就是上面這些专普,除此之外悯衬,需要每個(gè)項(xiàng)目根據(jù)自身的需求添加一些混淆規(guī)則:
第三方庫(kù)所需的混淆規(guī)則。正規(guī)的第三方庫(kù)一般都會(huì)在接入文檔中寫好所需混淆規(guī)則檀夹,使用時(shí)注意添加筋粗。
在運(yùn)行時(shí)動(dòng)態(tài)改變的代碼,例如反射炸渡。比較典型的例子就是會(huì)與 json 相互轉(zhuǎn)換的實(shí)體類娜亿。假如項(xiàng)目命名規(guī)范要求實(shí)體類都要放在model包下的話,可以添加類似這樣的代碼把所有實(shí)體類都保持装龆隆:-keep public class .Model.** {*;}
JNI中調(diào)用的類暇唾。
WebView中JavaScript調(diào)用的方法
Layout布局使用的View構(gòu)造函數(shù)、android:onClick等。
- 檢查混淆結(jié)果
混淆過的包必須進(jìn)行檢查策州,避免因混淆引入的bug。
一方面宫仗,需要從代碼層面檢查够挂。使用上文的配置進(jìn)行混淆打包后在
<module-name>/build/outputs/mapping/release/
目錄下會(huì)輸出以下文件:
dump.txt
描述APK文件中所有類的內(nèi)部結(jié)構(gòu)
mapping.txt
提供混淆前后類、方法藕夫、類成員等的對(duì)照表
seeds.txt
列出沒有被混淆的類和成員
usage.txt
列出被移除的代碼
我們可以根據(jù)
seeds.txt
文件檢查未被混淆的類和成員中是否已包含所有期望保留的孽糖,再根據(jù) usage.txt
文件查看是否有被誤移除的代碼。
另一方面毅贮,需要從測(cè)試方面檢查办悟。將混淆過的包進(jìn)行全方面測(cè)試,檢查是否有 bug 產(chǎn)生滩褥。
- 解出混淆棧
混淆后的類病蛉、方法名等等難以閱讀,這固然會(huì)增加逆向工程的難度瑰煎,但對(duì)追蹤線上 crash 也造成了阻礙铺然。我們拿到 crash 的堆棧信息后會(huì)發(fā)現(xiàn)很難定位,這時(shí)需要將混淆反解酒甸。
在
<sdk-root>/tools/proguard/
路徑下有附帶的的反解工具(Window 系統(tǒng)為 proguardgui.bat魄健,Mac
或 Linux 系統(tǒng)為 proguardgui.sh)。
這里以 Window 平臺(tái)為例插勤。雙擊運(yùn)行
proguardgui.bat
后沽瘦,可以看到左側(cè)的一行菜單。點(diǎn)擊 ReTrace农尖,選擇該混淆包對(duì)應(yīng)的
mapping 文件(混淆后在 <module-name>/build/outputs/mapping/release/
路徑下會(huì)生成 mapping.txt
文件析恋,它的作用是提供混淆前后類、方法卤橄、類成員等的對(duì)照表)绿满,再將 crash 的 stack trace 黏貼進(jìn)輸入框中,點(diǎn)擊右下角的
ReTrace
窟扑,混淆后的堆棧信息就顯示出來了喇颁。
以上使用 GUI 程序進(jìn)行操作,另一種方式是利用該路徑下的
retrace
工具通過命令行進(jìn)行反解嚎货,命令是
retrace.bat|retrace.sh [-verbose] mapping.txt [<stacktrace_file>]
例如:
retrace.bat -verbose mapping.txt obfuscated_trace.txt
注意事項(xiàng):
- 所有在
AndroidManifest.xml
涉及到的類已經(jīng)自動(dòng)被保持橘霎,因此不用特意去添加這塊混淆規(guī)則。(很多老的混淆文件里會(huì)加殖属,現(xiàn)在已經(jīng)沒必要)
proguard-android.txt
已經(jīng)存在一些默認(rèn)混淆規(guī)則姐叁,沒必要在 proguard-rules.pro
重復(fù)添加,該文件具體規(guī)則見附錄1:
二、混淆簡(jiǎn)介
Android中的“混淆”可以分為兩部分外潜,一部分是 Java 代碼的優(yōu)化與混淆原环,依靠 proguard 混淆器來實(shí)現(xiàn);另一部分是資源壓縮处窥,將移除項(xiàng)目及依賴的庫(kù)中未被使用的資源(資源壓縮嚴(yán)格意義上跟混淆沒啥關(guān)系嘱吗,但一般我們都會(huì)放一起講)。
- 代碼壓縮
代碼混淆是包含了代碼壓縮滔驾、優(yōu)化谒麦、混淆等一系列行為的過程。如上圖所示哆致,混淆過程會(huì)有如下幾個(gè)功能:
壓縮绕德。移除無效的類、類成員摊阀、方法耻蛇、屬性等;
優(yōu)化驹溃。分析和優(yōu)化方法的二進(jìn)制代碼城丧;根據(jù)proguard-android-optimize.txt中的描述,優(yōu)化可能會(huì)造成一些潛在風(fēng)險(xiǎn)豌鹤,不能保證在所有版本的Dalvik上都正常運(yùn)行亡哄。
混淆。把類名布疙、屬性名蚊惯、方法名替換為簡(jiǎn)短且無意義的名稱;
預(yù)校驗(yàn)灵临。添加預(yù)校驗(yàn)信息截型。這個(gè)預(yù)校驗(yàn)是作用在Java平臺(tái)上的,Android平臺(tái)上不需要這項(xiàng)功能儒溉,去掉之后還可以加快混淆速度宦焦。
這四個(gè)流程默認(rèn)開啟。
在 Android 項(xiàng)目中我們可以選擇將“優(yōu)化”和“預(yù)校驗(yàn)”關(guān)閉顿涣,對(duì)應(yīng)命令是-dontoptimize波闹、-dontpreverify(當(dāng)然,默認(rèn)的
proguard-android.txt
文件已包含這兩條混淆命令涛碑,不需要開發(fā)者額外配置)精堕。
- 資源壓縮
資源壓縮將移除項(xiàng)目及依賴的庫(kù)中未被使用的資源,這在減少 apk 包體積上會(huì)有不錯(cuò)的效果蒲障,一般建議開啟歹篓。具體做法是在
build.grade
文件中瘫证,將 shrinkResources
屬性設(shè)置為 true。需要注意的是庄撮,只有在用minifyEnabled true開啟了代碼壓縮后背捌,資源壓縮才會(huì)生效。
資源壓縮包含了“合并資源”和“移除資源”兩個(gè)流程洞斯。
“合并資源”流程中载萌,名稱相同的資源被視為重復(fù)資源會(huì)被合并。需要注意的是巡扇,這一流程不受shrinkResources屬性控制,也無法被禁止垮衷,
gradle 必然會(huì)做這項(xiàng)工作厅翔,因?yàn)榧偃绮煌?xiàng)目中存在相同名稱的資源將導(dǎo)致錯(cuò)誤。gradle 在四處地方尋找重復(fù)資源:
src/main/res/
路徑
不同的構(gòu)建類型(debug搀突、release等等)
不同的構(gòu)建渠道
項(xiàng)目依賴的第三方庫(kù)
合并資源時(shí)按照如下優(yōu)先級(jí)順序:
依賴 -> main -> 渠道 -> 構(gòu)建類型
舉個(gè)例子刀闷,假如重復(fù)資源同時(shí)存在于main文件夾和不同渠道中,gradle
會(huì)選擇保留渠道中的資源仰迁。
同時(shí)甸昏,如果重復(fù)資源在同一層次出現(xiàn),比如src/main/res/
和 src/main/res/徐许,則
gradle 無法完成資源合并施蜜,這時(shí)會(huì)報(bào)資源合并錯(cuò)誤。
“移除資源”流程則見名知意雌隅,需要注意的是翻默,類似代碼,混淆資源移除也可以定義哪些資源需要被保留恰起,這點(diǎn)在下文給出修械。
三、自定義混淆規(guī)則
在上文“混淆配置”中有這樣一行代碼
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
這行代碼定義了混淆規(guī)則由兩部分構(gòu)成:位于 SDK 的
tools/proguard/
文件夾中的 proguard-android.txt
的內(nèi)容以及默認(rèn)放置于模塊根目錄的 proguard-rules.pro
的內(nèi)容检盼。前者是 SDK 提供的默認(rèn)混淆文件(內(nèi)容見附錄1)肯污,后者是開發(fā)者自定義混淆規(guī)則的地方。
- 常見混淆命令:
optimizationpasses
dontoptimize
dontusemixedcaseclassnames
dontskipnonpubliclibraryclasses
dontpreverify
dontwarn
verbose
optimizations
keep
keepnames
keepclassmembers
keepclassmembernames
keepclasseswithmembers
keepclasseswithmembernames
在第一部分 Android 混淆最佳實(shí)踐中已介紹部分需要使用到的混淆命令吨枉,這里不再贅述蹦渣,詳情請(qǐng)查閱官網(wǎng)。需要特別介紹的是與保持相關(guān)元素不參與混淆的規(guī)則相關(guān)的幾種命令:
命令 作用
-keep 防止類和成員被移除或者被重命名
-keepnames 防止類和成員被重命名
-keepclassmembers 防止成員被移除或者被重命名
-keepnames 防止成員被重命名
-keepclasseswithmembers 防止擁有該成員的類和成員被移除或者被重命名
-keepclasseswithmembernames 防止擁有該成員的類和成員被重命名
- 保持元素不參與混淆的規(guī)則
形如:
[保持命令] [類] {
[成員]
}
“類”代表類相關(guān)的限定條件东羹,它將最終定位到某些符合該限定條件的類剂桥。它的內(nèi)容可以使用:
具體的類
訪問修飾符(public、protected属提、private)
通配符*权逗,匹配任意長(zhǎng)度字符美尸,但不含包名分隔符(.)
通配符**,匹配任意長(zhǎng)度字符斟薇,并且包含包名分隔符(.)
extends师坎,即可以指定類的基類
implement,匹配實(shí)現(xiàn)了某接口的類
$堪滨,內(nèi)部類
“成員”代表類成員相關(guān)的限定條件胯陋,它將最終定位到某些符合該限定條件的類成員。它的內(nèi)容可以使用:
匹配所有構(gòu)造器
匹配所有域
匹配所有方法
通配符*袱箱,匹配任意長(zhǎng)度字符遏乔,但不含包名分隔符(.)
通配符**,匹配任意長(zhǎng)度字符发笔,并且包含包名分隔符(.)
通配符***盟萨,匹配任意參數(shù)類型
…,匹配任意長(zhǎng)度的任意類型參數(shù)了讨。比如void
test(…)就能匹配任意 void test(String a) 或者是
void test(int a, String b) 這些方法捻激。
訪問修飾符(public、protected前计、private)
舉個(gè)例子胞谭,假如需要將name.huihui.test包下所有繼承Activity的public類及其構(gòu)造函數(shù)都保持住,可以這樣寫:
-keep public class name.huihui.test.** extends Android.app.Activity {
<init>
}
- 常用的自定義混淆規(guī)則
不混淆某個(gè)類
-keep public class name.huihui.example.Test { *; }
不混淆某個(gè)包所有的類
-keep class name.huihui.test.** { *; }
不混淆某個(gè)類的子類
-keep public class * extends name.huihui.example.Test { *; }
不混淆所有類名中包含了“model”的類及其成員
-keep public class .model.** {*;}
不混淆某個(gè)接口的實(shí)現(xiàn)
-keep class * implements name.huihui.example.TestInterface { *; }
不混淆某個(gè)類的構(gòu)造方法
-keepclassmembers class name.huihui.example.Test {
public <init>();
}
不混淆某個(gè)類的特定的方法
-keepclassmembers class name.huihui.example.Test {
public void test(java.lang.String);
}
四男杈、自定義資源保持規(guī)則
- keep.xml
用shrinkResources true開啟資源壓縮后丈屹,所有未被使用的資源默認(rèn)被移除。假如你需要定義哪些資源必須被保留势就,在
res/raw/
路徑下創(chuàng)建一個(gè) xml 文件泉瞻,例如 keep.xml。
通過一些屬性的設(shè)置可以實(shí)現(xiàn)定義資源保持的需求苞冯,可配置的屬性有:
tools:keep
定義哪些資源需要被保留(資源之間用“,”隔開)
tools:discard
定義哪些資源需要被移除(資源之間用“,”隔開)
tools:shrinkMode
開啟嚴(yán)格模式
當(dāng)代碼中通過
Resources.getIdentifier()
用動(dòng)態(tài)的字符串來獲取并使用資源時(shí)袖牙,普通的資源引用檢查就可能會(huì)有問題。例如舅锄,如下代碼會(huì)導(dǎo)致所有以“img_”開頭的資源都被標(biāo)記為已使用鞭达。
String name = String.format("img_%1d", angle + 1);
res = getResources().getIdentifier(name, "drawable", getPackageName());
我們可以設(shè)置
tools:shrinkMode
為 strict
來開啟嚴(yán)格模式,使只有確實(shí)被使用的資源被保留皇忿。
以上就是自定義資源保持規(guī)則相關(guān)的配置畴蹭,舉個(gè)例子:
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"
tools:keep="@layout/l_used_c,@layout/l_used_a,@layout/l_used_b"
tools:discard="@layout/unused2"
tools:shrinkMode="strict"/>
- 移除替代資源
一些替代資源,例如多語(yǔ)言支持的
strings.xml鳍烁,多分辨率支持的
layout.xml
等叨襟,在我們不需要使用又不想刪除掉時(shí),可以使用資源壓縮將它們移除幔荒。
我們使用
resConfig
屬性來指定需要支持的屬性糊闽,例如
android {
defaultConfig {
...
resConfigs "en", "fr"
}
}
其他未顯式聲明的語(yǔ)言資源將被移除梳玫。
參考資料
Shrink Your Code and Resources
proguard
Android安全攻防戰(zhàn),反編譯與混淆技術(shù)完全解析(下)
Android混淆從入門到精通
Android代碼混淆之ProGuard
附錄
proguard-android.txt文件內(nèi)容
包名不混合大小寫
-dontusemixedcaseclassnames
不跳過非公共的庫(kù)的類
-dontskipnonpubliclibraryclasses
混淆時(shí)記錄日志
-verbose
關(guān)閉預(yù)校驗(yàn)
-dontpreverify
不優(yōu)化輸入的類文件
-dontoptimize
保護(hù)注解
-keepattributes Annotation
保持所有擁有本地方法的類名及本地方法名
-keepclasseswithmembernames class * {
native <methods>;
}
保持自定義View的get和set相關(guān)方法
-keepclassmembers public class * extends android.view.View {
void set(*);
*** get();
}
保持Activity中View及其子類入?yún)⒌姆椒?/h1>
-keepclassmembers class * extends android.app.Activity {
public void *(android.view.View);
}
枚舉
-keepclassmembers enum * {
**[] $VALUES;
public *;
}
Parcelable
-keepclassmembers class * implements android.os.Parcelable {
public static final android.os.Parcelable$Creator CREATOR;
}
R文件的靜態(tài)成員
-keepclassmembers class *.R$ {
public static <fields>;
}
-dontwarn android.support.**
keep相關(guān)注解
-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>(...);
}