Android 代碼混淆 混淆方案

本篇文章:自己在混淆的時候整理出比較全面的混淆方法,比較實用,自己走過的坑,淌出來的路。請大家不要再走回頭路,可能只要我們代碼加混淆,一點不對就會導致項目運行崩潰等后果,有許多人發現沒有打包運行好好地,打包完成以后而又不不可以了,導致了許多困惑,本片文章來問大家解決困惑,希望對大家有幫助。

Android混淆最佳實踐

1. 混淆配置

android{

buildTypes {
        release {
            buildConfigField "boolean", "LOG_DEBUG", "false" //不顯示log
            minifyEnabled true
            shrinkResources true
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
            signingConfig signingConfigs.config
            }
        }
}

因為開啟混淆會使編譯時間變長,所以debug模式下不開啟。我們需要做的是:
1.將release下minifyEnabled的值改為true,打開混淆;
2.加上shrinkResources true,打開資源壓縮。
3.buildConfigField 不顯示log日志
4.signingConfig signingConfigs.config配置簽名文件文件

自定義混淆規則

自定義混淆方案適用于大部分的項目

#指定壓縮級別
-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.**

真正通用的、需要添加的就是上面這些,除此之外,需要每個項目根據自身的需求添加一些混淆規則:

第三方庫所需的混淆規則。正規的第三方庫一般都會在接入文檔中寫好所需混淆規則,使用時注意添加。

在運行時動態改變的代碼,例如反射。比較典型的例子就是會與 json 相互轉換的實體類。假如項目命名規范要求實體類都要放在model包下的話,可以添加類似這樣的代碼把所有實體類都保持住:-keep public class **.*Model*.** {*;}
JNI中調用的類。
WebView中JavaScript調用的方法
Layout布局使用的View構造函數、android:onClick等。

檢查混淆結果

混淆過的包必須進行檢查,避免因混淆引入的bug。
一方面,需要從代碼層面檢查。使用上文的配置進行混淆打包后在<module-name>/build/outputs/mapping/release/目錄下會輸出以下文件:
dump.txt
描述APK文件中所有類的內部結構
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 重復添加

混淆簡介

Android中的“混淆”可以分為兩部分,一部分是Java 代碼的優化與混淆,依靠 proguard混淆器來實現;另一部分是資源壓縮,將移除項目及依賴的庫中未被使用的資源(資源壓縮嚴格意義上跟混淆沒啥關系,但一般我們都會放一起用)。

代碼壓縮

代碼壓縮流程

代碼混淆是包含了代碼壓縮、優化、混淆等一系列行為的過程。如上圖所示,混淆過程會有如下幾個功能:

壓縮。移除無效的類、類成員、方法、屬性等;
優化。分析和優化方法的二進制代碼;根據proguard-android-optimize.txt中的描述,優化可能會造成一些潛在風險,不能保證在所有版本的Dalvik上都正常運行。
混淆。把類名、屬性名、方法名替換為簡短且無意義的名稱;
預校驗。添加預校驗信息。這個預校驗是作用在Java平臺上的,Android平臺上不需要這項功能,去掉之后還可以加快混淆速度。
這四個流程默認開啟。

Android項目中我們可以選擇將“優化”和“預校驗”關閉,對應命令是-dontoptimize、-dontpreverify(當然,默認的 proguard-android.txt文件已包含這兩條混淆命令,不需要開發者額外配置)。

資源壓縮

資源壓縮將移除項目及依賴的庫中未被使用的資源,這在減少apk 包體積上會有不錯的效果,一般建議開啟。具體做法是在 build.grade文件中,將shrinkResources屬性設置為true。需要注意的是,只有在用minifyEnabled true開啟了代碼壓縮后,資源壓縮才會生效。

資源壓縮包含了“合并資源”和“移除資源”兩個流程。

“合并資源”流程中,名稱相同的資源被視為重復資源會被合并。需要注意的是,這一流程不受shrinkResources屬性控制,也無法被禁止,gradle必然會做這項工作,因為假如不同項目中存在相同名稱的資源將導致錯誤。gradle 在四處地方尋找重復資源:

src/main/res/ 路徑
不同的構建類型(debug、release等等)
不同的構建渠道
項目依賴的第三方庫
合并資源時按照如下優先級順序

依賴 -> 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 提供的默認混淆文件,后者是開發者自定義混淆規則的地方。

常見的混淆指令

  • optimizationpasses
  • dontoptimize
  • dontusemixedcaseclassnames
  • dontskipnonpubliclibraryclasses
  • dontpreverify
  • dontwarn
  • verbose
  • optimizations
  • keep
  • keepnames
  • keepclassmembers
  • keepclassmembernames
  • keepclasseswithmembers
  • keepclasseswithmembernames
    更多詳細的請到官網

