Android打包混淆壓縮
宣告
混淆
簡介
說到混淆,就要說到proGuard,Android的混淆是有proGuard來完成的,ProGuard是一個開源專案在SourceForge上進行維護。
流程
程式碼混淆是包含了程式碼壓縮、優化、混淆等一系列行為的過程。如上圖所示,混淆過程會有如下幾個功能:
- 壓縮。移除無效的類、類成員、方法、屬性等;
- 優化。分析和優化方法的二進位制程式碼,移除無用指令;根據proguard-android-optimize.txt中的描述,優化可能會造成一些潛在風險,不能保證在所有版本的Dalvik上都正常執行。
- 混淆。把類名、屬性名、方法名替換為簡短且無意義的名稱;
- 預校驗。新增預校驗資訊。這個預校驗是作用在Java平臺上的,Android平臺上不需要這項功能,去掉之後還可以加快混淆速度。
這四個流程預設開啟。
如何混淆
混淆配置
一般的,我們在build.gradle都會這樣寫:
buildTypes {
release {
zipAlignEnabled true
minifyEnabled true
// 移除無用的resource檔案
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android.txt' ), 'proguard-rules.pro'
signingConfig signingConfigs.xxxxx
}
}
minifyEnabled true
設為true開啟混淆,proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
設定混淆檔案的配置。其中proguard-android.txt
是Android自帶的混淆配置,我們可以在SDK的sdk\tools\proguard
目錄找到該檔案,裡邊配置了一些基本需要的混淆。proguard-rules.pro
proguard-android.txt
的內容:
# 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);
}
-keepclassmembers class * implements android.os.Parcelable {
public static final android.os.Parcelable$Creator 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.**
# Understand the @Keep support annotation.
-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>(...);
}
一般的proguard-android.txt檔案中已經有的混淆配置,我們可以不再新增,當然寫了也沒有問題!
檢視混淆結果
混淆過的包必須進行檢查,避免因混淆引入的bug。
一方面,需要從程式碼層面檢查。
使用上文的配置進行混淆打包後在·app/build/outputs/mapping/release/
目錄下會輸出以下檔案:
- dump.txt
描述APK檔案中所有類的內部結構 - mapping.txt
提供混淆前後類、方法、類成員等的對照表 - seeds.txt
列出沒有被混淆的類和成員 - usage.txt
列出被移除的程式碼
我們可以根據 seeds.txt 檔案檢查未被混淆的類和成員中是否已包含所有期望保留的,再根據 usage.txt 檔案檢視是否有被誤移除的程式碼。
另一方面,需要從測試方面檢查
將混淆過的包進行全方面測試,檢查是否有 bug 產生。這時候解出混淆棧。
混淆後的類、方法名等等難以閱讀,這固然會增加逆向工程的難度,但對追蹤線上 crash 也造成了阻礙。我們拿到 crash 的堆疊資訊後會發現很難定位,這時需要將混淆反解。
在 sdk/tools/proguard/
路徑下有附帶的的反解工具(Window 系統為 proguardgui.bat
,Mac 或 Linux 系統為 proguardgui.sh
)。
這裡以 Window 平臺為例。雙擊執行proguardgui.bat
後,可以看到左側的一行選單。點選 ReTrace,選擇該混淆包對應的 mapping 檔案(混淆後在 app/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
自定義的混淆配置
保持元素不被混淆的相關命令
-keep
防止類和成員被移除或者被重新命名-keepnames
防止類和成員被重新命名,但當成員沒有被引用時會被移除-keepclassmembers
防止成員被移除或者被重新命名-keepclassmembernames
防止成員被重新命名,但當成員沒有被引用時會被移除-keepclasseswithmembers
防止擁有該成員的類和成員被移除或者被重新命名-keepclasseswithmembernames
防止擁有該成員的類和成員被重新命名
保持元素不被混淆的相關規則
規則形如:
[保持命令] [類] {
[成員]
}
①保持命令:就是指上邊說的幾個保持命令
②類:類相關的限定條件,它將最終定位到某些符合該限定條件的類。它的內容可以使用:
- 具體的類
- 訪問修飾符(public、protected、private)
- 萬用字元
*
,匹配任意長度字元,但不含包名分隔符(.) - 萬用字元
**
,匹配任意長度字元,並且包含包名分隔符(.) extends
,即可以指定類的基類implement
,匹配實現了某介面的類$
,內部類
③成員:代表類成員相關的限定條件,它將最終定位到某些符合該限定條件的類成員。它的內容可以使用:
<init>
匹配所有構造器<fields>
匹配所有域<methods>
匹配所有方法- 萬用字元
*
,匹配任意長度字元,但不含包名分隔符(.) - 萬用字元
**
,匹配任意長度字元,並且包含包名分隔符(.) - 萬用字元
***
,匹配任意引數型別 …
,匹配任意長度的任意型別引數。比如void test(…)就能匹配任意 void test(String a) 或者是 void test(int a, String b) 這些方法。- 訪問修飾符(public、protected、private)
通用的混淆規則
混淆的時候,可以直接複製:
#不優化輸入的類檔案
-dontoptimize
-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>(...);
}
# 程式碼混淆壓縮比,在0~7之間,預設為5,一般不下需要修改
-optimizationpasses 5
# 混淆時不使用大小寫混合,混淆後的類名為小寫
# windows下的同學還是加入這個選項吧(windows大小寫不敏感)
-dontusemixedcaseclassnames
# 指定不去忽略非公共的庫的類
# 預設跳過,有些情況下編寫的程式碼與類庫中的類在同一個包下,並且持有包中內容的引用,此時就需要加入此條宣告
-dontskipnonpubliclibraryclasses
# 指定不去忽略非公共的庫的類的成員
-dontskipnonpubliclibraryclassmembers
# 不做預檢驗,preverify是proguard的四個步驟之一
# Android不需要preverify,去掉這一步可以加快混淆速度
-dontpreverify
# 有了verbose這句話,混淆後就會生成對映檔案
# 包含有類名->混淆後類名的對映關係
# 然後使用printmapping指定對映檔案的名稱
-verbose
-printmapping priguardMapping.txt
# 指定混淆時採用的演算法,後面的引數是一個過濾器
# 這個過濾器是谷歌推薦的演算法,一般不改變
-optimizations !code/simplification/artithmetic,!field/*,!class/merging/*
# 保護程式碼中的Annotation不被混淆
# 這在JSON實體對映時非常重要,比如fastJson
-keepattributes *Annotation*
# 避免混淆泛型
# 這在JSON實體對映時非常重要,比如fastJson
-keepattributes Signature
#將檔案來源重新命名為“SourceFile”字串
-renamesourcefileattribute SourceFile
# 丟擲異常時保留程式碼行號
-keepattributes SourceFile,LineNumberTable
# 保留所有的本地native方法不被混淆
-keepclasseswithmembernames class * {
native <methods>;
}
#把混淆類中的方法名也混淆了
-useuniqueclassmembernames
#優化時允許訪問並修改有修飾符的類和類的成員
-allowaccessmodification
# 保留了繼承自Activity、Application這些類的子類
# 因為這些子類有可能被外部呼叫
# 比如第一行就保證了所有Activity的子類不要被混淆
-keep public class * extends android.app.Activity
-keep public class * extends android.app.Application
-keep public class * extends android.app.Service
-keep public class * extends android.content.BroadcastReceiver
-keep public class * extends android.content.ContentProvider
-keep public class * extends android.app.backup.BackupAgentHelper
-keep public class * extends android.preference.Preference
-keep public class * extends android.view.View
-keep public class com.android.vending.licensing.ILicensingService
#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.**
# 保留Activity中的方法引數是view的方法,
# 從而我們在layout裡面編寫onClick就不會影響
-keepclassmembers class * extends android.app.Activity {
public void * (android.view.View);
}
# 列舉類不能被混淆
-keepclassmembers enum * {
public static **[] values();
public static ** valueOf(java.lang.String);
}
# 保留自定義控制元件(繼承自View)不能被混淆
-keep public class * extends android.view.View {
public <init>(android.content.Context);
public <init>(android.content.Context, android.util.AttributeSet);
public <init>(android.content.Context, android.util.AttributeSet, int);
public void set*(***);
*** get* ();
}
# 保留Parcelable序列化的類不能被混淆
-keep class * implements android.os.Parcelable{
public static final android.os.Parcelable$Creator *;
}
# 保留Serializable 序列化的類不被混淆
-keepclassmembers class * implements java.io.Serializable {
static final long serialVersionUID;
private static final java.io.ObjectStreamField[] serialPersistentFields;
!static !transient <fields>;
private void writeObject(java.io.ObjectOutputStream);
private void readObject(java.io.ObjectInputStream);
java.lang.Object writeReplace();
java.lang.Object readResolve();
}
# 對R檔案下的所有類及其方法,都不能被混淆
-keepclassmembers class **.R$* {
*;
}
# 對於帶有回撥函式onXXEvent的,不能混淆
-keepclassmembers class * {
void *(**On*Event);
}
#實體類不能被混淆
-keep class com.test.beans.** {*;}
#忽略get和set方法
-keep class com.test.beans.** {
public void set*(***);
public *** get*();
public *** is*();
}
#以上兩種任意一種都行
#對於內部類,$就是用來分割內嵌類和母體的標誌
#-keep class com.test.**$*{*;}
#保留support下的所有類及其內部類
-keep class android.support.** {*;}
#不需要提示 警告
-dontwarn android.support.**
# support-v7-appcompat
-keep public class android.support.v7.widget.** { *; }
-keep public class android.support.v7.internal.widget.** { *; }
-keep public class android.support.v7.internal.view.menu.** { *; }
-keep public class * extends android.support.v4.view.ActionProvider {
public <init>(android.content.Context);
}
# support-design
-dontwarn android.support.design.**
-keep class android.support.design.** { *; }
-keep interface android.support.design.** { *; }
-keep public class android.support.design.R$* { *; }
其他混淆規則
OkHttp3
-dontwarn okhttp3.logging.**
-keep class okhttp3.internal.**{*;}
-dontwarn okio.**
Retrofit2.x
-dontwarn retrofit2.**
-keep class retrofit2.** { *; }
RxJava RxAndroid
-dontwarn sun.misc.**
-keepclassmembers class rx.internal.util.unsafe.*ArrayQueue*Field* {
long producerIndex;
long consumerIndex;
}
-keepclassmembers class rx.internal.util.unsafe.BaseLinkedQueueProducerNodeRef {
rx.internal.util.atomic.LinkedQueueNode producerNode;
}
-keepclassmembers class rx.internal.util.unsafe.BaseLinkedQueueConsumerNodeRef {
rx.internal.util.atomic.LinkedQueueNode consumerNode;
}
Gson
-keep class com.google.gson.** { *; }
-keepattributes EnclosingMethod
Glide
-keep public class * implements com.bumptech.glide.module.GlideModule
-keep public enum com.bumptech.glide.load.resource.bitmap.ImageHeaderParser$** {
**[] $VALUES;
public *;
}
# for DexGuard only
-keepresourcexmlelements manifest/application/meta-data@value=GlideModule
ButterKnife
-keep class butterknife.** { *; }
-dontwarn butterknife.internal.**
-keep class **$$ViewBinder { *; }
-keepclasseswithmembernames class * {
@butterknife.* <fields>;
}
-keepclasseswithmembernames class * {
@butterknife.* <methods>;
}
資源壓縮
資源壓縮將移除專案及依賴的庫中未被使用的資源,這在減少 apk 包體積上會有不錯的效果,一般在打realease包的時候建議開啟。
具體做法是在 build.grade 檔案中,將shrinkResources
屬性設定為 true。需要注意的是,只有在用minifyEnabled true
開啟了程式碼壓縮後,資源壓縮才會生效。
資源壓縮包含了“合併資源”和“移除資源”兩個流程。
合併資源
“合併資源”流程中,名稱相同的資源被視為重複資源會被合併。需要注意的是,這一流程不受shrinkResources屬性控制,也無法被禁止, gradle 必然會做這項工作,因為假如不同專案中存在相同名稱的資源將導致錯誤。gradle 在四處地方尋找重複資源:
①src/main/res/ 路徑
②不同的構建型別(debug、release等等)
③不同的構建渠道
④專案依賴的第三方庫
合併資源時按照如下優先順序順序:
依賴 -> main -> 渠道 -> 構建型別
舉個例子,
假如重複資源同時存在於main資料夾和不同渠道中,gradle 會選擇保留渠道中的資源。同時,如果重複資源在同一層次出現,比如src/main/res/
和 src/main/res2/
,則 gradle 無法完成資源合併,這時會報資源合併錯誤。
移除資源
資源移除的時候,跟程式碼混淆一樣,也可以定義哪些資源需要被保留。
保持某些資源不被移除
用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"
}
}
其他未顯式宣告的語言資源將被移除。