1. 程式人生 > >Android打包混淆壓縮

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 } }
  1. minifyEnabled true設為true開啟混淆,
  2. 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"
    }
}

其他未顯式宣告的語言資源將被移除。