編輯:關於Android編程
在Android開發工作中,我們都或多或少接觸過代碼混淆。比如我們想要集成某個SDK,往往需要做一些排除混淆的操作。
本文為本人的一些實踐總結,介紹一些混淆的知識和注意事項。希望可以幫助大家更好的學習和使用代碼混淆。
關於混淆維基百科上該詞條的解釋為
代碼混淆(Obfuscated code)亦稱花指令,是將計算機程序的代碼,轉換成一種功能上等價,但是難於閱讀和理解的形式的行為。
代碼混淆影響到的元素有
混淆的目的是為了加大反編譯的成本,但是並不能徹底防止反編譯.
一個簡單的示例如下
buildTypes { release { minifyEnabled true proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
proguard是什麼
Java官網對Proguard的定義
ProGuard is a freeJava Classfile 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.
Keep用來保留Java的元素不進行混淆. keep有很多變種,他們一般都是
保留某個包下面的類以及子包
-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使用,可以參考vc7EtbU=">官方文檔
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 () ->
void () ->
com.example.admin.proguardsample.MainActivity -> com.example.admin.proguardsample.MainActivity:
void () ->
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 {
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
關於混淆,我的一些個人經驗總結就是這些.希望可以對大家有所幫助.
啦啦啦,這是山寨UC浏覽器的下拉刷新效果的第二篇,第一篇請移步Android 自定義View UC下拉刷新效果(一)我們看圖說話:主要工作1.下拉刷新的圓形向回首頁的圓形
AMS對startActivity請求處理及返回過程根據上一章的分析了解了調用startActivity(),終於把數據和要開啟Activity的請求發送到了AMS了,接
一、---框架---首先還是來把總體的編碼流程來樹梳理一下,按照這個順序來編碼可以使思路更加清晰。(1)創建兩個View,一個listview一個item_view(2)
handler在安卓開發中是必須掌握的技術,但是很多人都是停留在使用階段。使用起來很簡單,就兩個步驟,在主線程重寫handler的handleMessage( )方法,在