需要特別介紹的是與保持相關元素不參與混淆的規則相關的幾種命令:

命令 作用
-keep 防止類和成員被移除或者被重命名
-keepnames 防止類和成員被重命名
-keepclassmembers 防止成員被移除或者被重命名
-keepnames 防止成員被重命名
-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)
    舉個例子,假如需要將com.biaobiao.test包下所有繼承Activity的public類及其構造函數都保持住,可以這樣寫:
 -keep public class com.biaobiao.test.** extends Android.app.Activity {
    <init>
}

常用自定義混淆規則

  • 不混淆某個類
-keep public class com.biaobiao.example.Test { *; }

不混淆某個包所有的類

-keep class com.biaobiao.test.** { *; }
}

不混淆某個類的子類

-keep public class * extends com.biaobiao.example.Test { *; }

不混淆所有類名中包含了“model”的類及其成員

-keep public class * extends com.biaobiao.example.Test { *; }

不混淆某個接口的實現

-keep class * implements com.biaobiao.example.TestInterface { *; }

不混淆某個類的構造方法

-keepclassmembers class com.biaobiao.example.Test { 
    public <init>(); 
}

不混淆某個類的特定的方法

-keepclassmembers class com.biaobiao.example.Test { 
    public void test(java.lang.String); 
}
}

不混淆某個類的內部類

-keep class com.biaobiao.example.Test$* {
        *;
 }

自定義資源保持規則

1. keep.xml

shrinkResources true開啟資源壓縮后,所有未被使用的資源默認被移除。假如你需要定義哪些資源必須被保留,在 res/raw/ 路徑下創建一個 xml 文件,例如keep.xml

通過一些屬性的設置可以實現定義資源保持的需求,可配置的屬性有:

  • keep 定義哪些資源需要被保留(資源之間用“,”隔開)
  • discard 定義哪些資源需要被移除(資源之間用“,”隔開)
  • shrinkMode 開啟嚴格模式
  • 當代碼中通過Resources.getIdentifier() 用動態的字符串來獲取并使用資源時,普通的資源引用檢查就可能會有問題。例如,如下代碼會導致所有以“img_”開頭的資源都被標記為已使用。
    當代碼中通過 Resources.getIdentifier() 用動態的字符串來獲取并使用資源時,普通的資源引用檢查就可能會有問題。例如,如下代碼會導致所有以“img_”開頭的資源都被標記為已使用。
String name = String.format("img_%1d", angle + 1);
res = getResources().getIdentifier(name, "drawable", getPackageName());

我們可以設置 tools:shrinkModestrict來開啟嚴格模式,使只有確實被使用的資源被保留。

以上就是自定義資源保持規則相關的配置,舉個例子:

<?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 屬性來指定需要支持的屬性,例如
一些替代資源,例如多語言支持的strings.xml,多分辨率支持的 layout.xml等,在我們不需要使用又不想刪除掉時,可以使用資源壓縮將它們移除。

我們使用 resConfig屬性來指定需要支持的屬性,例如

android {
    defaultConfig {
        ...
        resConfigs "en", "fr"
    }
}

其他未顯式聲明的語言資源將被移除。

最后附上一個我在實際項目中的混淆方案

proguard-android.txt文件內容

# 代碼混淆壓縮比,在0~7之間
-optimizationpasses 5
# 混合時不使用大小寫混合,混合后的類名為小寫
-dontusemixedcaseclassnames
# 指定不去忽略非公共庫的類
-dontskipnonpubliclibraryclasses
# 不做預校驗,preverify是proguard的四個步驟之一,Android不需要preverify,去掉這一步能夠加快混淆速度。
-dontpreverify
-verbose
# 避免混淆泛型
-keepattributes Signature

