Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> Android資訊 >> 讀懂 Android 中的代碼混淆

讀懂 Android 中的代碼混淆

編輯:Android資訊

在Android開發工作中,我們都或多或少接觸過代碼混淆。比如我們想要集成某個SDK,往往需要做一些排除混淆的操作。

本文為本人的一些實踐總結,介紹一些混淆的知識和注意事項。希望可以幫助大家更好的學習和使用代碼混淆。

什麼是混淆

關於混淆維基百科上該詞條的解釋為

代碼混淆(Obfuscated code)亦稱花指令,是將計算機程序的代碼,轉換成一種功能上等價,但是難於閱讀和理解的形式的行為。

代碼混淆影響到的元素有

  • 類名
  • 變量名
  • 方法名
  • 包名
  • 其他元素

混淆的目的

混淆的目的是為了加大反編譯的成本,但是並不能徹底防止反編譯.

如何開啟混淆

  • 通常我們需要找到項目路徑下app目錄下的build.gradle文件
  • 找到minifyEnabled這個配置,然後設置為true即可.

一個簡單的示例如下

buildTypes {
        release {
            minifyEnabled true
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'

proguard是什麼

Java官網對Proguard的定義

ProGuard is a free Java Class file shrinker, optimizer, obfuscator, and preverifier. It detects and removes unused classes, fields, methods, and attributes. It optimizes bytecode and removes unused instructions. It renames the remaining classes, fields, and methods using short meaningless names. Finally, it preverifies the processed code for Java 6 or higher, or for Java Micro Edition.

  • Proguard是一個集文件壓縮,優化,混淆和校驗等功能的工具
  • 它檢測並刪除無用的類,變量,方法和屬性
  • 它優化字節碼並刪除無用的指令.
  • 它通過將類名,變量名和方法名重命名為無意義的名稱實現混淆效果.
  • 最後它還校驗處理後的代碼

混淆的常見配置

-keep

Keep用來保留Java的元素不進行混淆. keep有很多變種,他們一般都是

  • -keep
  • -keepclassmembers
  • -keepclasseswithmembers

一些例子

保留某個包下面的類以及子包

-keep public class com.droidyue.com.widget.**

保留所有類中使用otto的public方法

# Otto
-keepclassmembers class ** {
    @com.squareup.otto.Subscribe public *;
    @com.squareup.otto.Produce public *;
}

保留Contants類的BOOK_NAME屬性

-keepclassmembers class com.example.admin.proguardsample.Constants {
     public static java.lang.String BOOK_NAME;
}

更多關於Proguard keep使用,可以參考官方文檔

-dontwarn

dontwarn是一個和keep可以說是形影不離,尤其是處理引入的library時.

引入的library可能存在一些無法找到的引用和其他問題,在build時可能會發出警告,如果我們不進行處理,通常會導致build中止.因此為了保證build繼續,我們需要使用dontwarn處理這些我們無法解決的library的警告.

比如關閉Twitter sdk的警告,我們可以這樣做

-dontwarn com.twitter.sdk.**

其他混淆相關的介紹,都可以通過訪問官方文檔獲取.

哪些不應該混淆

反射中使用的元素

如果一些被混淆使用的元素(屬性,方法,類,包名等)進行了混淆,可能會出現問題,如NoSuchFiledException或者NoSuchMethodException等.

比如下面的示例源碼

//Constants.java
public class Constants {
    public static  String BOOK_NAME = "book_name";
}

//MainActivity.java
Field bookNameField = null;
try {
    String fieldName = "BOOK_NAME";
    bookNameField = Constants.class.getField(fieldName);
    Log.i(LOGTAG, "bookNameField=" + bookNameField);
} catch (NoSuchFieldException e) {
    e.printStackTrace();
}

如果上面的Constants類進行了混淆,那麼上面的語句就可能拋出NoSuchFieldException.

想要驗證,我們需要看一看混淆的映射文件,文件名為mapping.txt,該文件保存著混淆前後的映射關系.

com.example.admin.proguardsample.Constants -> com.example.admin.proguardsample.a:
    java.lang.String BOOK_NAME -> a
    void <init>() -> <init>
    void <clinit>() -> <clinit>
com.example.admin.proguardsample.MainActivity -> com.example.admin.proguardsample.MainActivity:
    void <init>() -> <init>
    void onCreate(android.os.Bundle) -> onCreate

從映射文件中,我們可以看到

  • Constants類被重命名為a.
  • Constants類的BOOK_NAME重命名了a

然後,我們對APK文件進行反編譯一探究竟.推薦一下這個在線反編譯工具 http://www.javadecompilers.com/apk

注意,使用jadx decompiler後,會重新命名,正如下面注釋/* renamed from: com.example.admin.proguardsample.a */所示.

package com.example.admin.proguardsample;

/* renamed from: com.example.admin.proguardsample.a */
public class C0314a {
    public static String f1712a;

    static {
        f1712a = "book_name";
    }
}

而MainActivity的翻譯後的對應的源碼為

try {
    Log.i("MainActivity", "bookNameField=" + C0314a.class.getField("BOOK_NAME"));
} catch (NoSuchFieldException e) {
    e.printStackTrace();
}

MainActivity中反射獲取的屬性名稱依然是BOOK_NAME,而對應的類已經沒有了這個屬性名,所以會拋出NoSuchFieldException.

注意,如果上面的filedName使用字面量或者字符串常量,即使混淆也不會出現NoSuchFieldException異常。因為這兩種情況下,混淆可以感知外界對filed的引用,已經在調用出替換成了混淆後的名稱。

GSON的序列化與反序列化

GSON是一個很好的工具,使用它我們可以輕松的實現序列化和反序列化.但是當它一旦遇到混淆,就需要我們注意了.

一個簡單的類Item,用來處理序列化和反序列化

public class Item {
    public String name;
    public int id;
}

序列化的代碼

Item toSerializeItem = new Item();
toSerializeItem.id = 2;
toSerializeItem.name = "Apple";
String serializedText = gson.toJson(toSerializeItem);
Log.i(LOGTAG, "testGson serializedText=" + serializedText);

開啟混淆之後的日志輸出結果

I/MainActivity: testGson serializedText={"a":"Apple","b":2}

屬性名已經改變了,變成了沒有意思的名稱,對我們後續的某些處理是很麻煩的.

反序列化的代碼

Gson gson = new Gson();
Item item = gson.fromJson("{\"id\":1, \"name\":\"Orange\"}", Item.class);
Log.i(LOGTAG, "testGson item.id=" + item.id + ";item.name=" + item.name);

對應的日志結果是

I/MainActivity: testGson item.id=0;item.name=null

可見,混淆之後,反序列化的屬性值設置都失敗了.

為什麼呢?

  • 因為反序列化創建對象本質還是利用反射,會根據json字符串的key作為屬性名稱,value則對應屬性值.

如何解決

  • 將序列化和反序列化的類排除混淆
  • 使用@SerializedName注解字段

@SerializedName(parameter)通過注解屬性實現了

  • 序列化的結果中,指定該屬性key為parameter的值.
  • 反序列化生成的對象中,用來匹配key與parameter並賦予屬性值.

一個簡單的用法為

public class Item {
    @SerializedName("name")
    public String name;
    @SerializedName("id")
    public int id;

枚舉也不要混淆

枚舉是Java 5 中引入的一個很便利的特性,可以很好的替代之前的常量形式.

枚舉使用起來很簡單,如下

public enum Day {
    MONDAY,
    TUESDAY,
    WEDNESDAY,
    THURSDAY,
    FRIDAY,
    SATURDAY,
    SUNDAY
}

這裡我們這樣使用枚舉

Day day = Day.valueOf("monday");
Log.i(LOGTAG, "testEnum day=" + day);

運行上面的的代碼,通常情況下是沒有問題的,是否說明枚舉就可以混淆呢?

其實不是.

為什麼沒有問題呢,因為默認的Proguard配置已經處理了枚舉相關的keep操作.

# For enumeration classes, see http://proguard.sourceforge.net/manual/examples.html#enumerations
-keepclassmembers enum * {
    public static **[] values();
    public static ** valueOf(java.lang.String);
}

如果我們手動去掉這條keep配置,再次運行,一個這樣的異常會從天而降.

E AndroidRuntime: Process: com.example.admin.proguardsample, PID: 17246
E AndroidRuntime: java.lang.AssertionError: impossible
E AndroidRuntime:  at java.lang.Enum$1.create(Enum.java:45)
E AndroidRuntime:  at java.lang.Enum$1.create(Enum.java:36)
E AndroidRuntime:  at libcore.util.BasicLruCache.get(BasicLruCache.java:54)
E AndroidRuntime:  at java.lang.Enum.getSharedConstants(Enum.java:211)
E AndroidRuntime:  at java.lang.Enum.valueOf(Enum.java:191)
E AndroidRuntime:  at com.example.admin.proguardsample.a.a(Unknown Source)
E AndroidRuntime:  at com.example.admin.proguardsample.MainActivity.j(Unknown Source)
E AndroidRuntime:  at com.example.admin.proguardsample.MainActivity.onCreate(Unknown Source)
E AndroidRuntime:  at android.app.Activity.performCreate(Activity.java:6237)
E AndroidRuntime:  at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1107)
E AndroidRuntime:  at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2369)
E AndroidRuntime:  at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2476)
E AndroidRuntime:  at android.app.ActivityThread.-wrap11(ActivityThread.java)
E AndroidRuntime:  at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1344)
E AndroidRuntime:  at android.os.Handler.dispatchMessage(Handler.java:102)
E AndroidRuntime:  at android.os.Looper.loop(Looper.java:148)
E AndroidRuntime:  at android.app.ActivityThread.main(ActivityThread.java:5417)
E AndroidRuntime:  at java.lang.reflect.Method.invoke(Native Method)
E AndroidRuntime:  at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:726)
E AndroidRuntime:  at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:616)
E AndroidRuntime: Caused by: java.lang.NoSuchMethodException: values []
E AndroidRuntime:  at java.lang.Class.getMethod(Class.java:624)
E AndroidRuntime:  at java.lang.Class.getDeclaredMethod(Class.java:586)
E AndroidRuntime:  at java.lang.Enum$1.create(Enum.java:41)
E AndroidRuntime:  ... 19 more

