編輯:Android資訊
在Android開發工作中,我們都或多或少接觸過代碼混淆。比如我們想要集成某個SDK,往往需要做一些排除混淆的操作。
本文為本人的一些實踐總結,介紹一些混淆的知識和注意事項。希望可以幫助大家更好的學習和使用代碼混淆。
關於混淆維基百科上該詞條的解釋為
代碼混淆(Obfuscated code)亦稱花指令,是將計算機程序的代碼,轉換成一種功能上等價,但是難於閱讀和理解的形式的行為。
代碼混淆影響到的元素有
混淆的目的是為了加大反編譯的成本,但是並不能徹底防止反編譯.
一個簡單的示例如下
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.
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使用,可以參考官方文檔
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
.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是一個很好的工具,使用它我們可以輕松的實現序列化和反序列化.但是當它一旦遇到混淆,就需要我們注意了.
一個簡單的類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
可見,混淆之後,反序列化的屬性值設置都失敗了.
@SerializedName
注解字段@SerializedName(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 {}; }
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()方法,具體步驟是
混淆之後,values被重新命名,所以會發生NoSuchMethodException
.
關於調用軌跡,感興趣的可以自己研究一下源碼,不難.
Android中四大組件我們都很常用,這些組件不能被混淆的原因為
注解在Android平台中使用的越來越多,常用的有ButterKnife和Otto.很多場景下注解被用作在運行時反射確定一些元素的特征.
為了保證注解正常工作,我們不應該對注解進行混淆.Android工程默認的混淆配置已經包含了下面保留注解的配置
-keepattributes *Annotation*
關於注解,可以閱讀這篇文章了解.詳解Java中的注解
Proguard混淆帶來了很多好處,但是也會導致我們收集到的崩潰的stacktrace變得更加難以讀懂,好在有補救的措施,這裡就介紹一個工具,retrace,用來將混淆後的stacktrace還原成混淆之前的信息.
Android 開發環境默認帶著retrace腳本,一般情況下路徑為./tools/proguard/bin/retrace.sh
Proguard進行混淆之後,會生成一個映射表,文件名為mapping.txt,我們可以使用find工具在Project下查找
find . -name mapping.txt ./app/build/outputs/mapping/release/mapping.txt
一個原始的崩潰信息是這樣的.
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
關於混淆,我的一些個人經驗總結就是這些.希望可以對大家有所幫助.
本文由碼農網 – 小峰原創,轉載請看清文末的轉載要求,歡迎參與我們的付費投稿計劃! ASimpleCache是一款基於Android的輕量級緩存框架,它只
一、OOM問題出現的場景和原因 一個好的app總少不了精美的圖片,所以Android開發中圖片的加載總是避免不了的,而在加載圖片過程中,如果處理不當則會出現OOM
本文由碼農網 – 蘇耀東原創,轉載請看清文末的轉載要求,歡迎參與我們的付費投稿計劃! 簡介 本文是參考google官方發布的MVP架構demo以及前人對M
一. 概述 Android系統將進程做得很友好的封裝,對於上層app開發者來說進程幾乎是透明的. 了解Android的朋友,一定知道Android四大組件,但對於