混淆想必大家都不陌生,android上用的這一套混淆規則和java混淆幾乎是一樣的。爲何需要混淆呢?簡單的說,就是將原本正常的項目文件,對其類,方法,字段,重新命名,a,b,c,d,e,f…之類的字母,達到混淆代碼的目的,這樣反編譯出來,結構亂糟糟的,給反編譯者製造一些代碼閱讀的麻煩。
ProGuard簡介
ProGuard是2002年由比利時程序員Eric Lafortune發佈的一款優秀的開源代碼優化、混淆工具,適用於Java和Android應用,目標是讓程序更小,運行更快,在Java界處於壟斷地位。主要分爲四個模塊:Shrinker(壓縮器)、Optimizer(優化器)、Obfuscator(混淆器)、Retrace(堆棧反混淆)。
- 壓縮(shrink)通過引用標記算法,移除未使用的類、方法、字段等
- 優化(optimize)優化字節碼,簡化代碼等操作
- 混淆(obfuscate)使用簡短的,無意義的名稱重命名類名,方法名,參數字段等
- 預校驗(perverify)爲class添加預校驗消息
Android啓用壓縮、混淆、優化
在 Android 中,我們平常所說的"混淆"其實有兩層意思:
- 是 Java 代碼的混淆
- 是資源的壓縮
其實這兩者之間並沒有什麼關聯,只不過習慣性地放在一起來使用。那麼,說了這麼多,Android平臺上到底該如何開啓混淆呢?
android {
buildTypes {
release {
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
以上就是開啓混淆的基本操作了,通過 minifyEnabled 設置爲 true 來開啓混淆。同時,可以設置 shrinkResources 爲 true 來開啓資源的壓縮。
不難看出,我們一般在打 release 包時才啓用混淆,因爲混淆會增加額外的編譯時間,所以不建議在 debug 模式下啓用。此外,需要注意的是:只有在啓用混淆的前提下開啓資源壓縮纔會有效!
以上代碼中的 proguard-android.txt 表示 Android 系統爲我們提供的默認混淆規則文件,而 proguard-rules.pro 則是我們想要自定義的混淆規則。
如果你不對proguard-rules.pro文件做定製化,默認是整個工程全開啓混淆的,但是由於一些三方代碼、反射、自定義view等,一旦這些都混淆了編譯器在運行時找不到具體的成員,從而會導致錯誤,所以我們也要忽略一些類或者成員的混淆。
Android混淆配置關鍵字
系統混淆配置
#混淆時不使用大小寫混合類名
-dontusemixedcaseclassnames
#不跳過library中的非public的類
-dontskipnonpubliclibraryclasses
#打印混淆的詳細信息
-verbose
#不進行優化,建議使用此選項
-dontoptimize
#不進行預校驗,Android不需要,可加快混淆速度
-dontpreverify
#忽略警告
-ignorewarnings
#指定代碼的壓縮級別
-optimizationpasses 5
Proguard關鍵字
如果你不確定你需要使用哪一個選項,那麼你應該儘量使用 -keep 。它會確保指定的類和成員在 壓縮階段(shrinking step)不會被刪除,並且在 混淆階段(obfuscation step)不會被混淆。
Proguard通配符
上邊的通配符沒有返回類型。僅僅通配符有一個參數列表。
字段和方法的名稱可以包含如下通配符:
? 匹配名稱中的任意單個字符
* 匹配名稱中的任意一部分
類型描述可以包含如下通配符:
% 匹配任意基本類型(‘boolean’,'int'以及其他,但是不包括‘void’).
? 匹配類名中的任意單個字符
* 匹配類名中的任意部分,但是不包含包分隔符 。
** 匹配類名中的任意部分,可以包含任意個數的包分隔符。
*** 匹配任意類型(基本類型或者非基本類型,數組或者非數組)
... 匹配任意個數任意類型的參數。
注意:? , * 以及 ** 通配符永遠都不會匹配基本類型。此外,僅僅 *** 通配符會匹配任意長度任意類型的數組。
哪些不應該進行混淆
我們在瞭解了混淆的基本命令之後,很多人應該還是一頭霧水:到底哪些內容該混淆?其實,我們在使用代碼混淆時,ProGuard 對我們項目中大部分代碼進行了混淆操作,爲了防止編譯時出錯,我們應該通過 keep 命令保留一些元素不被混淆。所以,我們只需要知道哪些元素不應該被混淆:
枚舉
項目中難免可能會用到枚舉類型,然而它不能參與到混淆當中去。原因是:枚舉類內部存在 values 方法,混淆後該方法會被重新命名,並拋出 NoSuchMethodException。慶幸的是,Android 系統默認的混淆規則中已經添加了對於枚舉類的處理,我們無需再去做額外工作。
被反射的元素
被反射使用的類、變量、方法、包名等不應該被混淆處理。原因在於:代碼混淆過程中,被反射使用的元素會被重命名,然而反射依舊是按照先前的名稱去尋找元素,所以會經常發生 NoSuchMethodException 和 NoSuchFiledException 問題。
實體類
實體類即我們常說的"數據類",當然經常伴隨着序列化與反序列化操作。很多人也應該都想到了,混淆是將原本有特定含義的"元素"轉變爲無意義的名稱,所以,經過混淆的"洗禮"之後,序列化之後的 value 對應的 key 已然變爲沒有意義的字段,這肯定是我們不希望的。同時,反序列化的過程創建對象從根本上來說還是藉助於反射,混淆之後 key 會被改變,所以也會違揹我們預期的效果。
四大組件
Android 中的四大組件同樣不應該被混淆。原因在於:
四大組件使用前都需要在 AndroidManifest.xml 文件中進行註冊聲明,然而混淆處理之後,四大組件的類名就會被篡改,實際使用的類與 manifest 中註冊的類並不匹配,故而出錯。其他應用程序訪問組件時可能會用到類的包名加類名,如果經過混淆,可能會無法找到對應組件或者產生異常。
JNI 調用的Java 方法
當 JNI 調用的 Java 方法被混淆後,方法名會變成無意義的名稱,這就與 C++ 中原本的 Java 方法名不匹配,因而會無法找到所調用的方法。
其他不應該被混淆的
自定義控件不需要被混淆
JavaScript 調用 Java 的方法不應混淆
Java 的 native 方法不應該被混淆
項目中引用的第三方庫也不建議混淆
混淆舉例
以下是一些混淆的舉例和說明
//不混淆某個類
-keep public class name.huihui.example.Test { *; }
//不混淆某個類的子類
-keep public class * extends name.huihui.example.Test { *; }
//不混淆所有類名中包含了“model”的類及其成員
-keep public class **.*model*.** {*;}
//不混淆某個接口的實現
-keep class * implements name.huihui.example.TestInterface { *; }
//不混淆某個類的構造方法
-keepclassmembers class name.huihui.example.Test {
public <init>();
}
//不混淆某個類的特定的方法
-keepclassmembers class name.huihui.example.Test {
public void test(java.lang.String);
}
//不混淆某個類的內部類
-keep class name.huihui.example.Test$* {*;}
//兩個常用的混淆命令,注意:
//一顆星表示只是保持該包下的類名,而子包下的類名還是會被混淆;
//兩顆星表示把本包和所含子包下的類名都保持;
-keep class com.suchengkeji.android.ui.*
-keep class com.suchengkeji.android.ui.**
//用以上方法保持類後,你會發現類名雖然未混淆,但裏面的具體方法和變量命名還是變了,
//如果既想保持類名,又想保持裏面的內容不被混淆,我們就需要以下方法了
//不混淆某個包所有的類
-keep class com.suchengkeji.android.bean.** { *; }
//不混淆某個具體類
-keep public class com.android.vending.licensing.ILicensingService
一個通用的混淆模板
這裏給大家提供一個通用的混淆模板
###########################################基本指令(基本不動)#######################################
#代碼混淆壓縮比,在0~7之間,默認爲5,一般不做修改
-optimizationpasses 5
#混合時不使用大小寫混合,混合後的類名爲小寫
-dontusemixedcaseclassnames
#指定不去忽略非公共庫的類
-dontskipnonpubliclibraryclasses
#指定不去忽略非公共庫的類的成員
-dontskipnonpubliclibraryclassmembers
#這句話能夠使我們的項目混淆後產生映射文件
#包含有類名->混淆後類名的映射關係
-verbose
#不做預校驗,preverify是proguard的四個步驟之一,Android不需要preverify,去掉這一步能夠加快混淆速度。
-dontpreverify
#保留Annotation不混淆
-keepattributes *Annotation*,InnerClasses
#保留泛型不混淆
-keepattributes Signature
#拋出異常時保留代碼行號
-keepattributes SourceFile,LineNumberTable
#指定混淆是採用的算法,後面的參數是一個過濾器 #這個過濾器是谷歌推薦的算法,一般不做更改
-optimizations !code/simplification/cast,!field/*,!class/merging/*
#########################################java部分###########################################
#native方法不被混淆
-keepclasseswithmembernames class * {
native <methods>;
}
#枚舉enum類不被混淆
-keepclassmembers enum * {
public static **[] values();
public static ** valueOf(java.lang.String);
}
#Serializable類不被混淆
-keep public class * implements java.io.Serializable {*;}
#########################################android部分####################################
#保留support下的所有類及其內部類
-keep class android.support.** {*;}
#基類不被混淆
-keep class * extends android.app.Activity
-keep class * extends android.app.Application
-keep class * extends android.app.Service
-keep class * extends android.content.BroadcastReceiver
-keep class * extends android.content.ContentProvider
-keep class * extends android.app.backup.BackupAgentHelper
-keep class * extends android.preference.Preference
#自定義控件類不被混淆
-keepclasseswithmembers class * {
public <init>(android.content.Context, android.util.AttributeSet);
}
-keepclasseswithmembers class * {
public <init>(android.content.Context, android.util.AttributeSet, int);
}
#表示不混淆任何一個View中的setXxx()和getXxx()方法,
#因爲屬性動畫需要有相應的setter和getter的方法實現,混淆了就無法工作了。
-keep class * extends android.view.View{
*** get*();
void set*(***);
<init>(...);
}
# 保留R下面的資源
#不混淆資源類下static的
-keepclassmembers class **.R$* {
public static <fields>;
}
#Parcelable類不被混淆
-keep class * implements android.os.Parcelable {
public static final android.os.Parcelable$Creator *;
}
#WebView相關不被混淆
-keepclassmembers class * extends android.webkit.WebView {*;}
-keepclassmembers class * extends android.webkit.WebViewClient {*;}
-keepclassmembers class * extends android.webkit.WebChromeClient {*;}
-keepclassmembers class * {
@android.webkit.JavascriptInterface <methods>;
}
########################################本項目混淆規則##########################################
#非本項目不進行混淆
-keep class !com.ehai.store.** {*;}
-dontwarn **
#########################################其他#####################################################
#
##避免混淆Bugly
#-dontwarn com.tencent.bugly.**
#-keep public class com.tencent.bugly.**{*;}
#
##極光推送
#-dontoptimize
#-dontpreverify
#
#-dontwarn cn.jpush.**
#-keep class cn.jpush.** { *; }
#-keep class * extends cn.jpush.android.helpers.JPushMessageReceiver { *; }
#
#-dontwarn cn.jiguang.**
#-keep class cn.jiguang.** { *; }
#
##微衆銀行OCR SDK 混淆
##webank-cloud-normal-proguard-rules.pro的規則已經被webank-cloud-ocr-proguard-rules.pro “include”了,不需要再添加
#-include webank-cloud-ocr-proguard-rules.pro
混淆後的堆棧跟蹤
代碼經過 ProGuard 混淆處理後,想要讀取 StackTrace(堆棧追蹤)信息就會變得很困難。由於方法名稱和類的名稱都經過混淆處理,即使程序發生崩潰問題,也很難定位問題所在。幸運的是,ProGuard 爲我們提供了補救的措施,在着手進行之前,我們先來看一下 ProGuard 每次構建後生成了哪些內容。
混淆輸出結果
混淆構建完成之後,會在<module-name>/build/outputs/mapping/./
目錄下生成以下文件:
- dump.txt
說明 APK 內所有類文件的內部結構。 - mapping.txt
提供混淆前後的內容對照表,內容主要包含類、方法和類的成員變量。 - seeds.txt
羅列出未進行混淆處理的類和成員。 - usage.txt
羅列出從 APK 中移除的代碼。
如何從堆棧中還原ProGuard混淆後的代碼
混淆後的代碼一旦發生崩潰,那麼所產生的日誌也是混淆的,調試起來很麻煩,所以此時有必要將其還原到原生態進行分析。
還原前
Caused by: java.lang.NullPointerException
at net.simplyadvanced.ltediscovery.be.u(Unknown Source)
at net.simplyadvanced.ltediscovery.at.v(Unknown Source)
at net.simplyadvanced.ltediscovery.at.d(Unknown Source)
at net.simplyadvanced.ltediscovery.av.onReceive(Unknown Source)
還原後
Caused by: java.lang.NullPointerException
at net.simplyadvanced.ltediscovery.UtilTelephony.boolean is800MhzNetwork()(Unknown Source)
at net.simplyadvanced.ltediscovery.ServiceDetectLte.void checkAndAlertUserIf800MhzConnected()(Unknown Source)
at net.simplyadvanced.ltediscovery.ServiceDetectLte.void startLocalBroadcastReceiver()(Unknown Source)
at net.simplyadvanced.ltediscovery.ServiceDetectLte$2.void onReceive(android.content.Context,android.content.Intent)(Unknown Source)
那麼如何還原呢?這裏提供兩種方式:GUI工具和命令行
GUI工具還原
- 打開/tools/proguard/bin/proguardgui.bat
- 選擇左邊欄的ReTrace選項
- 添加你的mapping文件和混淆過的堆棧信息
- 點擊ReTrace!
如圖:
命令行還原
- 需要你的ProGuard的mapping文件和你想要還原的堆棧信息(如stacktrace.txt)
- 最簡單的方法就是將這些文件拷貝到/tools/proguard/bin/目錄
- 運行以下命令
//Windows
retrace.bat -verbose mapping.txt stacktrace.txt > out.txt
//Mac\Linux
retrace.sh -verbose mapping.txt stacktrace.txt > out.txt
參考
- https://tech.meituan.com/2018/04/27/mt-proguard.html
- https://stuff.mit.edu/afs/sipb/project/android/sdk/android-sdk-linux/tools/proguard/docs/index.html#manual/introduction.html
- https://developer.android.com/studio/build/shrink-code.html