好玩的事情來了,我們看一看為什麼會拋出這個異常

1.首先,一個枚舉類會生成一個對應的類文件,這裡是Day.class. 這裡類裡面包含什麼呢,看一下反編譯的結果

➜  proguardsample javap  Day
Warning: Binary file Day contains com.example.admin.proguardsample.Day
Compiled from "Day.java"
public final class com.example.admin.proguardsample.Day extends java.lang.Enum<com.example.admin.proguardsample.Day> {
  public static final com.example.admin.proguardsample.Day MONDAY;
  public static final com.example.admin.proguardsample.Day TUESDAY;
  public static final com.example.admin.proguardsample.Day WEDNESDAY;
  public static final com.example.admin.proguardsample.Day THURSDAY;
  public static final com.example.admin.proguardsample.Day FRIDAY;
  public static final com.example.admin.proguardsample.Day SATURDAY;
  public static final com.example.admin.proguardsample.Day SUNDAY;
  public static com.example.admin.proguardsample.Day[] values();
  public static com.example.admin.proguardsample.Day valueOf(java.lang.String);
  static {};
}
  • 枚舉實際是創建了一個繼承自java.lang.Enum的類
  • java代碼中的枚舉類型最後轉換成類中的static final屬性
  • 多出了兩個方法,values()和valueOf().
  • values方法返回定義的枚舉類型的數組集合,即從MONDAY到SUNDAY這7個類型.

