读懂 Android 中的代码混淆

更新时间:2016-07-11 15:59:48 点击次数:1995次

在Android开发工作中,我们都或多或少接触过代码混淆。比如我们想要集成某个SDK,往往需要做一些排除混淆的操作。

本文为本人的一些实践总结,介绍一些混淆的知识和注意事项。希望可以帮助大家更好的学习和使用代码混淆。

什么是混淆

关于混淆维基百科上该词条的解释为

代码混淆(Obfuscated code)亦称花指令,是将计算机程序的代码,转换成一种功能上等价,但是难于阅读和理解的形式的行为。

代码混淆影响到的元素有

混淆的目的

混淆的目的是为了加大反编译的成本,但是并不能彻底防止反编译.

如何开启混淆

一个简单的示例如下

1 2 3 4 
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

Keep用来保留Java的元素不进行混淆. keep有很多变种,他们一般都是

一些例子

保留某个包下面的类以及子包

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

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

1 2 3 4 5 
# Otto -keepclassmembers class ** {  @com.squareup.otto.Subscribe public *;  @com.squareup.otto.Produce public *; } 

保留Contants类的BOOK_NAME属性

1 2 3 
-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的警告,我们可以这样做

1 
-dontwarn com..sdk.** 

其他混淆相关的介绍,都可以通过访问官方文档获取.

哪些不应该混淆

反射中使用的元素

如果一些被混淆使用的元素(属性,方法,类,包名等)进行了混淆,可能会出现问题,如NoSuchFiledException或者NoSuchMethodException等.

比如下面的示例源码

1 2 3 4 5 6 7 8 9 10 11 12 13 14 
//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,该文件保存着混淆前后的映射关系.

1 2 3 4 5 6 7 
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 

从映射文件中,我们可以看到

然后,我们对APK文件进行反编译一探究.推荐一下这个在线反编译工具 http://www.javadecompilers.com/apk

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

1 2 3 4 5 6 7 8 9 10 
package com.example.admin.proguardsample;  /* renamed from: com.example.admin.proguardsample.a */ public class C0314a {  public static String f1712a;   static {  f1712a = "book_name";  } } 

而MainActivity的翻译后的对应的源码为

1 2 3 4 5 
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,用来处理序列化和反序列化

1 2 3 4 
public class Item {  public String name;  public int id; } 

序列化的代码

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

开启混淆之后的日志输出结果

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

属性名已经改变了,变成了没有意思的名称,对我们后续的某些处理是很麻烦的.

反序列化的代码

1 2 3 
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); 

对应的日志结果是

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

可见,混淆之后,反序列化的属性值设置都失败了.

为什么呢?

如何解决

@SerializedName(parameter)通过注解属性实现了

一个简单的用法为

1 2 3 4 5 
public class Item {  @SerializedName("name")  public String name;  @SerializedName("id")  public int id; 

枚举也不要混淆

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

枚举使用起来很简单,如下

1 2 3 4 5 6 7 8 9 
public enum Day {  MONDAY,  TUESDAY,  WEDNESDAY,  THURSDAY,  FRIDAY,  SATURDAY,  SUNDAY } 

这里我们这样使用枚举

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

运行上面的的代码,通常情况下是没有问题的,是否说明枚举就可以混淆呢?

其实不是.

为什么没有问题呢,因为默认的Proguard配置已经处理了枚举相关的keep操作.

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

如果我们手动去掉这条keep配置,再次运行,一个这样的异常会从天而降.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 
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. 这里类里面包含什么呢,看一下反编译的结果

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 
 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)方法

1 2 3 4 5 6 7 
 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工程默认的混淆配置已经包含了下面保留注解的配置

1 
-keepattributes *Annotation* 

关于注解,可以阅读这篇文章了解.详解Java中的注解

其他不该混淆的

stacktrace的恢复

Proguard混淆带来了很多好处,但是也会导致我们收集到的崩溃的stacktrace变得更加难以读懂,好在有补救的措施,这里就介绍一个工具,retrace,用来将混淆后的stacktrace还原成混淆之前的信息.

retrace脚本

Android 开发环境默认带着retrace脚本,一般情况下路径为./tools/proguard/bin/retrace.sh

mapping映射表

Proguard进行混淆之后,会生成一个映射表,文件名为mapping.txt,我们可以使用find工具在Project下查找

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

一个崩溃stacktrace信息

一个原始的崩溃信息是这样的.

1 2 3 4 5 6 7 
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才能正常工作.得到的字符串是

1 2 3 4 5 6 7 
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.

开搞

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

得到的易读的stacktrace是

1 2 3 4 5 6 7 
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属性

1 
-keepattributes SourceFile,LineNumberTable

本站文章版权归原作者及原出处所有 。内容为作者个人观点, 并不代表本站赞同其观点和对其真实性负责,本站只提供参考并不构成任何投资及应用建议。本站是一个个人学习交流的平台,网站上部分文章为转载,并不用于任何商业目的,我们已经尽可能的对作者和来源进行了通告,但是能力有限或疏忽,造成漏登,请及时联系我们,我们将根据著作权人的要求,立即更正或者删除有关内容。本站拥有对此声明的最终解释权。

回到顶部
嘿,我来帮您!