你們好,我是光源。html
本文首發於個人我的公衆帳號,同時會在我的博客上同步。假若有任何建議還請移步博客點評,同時若是博客自己有修改或勘誤,也會在博客更新。java
毫無疑問,混淆是打包過程當中最重要的流程之一,在沒有特殊緣由的狀況下,全部 app 都應該開啓混淆。android
首先,這裏說的的混淆實際上是包括了代碼壓縮、代碼混淆以及資源壓縮等的優化過程。依靠 ProGuard,混淆流程將主項目以及依賴庫中未被使用的類、類成員、方法、屬性移除,這有助於規避64K方法數的瓶頸;同時,將類、類成員、方法重命名爲無心義的簡短名稱,增長了逆向工程的難度。而依靠 Gradle 的 Android 插件,咱們將移除未被使用的資源,能夠有效減少 apk 安裝包大小。算法
本文由兩部分構成,第一部分給出混淆的最佳實踐,力求讓零基礎的新手均可以直接使用混淆;第二部分會介紹一下混淆的總體、自定義混淆規則的語法與實踐、自定義資源保持的規則等。json
通常狀況下,app module 的 build.gradle
文件默認會有以下結構:安全
android { buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } }
由於開啓混淆會使編譯時間變長,因此debug
模式下不該該開啓。咱們須要作的是:app
release
下minifyEnabled
的值改成true
,打開混淆;shrinkResources true
,打開資源壓縮。修改後文件內容以下:函數
android { buildTypes { release { minifyEnabled true shrinkResources true proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } }
在 app module
下默認生成了項目的自定義混淆規則文件 proguard-rules.pro
,多方調研後,一份適用於大部分項目的混淆規則最佳實踐以下:工具
#指定壓縮級別 -optimizationpasses 5 #不跳過非公共的庫的類成員 -dontskipnonpubliclibraryclassmembers #混淆時採用的算法 -optimizations !code/simplification/arithmetic,!field/*,!class/merging/* #把混淆類中的方法名也混淆了 -useuniqueclassmembernames #優化時容許訪問並修改有修飾符的類和類的成員 -allowaccessmodification #將文件來源重命名爲「SourceFile」字符串 -renamesourcefileattribute SourceFile #保留行號 -keepattributes SourceFile,LineNumberTable #保持泛型 -keepattributes Signature #保持全部實現 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中註冊,須要額外保護下 -keep public class * extends android.support.v4.app.Fragment -keep public class * extends android.app.Fragment # 保持測試相關的代碼 -dontnote junit.framework.** -dontnote junit.runner.** -dontwarn android.test.** -dontwarn android.support.test.** -dontwarn org.junit.**
真正通用的、須要添加的就是上面這些,除此以外,須要每一個項目根據自身的需求添加一些混淆規則:佈局
model
包下的話,能夠添加相似這樣的代碼把全部實體類都保持住:-keep public class **.*Model*.** {*;}
JNI
中調用的類。WebView
中JavaScript
調用的方法Layout
佈局使用的View
構造函數、android:onClick
等。混淆過的包必須進行檢查,避免因混淆引入的bug。
一方面,須要從代碼層面檢查。使用上文的配置進行混淆打包後在 <module-name>/build/outputs/mapping/release/
目錄下會輸出如下文件:
dump.txt
mapping.txt
seeds.txt
usage.txt
咱們能夠根據 seeds.txt
文件檢查未被混淆的類和成員中是否已包含全部指望保留的,再根據 usage.txt
文件查看是否有被誤移除的代碼。
另外一方面,須要從測試方面檢查。將混淆過的包進行全方面測試,檢查是否有 bug 產生。
混淆後的類、方法名等等難以閱讀,這當然會增長逆向工程的難度,但對追蹤線上 crash 也形成了阻礙。咱們拿到 crash 的堆棧信息後會發現很難定位,這時須要將混淆反解。
在 <sdk-root>/tools/proguard/
路徑下有附帶的的反解工具(Window 系統爲 proguardgui.bat
,Mac 或 Linux 系統爲 proguardgui.sh
)。
這裏以 Window 平臺爲例。雙擊運行 proguardgui.bat
後,能夠看到左側的一行菜單。點擊 ReTrace
,選擇該混淆包對應的 mapping 文件(混淆後在 <module-name>/build/outputs/mapping/release/
路徑下會生成 mapping.txt
文件,它的做用是提供混淆先後類、方法、類成員等的對照表),再將 crash 的 stack trace
黏貼進輸入框中,點擊右下角的 ReTrace
,混淆後的堆棧信息就顯示出來了。
以上使用 GUI 程序進行操做,另外一種方式是利用該路徑下的 retrace
工具經過命令行進行反解,命令是
retrace.bat|retrace.sh [-verbose] mapping.txt [<stacktrace_file>]
例如:
retrace.bat -verbose mapping.txt obfuscated_trace.txt
全部在 AndroidManifest.xml
涉及到的類已經自動被保持,所以不用特地去添加這塊混淆規則。(不少老的混淆文件裏會加,如今已經不必)
proguard-android.txt
已經存在一些默認混淆規則,不必在 proguard-rules.pro
重複添加,該文件具體規則見附錄1:
Android中的「混淆」能夠分爲兩部分,一部分是 Java 代碼的優化與混淆,依靠 proguard 混淆器來實現;另外一部分是資源壓縮,將移除項目及依賴的庫中未被使用的資源(資源壓縮嚴格意義上跟混淆沒啥關係,但通常咱們都會放一塊兒講)。
代碼混淆是包含了代碼壓縮、優化、混淆等一系列行爲的過程。如上圖所示,混淆過程會有以下幾個功能:
這四個流程默認開啓。
在 Android 項目中咱們能夠選擇將「優化」和「預校驗」關閉,對應命令是-dontoptimize
、-dontpreverify
(固然,默認的 proguard-android.txt
文件已包含這兩條混淆命令,不須要開發者額外配置)。
資源壓縮將移除項目及依賴的庫中未被使用的資源,這在減小 apk 包體積上會有不錯的效果,通常建議開啓。具體作法是在 build.grade
文件中,將 shrinkResources
屬性設置爲 true
。須要注意的是,只有在用minifyEnabled true
開啓了代碼壓縮後,資源壓縮纔會生效。
資源壓縮包含了「合併資源」和「移除資源」兩個流程。
「合併資源」流程中,名稱相同的資源被視爲重複資源會被合併。須要注意的是,這一流程不受shrinkResources
屬性控制,也沒法被禁止,gradle 必然會作這項工做,由於假如不一樣項目中存在相同名稱的資源將致使錯誤。gradle 在四處地方尋找重複資源:
src/main/res/
路徑合併資源時按照以下優先級順序:
依賴 -> main -> 渠道 -> 構建類型
舉個例子,假如重複資源同時存在於main
文件夾和不一樣渠道中,gradle 會選擇保留渠道中的資源。
同時,若是重複資源在同一層次出現,好比src/main/res/
和 src/main/res2/
,則 gradle 沒法完成資源合併,這時會報資源合併錯誤。
「移除資源」流程則見名知意,須要注意的是,相似代碼,混淆資源移除也能夠定義哪些資源須要被保留,這點在下文給出。
在上文「混淆配置」中有這樣一行代碼
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
這行代碼定義了混淆規則由兩部分構成:位於 SDK 的 tools/proguard/
文件夾中的 proguard-android.txt
的內容以及默認放置於模塊根目錄的 proguard-rules.pro
的內容。前者是 SDK 提供的默認混淆文件(內容見附錄1),後者是開發者自定義混淆規則的地方。
在第一部分 Android 混淆最佳實踐中已介紹部分須要使用到的混淆命令,這裏再也不贅述,詳情請查閱官網。須要特別介紹的是與保持相關元素不參與混淆的規則相關的幾種命令:
命令 | 做用 |
---|---|
-keep | 防止類和成員被移除或者被重命名 |
-keepnames | 防止類和成員被重命名 |
-keepclassmembers | 防止成員被移除或者被重命名 |
-keepnames | 防止成員被重命名 |
-keepclasseswithmembers | 防止擁有該成員的類和成員被移除或者被重命名 |
-keepclasseswithmembernames | 防止擁有該成員的類和成員被重命名 |
形如:
[保持命令] [類] { [成員] }
「類」表明類相關的限定條件,它將最終定位到某些符合該限定條件的類。它的內容可使用:
public
、protected
、private
)*
,匹配任意長度字符,但不含包名分隔符(.)**
,匹配任意長度字符,而且包含包名分隔符(.)extends
,便可以指定類的基類implement
,匹配實現了某接口的類「成員」表明類成員相關的限定條件,它將最終定位到某些符合該限定條件的類成員。它的內容可使用:
*
,匹配任意長度字符,但不含包名分隔符(.)**
,匹配任意長度字符,而且包含包名分隔符(.)***
,匹配任意參數類型…
,匹配任意長度的任意類型參數。好比void test(…)就能匹配任意 void test(String a)
或者是 void test(int a, String b)
這些方法。public
、protected
、private
)舉個例子,假如須要將name.huihui.test
包下全部繼承Activity
的public
類及其構造函數都保持住,能夠這樣寫:
-keep public class name.huihui.test.** extends Android.app.Activity { <init> }
-keep public class name.huihui.example.Test { *; }
-keep class name.huihui.test.** { *; }
-keep public class * extends name.huihui.example.Test { *; }
-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$* { *; }
用shrinkResources true
開啓資源壓縮後,全部未被使用的資源默認被移除。假如你須要定義哪些資源必須被保留,在 res/raw/
路徑下建立一個 xml 文件,例如 keep.xml
。
經過一些屬性的設置能夠實現定義資源保持的需求,可配置的屬性有:
tools:keep
定義哪些資源須要被保留(資源之間用「,」隔開)tools:discard
定義哪些資源須要被移除(資源之間用「,」隔開)tools:shrinkMode
開啓嚴格模式當代碼中經過 Resources.getIdentifier()
用動態的字符串來獲取並使用資源時,普通的資源引用檢查就可能會有問題。例如,以下代碼會致使全部以「img_」開頭的資源都被標記爲已使用。
String name = String.format("img_%1d", angle + 1); res = getResources().getIdentifier(name, "drawable", getPackageName());
咱們能夠設置 tools:shrinkMode
爲 strict
來開啓嚴格模式,使只有確實被使用的資源被保留。
以上就是自定義資源保持規則相關的配置,舉個例子:
<?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"/>
一些替代資源,例如多語言支持的 strings.xml
,多分辨率支持的 layout.xml
等,在咱們不須要使用又不想刪除掉時,可使用資源壓縮將它們移除。
咱們使用 resConfig
屬性來指定須要支持的屬性,例如
android { defaultConfig { ... resConfigs "en", "fr" } }
其餘未顯式聲明的語言資源將被移除。
proguard-android.txt
文件內容#包名不混合大小寫 -dontusemixedcaseclassnames #不跳過非公共的庫的類 -dontskipnonpubliclibraryclasses #混淆時記錄日誌 -verbose #關閉預校驗 -dontpreverify #不優化輸入的類文件 -dontoptimize #保護註解 -keepattributes *Annotation* #保持全部擁有本地方法的類名及本地方法名 -keepclasseswithmembernames class * { native <methods>; } #保持自定義View的get和set相關方法 -keepclassmembers public class * extends android.view.View { void set*(***); *** get*(); } #保持Activity中View及其子類入參的方法 -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文件的靜態成員 -keepclassmembers class **.R$* { public static <fields>; } -dontwarn android.support.** #keep相關注解 -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>(...); }
做者:光源_Android 連接:https://www.jianshu.com/p/158aa484da13 來源:簡書 簡書著做權歸做者全部,任何形式的轉載都請聯繫做者得到受權並註明出處。