2.找尋崩潰軌跡 其中Day.valueOf(String)內部會調用Enum.valueOf(Class,String)方法

public static com.example.admin.proguardsample.Day valueOf(java.lang.String);
    Code:
       0: ldc           #4                  // class com/example/admin/proguardsample/Day
       2: aload_0
       3: invokestatic  #5                  // Method java/lang/Enum.valueOf:(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;
       6: checkcast     #4                  // class com/example/admin/proguardsample/Day
       9: areturn

而Enum的valueOf方法會間接調用Day.values()方法,具體步驟是

  • Enum.value調用Class.enumConstantDirectory方法獲取String到枚舉的映射
  • Class.enumConstantDirectory方法調用Class.getEnumConstantsShared獲取當前的枚舉類型
  • Class.getEnumConstantsShared方法使用反射調用values來獲取枚舉類型的集合.

混淆之後,values被重新命名,所以會發生NoSuchMethodException.

關於調用軌跡,感興趣的可以自己研究一下源碼,不難.

四大組件不建議混淆

Android中四大組件我們都很常用,這些組件不能被混淆的原因為

  • 四大組件聲明必須在manifest中注冊,如果混淆後類名更改,而混淆後的類名沒有在manifest注冊,是不符合Android組件注冊機制的.
  • 外部程序可能使用組件的字符串類名,如果類名混淆,可能導致出現異常