# 保留Annotation不混淆
-keepattributes *Annotation*,InnerClasses
#google推薦算法
-optimizations !code/simplification/arithmetic,!code/simplification/cast,!field/*,!class/merging/*
# 避免混淆Annotation、內部類、泛型、匿名類
-keepattributes *Annotation*,InnerClasses,Signature,EnclosingMethod
# 重命名拋出異常時的文件名稱
-renamesourcefileattribute SourceFile
# 拋出異常時保留代碼行號
-keepattributes SourceFile,LineNumberTable
# 處理support包
-dontnote android.support.**
-dontwarn android.support.**
# 保留繼承的
-keep public class * extends android.support.v4.**
-keep public class * extends android.support.v7.**
-keep public class * extends android.support.annotation.**

# 保留R下面的資源
-keep class **.R$* {*;}
# 保留四大組件,自定義的Application等這些類不被混淆
-keep public class * extends android.app.Activity
-keep public class * extends android.app.Appliction
-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.preference.Preference
-keep public class com.android.vending.licensing.ILicensingService

# 保留在Activity中的方法參數是view的方法,
# 這樣以來我們在layout中寫的onClick就不會被影響
-keepclassmembers class * extends android.app.Activity{
    public void *(android.view.View);
}
# 對于帶有回調函數的onXXEvent、**On*Listener的,不能被混淆
-keepclassmembers class * {
    void *(**On*Event);
    void *(**On*Listener);
}
# 保留本地native方法不被混淆
-keepclasseswithmembernames class * {
    native <methods>;
}

# 保留枚舉類不被混淆
-keepclassmembers enum * {
    public static **[] values();
    public static ** valueOf(java.lang.String);
}

# 保留Parcelable序列化類不被混淆
-keep class * implements android.os.Parcelable {
    public static final android.os.Parcelable$Creator *;
}

-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();
}
#assume no side effects:刪除android.util.Log輸出的日志
-assumenosideeffects class android.util.Log {
    public static *** v(...);
    public static *** d(...);
    public static *** i(...);
    public static *** w(...);
    public static *** e(...);
}
#保留Keep注解的類名和方法
-keep,allowobfuscation @interface android.support.annotation.Keep
-keep @android.support.annotation.Keep class *
-keepclassmembers class * {
    @android.support.annotation.Keep *;
}
#3D 地圖 V5.0.0之前:

-dontwarn com.amap.api.**
-dontwarn com.autonavi.**
-keep class com.amap.api.**{*;}
-keep class com.autonavi.**{*;}

-keep   class com.amap.api.maps.**{*;}
-keep   class com.autonavi.amap.mapcore.*{*;}
-keep   class com.amap.api.trace.**{*;}

#3D 地圖 V5.0.0之后:
-keep   class com.amap.api.maps.**{*;}
-keep   class com.autonavi.**{*;}
-keep   class com.amap.api.trace.**{*;}

#定位
-keep class com.amap.api.location.**{*;}
-keep class com.amap.api.fence.**{*;}
-keep class com.autonavi.aps.amapapi.model.**{*;}

#搜索
-keep   class com.amap.api.services.**{*;}

#2D地圖
-keep class com.amap.api.maps2d.**{*;}
-keep class com.amap.api.mapcore2d.**{*;}

#導航
-keep class com.amap.api.navi.**{*;}
-keep class com.autonavi.**{*;}
# Retain generic type information for use by reflection by converters and adapters.
-keepattributes Signature

# Retain service method parameters when optimizing.
-keepclassmembers,allowshrinking,allowobfuscation interface * {
    @retrofit2.http.* <methods>;
}

# Ignore annotation used for build tooling.
-dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement

# Ignore JSR 305 annotations for embedding nullability information.
-dontwarn javax.annotation.**

# JSR 305 annotations are for embedding nullability information.
-dontwarn javax.annotation.**

# A resource is loaded with a relative path so the package of this class must be preserved.
-keepnames class okhttp3.internal.publicsuffix.PublicSuffixDatabase

# Animal Sniffer compileOnly dependency to ensure APIs are compatible with older versions of Java.
-dontwarn org.codehaus.mojo.animal_sniffer.*

# OkHttp platform used only on JVM and when Conscrypt dependency is available.
-dontwarn okhttp3.internal.platform.ConscryptPlatform

#fastjson混淆
-keepattributes Signature
-dontwarn com.alibaba.fastjson.**
-keep class com.alibaba.**{*;}
-keep class com.alibaba.fastjson.**{*; }
-keep public class com.ninstarscf.ld.model.entity.**{*;}

所有文章參考

愿意與作者一起交流的加作者微信,備注信息簡書~某某
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 聲明 這篇文章更多的是做一個整理,內容來自于ProGuard官方文檔以及各種博客等,相關文章的鏈接在參考目錄里,感...
    夷陵小祖閱讀 3,708評論 0 23
  • Android 開發中為了代碼安全一般都會使用 ProGuard 進行代碼混淆,它可以把類名、屬性名和方法名變為毫...
    JohnnyShieh閱讀 4,312評論 2 13
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 173,124評論 25 708
  • 寫這篇文章的目的 一直以來,在項目中需要進行代碼混淆時每次都要去翻文檔,很麻煩。也沒有像寫代碼那樣記得那么多。既然...
    FindUById閱讀 3,119評論 0 4
  • 從本篇文章/音頻/視頻中我學到的最重要的概念 教育不是沿襲一貫的理論傳統,而是能夠具有獨立思考意識的去接受一切,及...
    劉夢園數學4班閱讀 680評論 3 0