AS混淆基本配置解釋及語法
部分轉自:http://blog.csdn.net/guolin_blog/article/details/50451259感謝大神
混淆APK
在Android Studio當中混淆APK實在是太簡單了,藉助SDK中自帶的Proguard工具,只需要修改build.gradle中的一行配置即可。可以看到,現在build.gradle中minifyEnabled的值是false,這裡我們只需要把值改成true,打出來的APK包就會是混淆過的了。如下所示:
<code class="language-groovy hljs bash has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;">release { minifyEnabled <span class="hljs-literal" style="color: rgb(0, 102, 102); box-sizing: border-box;">true</span> proguardFiles getDefaultProguardFile(<span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">'proguard-android.txt'</span>), <span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">'proguard-rules.pro'</span> }</code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li></ul>
其中minifyEnabled用於設定是否啟用混淆,proguardFiles用於選定混淆配置檔案。注意這裡是在release閉包內進行配置的,因此只有打出正式版的APK才會進行混淆,Debug版的APK是不會混淆的。當然這也是非常合理的,因為Debug版的APK檔案我們只會用來內部測試,不用擔心被人破解。
那麼現在我們來打一個正式版的APK檔案,在Android Studio導航欄中點選Build->Generate Signed APK,然後選擇簽名檔案並輸入密碼,如果沒有簽名檔案就建立一個,最終點選Finish完成打包,生成的APK檔案會自動存放在app目錄下。除此之外也可以在build.gradle檔案當中添加簽名檔案配置,然後通過gradlew assembleRelease來打出一個正式版的APK檔案,這種方式APK檔案會自動存放在
那麼現在已經得到了APK檔案,接下來就用上篇文章中學到的反編譯知識來對這個檔案進行反編譯吧,結果如下圖所示:
很明顯可以看出,我們的程式碼混淆功能已經生效了。
下面我們嘗試來閱讀一下這個混淆過後的程式碼,最頂層的包名結構主要分為三部分,第一個a.a已經被混淆的面目全非了,但是可以猜測出這個包下是LitePal的所有程式碼。第二個android.support可以猜測出是我們引用的android support庫的程式碼,第三個com.example.guolin.androidtest則很明顯就是我們專案的主包名了,下面將裡面所有的類一個個開啟看一下。
首先MainActivity中的程式碼如下所示:
可以看到,MainActivity的類名是沒有混淆的,onCreate()方法也沒有被混淆,但是我們定義的方法、全域性變數、區域性變數都被混淆了。
再來開啟下一個類NativeUtils,如下所示:
NativeUtils的類名沒有被混淆,其中宣告成native的方法也沒有被混淆,但是非native方法的方法名和區域性變數都被混淆了。
接下來是a類的程式碼,如下所示:
很明顯,這個是MainActivity中按鈕點選事件的匿名類,在onClick()方法中的呼叫程式碼雖然都被混淆了,但是呼叫順序是不會改變的,對照原始碼就可以看出哪一行是呼叫的什麼方法了。
再接下來是b類,程式碼如下所示:
雖然被混淆的很嚴重,但是我們還是可以看出這個是MyFragment類。其中所有的方法名、全域性變數、區域性變數都被混淆了。
最後再來看下c類,程式碼如下所示:
c類中只有一個a方法,從字串的內容我們可以看出,這個是Utils類中的methodNormal()方法。
我為什麼要建立這樣的一個專案呢?因為從這幾個類當中很能看出一些問題,接下來我們就分析一下上面的混淆結果。
首先像Utils這樣的普通類肯定是會被混淆的,不管是類名、方法名還是變數都不會放過。除了混淆之外Utils類還說明了一個問題,就是minifyEnabled會對資源進行壓縮,因為Utils類中我們明明定義了兩個方法,但是反編譯之後就只剩一個方法了,因為另外一個方法沒有被呼叫,所以認為是多餘的程式碼,在打包的時候就給移除掉了。不僅僅是程式碼,沒有被呼叫的資源同樣也會被移除掉,因此minifyEnabled除了混淆程式碼之外,還可以起到壓縮APK包的作用。
接著看一下MyFragment,這個類也是混淆的比較徹底的,基本沒有任何保留。那有些朋友可能會有疑問,Fragment怎麼說也算是系統元件吧,就算普通方法名被混淆了,至少像onCreateView()這樣的生命週期方法不應該被混淆吧?其實生命週期方法會不會被混淆和我們使用Fragment的方式有關,比如在本專案中,我使用的是android.support.v4.app.Fragment,support-v4包下的,就連Fragment的原始碼都被一起混淆了,因此生命週期方法當然也不例外了。但如果你使用的是android.app.Fragment,這就是呼叫手機系統中預編譯好的程式碼了,很明顯我們的混淆無法影響到系統內建的程式碼,因此這種情況下onCreateView()方法名就不會被混淆,但其它的方法以及變數仍然會被混淆。
接下來看一下MainActivity,同樣也是系統元件之一,但MainActivity的保留程度就比MyFragment好多了,至少像類名、生命週期方法名都沒有被混淆,這是為什麼呢?根據我親身測試得出結論,凡是需要在AndroidManifest.xml中去註冊的所有類的類名以及從父類重寫的方法名都自動不會被混淆(想改但是貌似沒有用,待研究一下)。因此,除了Activity之外,這份規則同樣也適用於Service、BroadcastReceiver和ContentProvider。
最後看一下NativeUtils類,這個類的類名也沒有被混淆,這是由於它有一個宣告成native的方法。只要一個類中有存在native方法,它的類名就不會被混淆,native方法的方法名也不會被混淆,因為C++程式碼要通過包名+類名+方法名來進行互動。 但是類中的別的程式碼還是會被混淆的。
除此之外,第三方的Jar包都是會被混淆的,LitePal(資料庫的開源框架)不管是包名還是類名還是方法名都被完完全全混淆掉了。
這些就是Android Studio打正式APK時預設的混淆規則。
那麼這些混淆規則是在哪裡定義的呢?其實就是剛才在build.gradle的release閉包下配置的proguard-android.txt檔案,這個檔案存放於<Android SDK>/tools/proguard目錄下,我們開啟來看一下:
# This is a configuration file for ProGuard.
# http://proguard.sourceforge.net/index.html#manual/usage.html
-dontusemixedcaseclassnames
-dontskipnonpubliclibraryclasses
-verbose
# Optimization is turned off by default. Dex does not like code run
# through the ProGuard optimize and preverify steps (and performs some
# of these optimizations on its own).
-dontoptimize
-dontpreverify
# Note that if you want to enable optimization, you cannot just
# include optimization flags in your own project configuration file;
# instead you will need to point to the
# "proguard-android-optimize.txt" file instead of this one from your
# project.properties file.
-keepattributes *Annotation*
-keep public class com.google.vending.licensing.ILicensingService
-keep public class com.android.vending.licensing.ILicensingService
# For native methods, see http://proguard.sourceforge.net/manual/examples.html#native
-keepclasseswithmembernames class * {
native <methods>;
}
# keep setters in Views so that animations can still work.
# see http://proguard.sourceforge.net/manual/examples.html#beans
-keepclassmembers public class * extends android.view.View {
void set*(***);
*** get*();
}
# We want to keep methods in Activity that could be used in the XML attribute onClick
-keepclassmembers class * extends android.app.Activity {
public void *(android.view.View);
}
# For enumeration classes, see http://proguard.sourceforge.net/manual/examples.html#enumerations
-keepclassmembers enum * {
public static **[] values();
public static ** valueOf(java.lang.String);
}
-keep class * implements android.os.Parcelable {
public static final android.os.Parcelable$Creator *;
}
-keepclassmembers class **.R$* {
public static <fields>;
}
# The support library contains references to newer platform versions.
# Don't warn about those in case this app is linking against an older
# platform version. We know about them, and they are safe.
-dontwarn android.support.**
這個就是預設的混淆配置檔案了,我們來一起逐行閱讀一下。 -dontusemixedcaseclassnames
表示混淆時不使用大小寫混合類名。 (在Java中大小寫指的不是同一個(例如class
A 跟 class a 不是同一個類),但是window下檔案命名A或a都是同一個)-dontskipnonpubliclibraryclasses
表示不跳過library中的非public的類。 -verbose
表示列印混淆的詳細資訊。 (用來生成Mapping.txt)-dontoptimize
表示不進行優化,建議使用此選項,因為根據proguard-android-optimize.txt中的描述,優化可能會造成一些潛在風險,不能保證在所有版本的Dalvik上都正常執行。 -dontpreverify
表示不進行預校驗。這個預校驗是作用在Java平臺上的,Android平臺上不需要這項功能,去掉之後還可以加快混淆速度。 -keepattributes *Annotation*
表示對註解中的引數進行保留。
-keep public class com.google.vending.licensing.ILicensingService
-keep public class com.android.vending.licensing.ILicensingService
表示不混淆上述宣告的兩個類,這兩個類我們基本也用不上,是接入Google原生的一些服務時使用的。
-keepclasseswithmembernames class * {
native <methods>;
}
表示不混淆任何包含native方法的類的類名以及native方法名,這個和我們剛才驗證的結果是一致的。(本地的類名不能混淆要不然調不了C程式碼,因為要跟C程式碼名字一樣)
-keepclassmembers public class * extends android.view.View {
void set*(***);
*** get*();
}
表示不混淆任何一個View中的setXxx()和getXxx()方法,因為屬性動畫需要有相應的setter和getter的方法實現,混淆了就無法工作了。
# We want to keep methods in Activity that could be used in the XML attribute onClick
-keepclassmembers class * extends android.app.Activity {
public void *(android.view.View);
}
表示不混淆Activity中引數是View的方法,因為有這樣一種用法,在XML中配置android:onClick=”buttonClick”屬性,當用戶點選該按鈕時就會呼叫Activity中的buttonClick(View view)方法,如果這個方法被混淆的話就找不到了。
-keepclassmembers enum * {
public static **[] values();
public static ** valueOf(java.lang.String);
}
表示不混淆列舉中的values()和valueOf()方法,列舉我用的非常少,這個就不評論了。
-keep class * implements android.os.Parcelable {
public static final android.os.Parcelable$Creator *;
}
表示不混淆Parcelable實現類中的CREATOR欄位,毫無疑問,CREATOR欄位是絕對不能改變的,包括大小寫都不能變,不然整個Parcelable工作機制都會失敗。
-keepclassmembers class **.R$* {
public static <fields>;
}
表示不混淆R檔案中的所有靜態欄位,我們都知道R檔案是通過欄位來記錄每個資源的id的,欄位名要是被混淆了,id也就找不著了。 -dontwarn android.support.**
表示對android.support包下的程式碼不警告,因為support包中有很多程式碼都是在高版本中使用的,如果我們的專案指定的版本比較低在打包時就會給予警告。不過support包中所有的程式碼都在版本相容性上做足了判斷,因此不用擔心程式碼會出問題,所以直接忽略警告就可以了。
好了,這就是proguard-android.txt檔案中所有預設的配置,而我們混淆程式碼也是按照這些配置的規則來進行混淆的。經過我上面的講解之後,相信大家對這些配置的內容基本都能理解了。不過proguard語法中還真有幾處非常難理解的地方,我自己也是研究了好久才搞明白,下面和大家分享一下這些難懂的語法部分。
proguard中一共有三組六個keep關鍵字,很多人搞不清楚它們的區別,這裡我們通過一個表格來直觀地看下:
關鍵字 | 描述 |
---|---|
keep | 保留類和類中的成員,防止它們被混淆或移除。 |
keepnames |
保留類和類中的成員,防止它們被混淆 ,但當成員沒有被引用時會被移除。 |
keepclassmembers | 只保留類中的成員,防止它們被混淆或移除。 |
keepclassmembernames |
只保留類中的成員,防止它們被混淆 ,但當成員沒有被引用時會被移除。 |
keepclasseswithmembers |
保留類和類中的成員,防止它們被混淆或移除 ,前提是指名的類中的成員必須存在, 如果不存在則還是會混淆。 |
keepclasseswithmembernames |
保留類和類中的成員,防止它們被混淆 ,但當成員沒有被引用時會被移除, 前提是指名的類中的成員必須存在, 如果不存在則還是會混淆。 |
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
TestClass testClass = new TestClass();
testClass.setSex("man");
}
}
(1)keep
<1>程式碼中有使用TestClass,使用了SetSex
<2>程式碼沒有使用TestClass
(2)keepnames
<1>程式碼中使用了TestClass
<2>程式碼中沒有使用testClass
(3)keepclassmembers
<1>程式碼中使用了TestClass
<2>程式碼中沒有使用testClass
(4)keepclassmembernames
<1>程式碼中使用了TestClass
<2>程式碼中沒有使用testClass
<1>程式碼中使用了TestClass
<2>程式碼中沒有使用testClass
(6)keepclasseswithmembernames
<1>程式碼中使用了TestClass
<2>程式碼中沒有使用testClass
除此之外,proguard中的萬用字元也比較讓人難懂,proguard-android.txt中就使用到了很多萬用字元,我們來看一下它們之間的區別:
通配 符 | 描述 |
---|---|
<field> | 匹配類中的所有欄位 |
<method> | 匹配類中的所有方法 |
<init> | 匹配類中的所有建構函式 |
* |
匹配任意長度字元,但不含包名分隔符(.)。 比如說我們的完整類名是com.example.test.MyActivity ,使用com.*,或者com.exmaple.*都是無法匹配的, 因為*無法匹配包名中的分隔符, 正確的匹配方式是com.exmaple.*.*, 或者com.exmaple.test.*,這些都是可以的。 但如果你不寫任何其它內容,只有一個*, 那就表示匹配所有的東西。 |
** |
匹配任意長度字元,並且包含包名分隔符(.) 。比如proguard-android.txt中使用的 -dontwarn android.support.** 就可以匹配android.support包下的所有內容, 包括任意長度的子包。 |
*** |
匹配任意引數型別。比如void set*(***) 就能匹配任意傳入的引數型別 ,*** get*()就能匹配任意返回值的型別。 |
… |
匹配任意長度的任意型別引數 。比如void test(…)就能匹配任意void test(String a) 或者是void test(int a, String b)這些方法。 |
雖說上面表格已經解釋的很詳細了,但是很多人對於keep和keepclasseswithmembers這兩個關鍵字的區別還是搞不懂。確實,它們之間用法有點太像了,我做了很多次試驗它們的結果都是相同的。其實唯一的區別就在於類中宣告的成員存不存在,我們還是通過一個例子來直接地看一下,先看keepclasseswithmember關鍵字:
-keepclasseswithmember class * {
native <methods>;
}
這段程式碼的意思其實很明顯,就是保留所有含有native方法的類的類名和native方法名,而如果某個類中沒有含有native方法,那就還是會被混淆。
也就是說只在有native方法的時候,類的類名及native方法名才會被保留,否則還是會被混淆
但是如果改成keep關鍵字,結果會完全不一樣:
-keep class * {
native <methods>;
}
使用keep關鍵字後,你會發現程式碼中所有類的類名都不會被混淆了,因為keep關鍵字看到class *就認為應該將所有類名進行保留,而不會關心該類中是否含有native方法。當然這樣寫只會保證類名不會被混淆,類中的成員還是會被混淆的。
也就是說有沒有沒有native方法,類名都會被保留,類中???
比較難懂的用法大概就這些吧,掌握了這些內容之後我們就能繼續前進了。
回到Android Studio專案當中,剛才打出的APK雖然已經成功混淆了,但是混淆的規則都是按照proguard-android.txt中預設的規則來的,當然我們也可以修改proguard-android.txt中的規則,但是直接在proguard-android.txt中修改會對我們本機上所有專案的混淆規則都生效,那麼有沒有什麼辦法只針對當前專案的混淆規則做修改呢?當然是有辦法的了,你會發現任何一個Android Studio專案在app模組目錄下都有一個proguard-rules.pro檔案,這個檔案就是用於讓我們編寫只適用於當前專案的混淆規則的,那麼接下來我們就利用剛才學到的所有知識來對混淆規則做修改吧。
這裡我們先列出來要實現的目標:
- 對MyFragment類進行完全保留,不混淆其類名、方法名、以及變數名。
- 對Utils類中的未呼叫方法進行保留,防止其被移除掉。
- 對第三方庫進行保留,不混淆android-support庫,以及LitePal庫中的程式碼。
下面我們就來逐一實現這些目標。
首先要對MyFragment類進行完全保留可以使用keep關鍵字,keep後宣告完整的類名,然後保留類中的所有內容可以使用*萬用字元實現,如下所示:
-keep class com.example.guolin.androidtest.MyFragment {
*;
}
然後保留Utils類中的未呼叫方法可以使用keepclassmembers關鍵字,後跟Utils完整類名,然後在內部宣告未呼叫的方法,如下所示:
-keepclassmembers class com.example.guolin.androidtest.Utils {
public void methodUnused();
}
最後不要混淆第三方庫,目前我們使用了兩種方式來引入第三方庫,一種是通過本地jar包引入的,一種是通過remote引入的,其實這兩種方式沒什麼區別,要保留程式碼都可以使用**這種萬用字元來實現,如下所示:
-keep class org.litepal.** {
*;
}
-keep class android.support.** {
*;
}
所有內容都在這裡了,現在我們重新打一個正式版的APK檔案,然後再反編譯看看效果:
可以看到,現在android-support包中所有程式碼都被保留下來了,不管是包名、類名、還是方法名都沒有被混淆。LitePal中的程式碼也是同樣的情況:
再來看下MyFragment中的程式碼,如下所示:
可以看到,MyFragment中的程式碼也沒有被混淆,按照我們的要求被完全保留下來了。
最後再來看一下Utils類中的程式碼:
很明顯,Utils類並沒有被完全保留下來,類名還是被混淆了,methodNormal()方法也被混淆了,但是methodUnused()沒有被混淆,當然也沒有被移除,因為我們的混淆配置生效了。
經過這些例子的演示,相信大家已經對Proguard的用法有了相當不錯的理解了,那麼根據自己的業務需求來去編寫混淆配置相信也不是什麼難事了吧?
Progaurd的使用非常靈活,基本上能夠覆蓋你所能想到的所有業務邏輯。這裡再舉個例子,之前一直有人問我使用LitePal時的混淆配置怎麼寫,其實真的很簡單,LitePal作為開源庫並不需要混淆,上面的配置已經演示瞭如何不混淆LitePal程式碼,然後所有程式碼中的Model是需要進行反射的,也不能混淆,那麼只需要這樣寫就行了:
-keep class * extends org.litepal.crud.DataSupport {
*;
}
因為LitePal中所有的Model都是應該繼承DataSupport類的,所以這裡我們將所有繼承自DataSupport的類都進行保留就可以了。
關於混淆APK的用法就講這麼多,如果你還想繼續瞭解關於Proguard的更多用法,可以參考官方文件:http://proguard.sourceforge.net/index.html#manual/usage.html