注解不能混淆

注解在Android平台中使用的越來越多,常用的有ButterKnife和Otto.很多場景下注解被用作在運行時反射確定一些元素的特征.

為了保證注解正常工作,我們不應該對注解進行混淆.Android工程默認的混淆配置已經包含了下面保留注解的配置

-keepattributes *Annotation*

關於注解,可以閱讀這篇文章了解.詳解Java中的注解

其他不該混淆的

  • jni調用的java方法
  • java的native方法
  • js調用java的方法
  • 第三方庫不建議混淆
  • 其他和反射相關的一些情況

stacktrace的恢復

Proguard混淆帶來了很多好處,但是也會導致我們收集到的崩潰的stacktrace變得更加難以讀懂,好在有補救的措施,這裡就介紹一個工具,retrace,用來將混淆後的stacktrace還原成混淆之前的信息.

retrace腳本

Android 開發環境默認帶著retrace腳本,一般情況下路徑為./tools/proguard/bin/retrace.sh

mapping映射表

Proguard進行混淆之後,會生成一個映射表,文件名為mapping.txt,我們可以使用find工具在Project下查找

find . -name mapping.txt
./app/build/outputs/mapping/release/mapping.txt

一個崩潰stacktrace信息

一個原始的崩潰信息是這樣的.

E/AndroidRuntime(24006): Caused by: java.lang.NullPointerException: Attempt to invoke virtual method 'int android.graphics.Bitmap.getWidth()' on a null object reference
E/AndroidRuntime(24006):    at com.example.admin.proguardsample.a.a(Utils.java:10)
E/AndroidRuntime(24006):    at com.example.admin.proguardsample.MainActivity.onCreate(MainActivity.java:22)
E/AndroidRuntime(24006):    at android.app.Activity.performCreate(Activity.java:6106)
E/AndroidRuntime(24006):    at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1123)
E/AndroidRuntime(24006):    at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2566)
E/AndroidRuntime(24006):    ... 10 more

對上面的信息處理,去掉E/AndroidRuntime(24006):這些字符串retrace才能正常工作.得到的字符串是

Caused by: java.lang.NullPointerException: Attempt to invoke virtual method 'int android.graphics.Bitmap.getWidth()' on a null object reference
at com.example.admin.proguardsample.a.a(Utils.java:10)
at com.example.admin.proguardsample.MainActivity.onCreate(MainActivity.java:22)
at android.app.Activity.performCreate(Activity.java:6106)
at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1123)
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2566)
... 10 more

將上面的stacktrace保存成一個文本文件,比如名稱為npe_stacktrace.txt.

開搞

./tools/proguard/bin/retrace.sh   /Users/admin/Downloads/ProguardSample/app/build/outputs/mapping/release/mapping.txt /tmp/npe_stacktrace.txt

得到的易讀的stacktrace是

Caused by: java.lang.NullPointerException: Attempt to invoke virtual method 'int android.graphics.Bitmap.getWidth()' on a null object reference
at com.example.admin.proguardsample.Utils.int getBitmapWidth(android.graphics.Bitmap)(Utils.java:10)
at com.example.admin.proguardsample.MainActivity.void onCreate(android.os.Bundle)(MainActivity.java:22)
at android.app.Activity.performCreate(Activity.java:6106)
at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1123)
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2566)
... 10 more

注意:為了更加容易和高效分析stacktrace,建議保留SourceFile和LineNumber屬性

-keepattributes SourceFile,LineNumberTable

關於混淆,我的一些個人經驗總結就是這些.希望可以對大家有所幫助.

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved