读懂 Android 中的代码混淆

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

在编程语言中我们,都会接触到枚举类型,通常我们进行有穷的列举来实现一些限定。Java也不例外。Java中的枚举类型为Enum,本文将对枚举进行一些比较深入的剖析。

  枚举是如何保证线程安全的且其在序列化和反序列化的操作中是单例的?

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

什么是Enum

Enum是自Java 5
引入的特性,用来方便Java开发者实现枚举应用。一个简单的Enum使用如下。

// ColorEnum.java
public enum ColorEmun {
    RED,
    GREEN,
    YELLOW
}

public void setColorEnum(ColorEmun colorEnum) {
    //some code here
}

setColorEnum(ColorEmun.GREEN);

  要想看源码,首先得有一个类吧,那么枚举类型到底是什么类呢?是enum吗?答案很明显不是,enum就和class一样,只是一个关键字,他并不是一个类,那么枚举是由什么类维护的呢,我们简单的写一个枚举:

什么是混淆

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

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

代码混淆影响到的元素有

  • 类名
  • 变量名
  • 方法名
  • 包名
  • 其他元素

为什么会有Enum

在Enum之前的我们使用类似如下的代码实现枚举的功能.

public static final int COLOR_RED = 0;
public static final int COLOR_GREEN = 1;
public static final int COLOR_YELLOW = 2;

public void setColor(int color) {
    //some code here
}
//调用
setColor(COLOR_RED)

然而上面的还是有不尽完美的地方

  • setColor(COLOR_RED)与setColor(0)效果一样,而后者可读性很差,但却可以正常运行
  • setColor方法可以接受枚举之外的值,比如setColor(3),这种情况下程序可能出问题

概括而言,传统枚举有如下两个弊端

  • 安全性
  • 可读性,尤其是打印日志时

因此Java引入了Enum,使用Enum,我们实现上面的枚举就很简单了,而且还可以轻松避免传入非法值的风险.

public enum T {
    SPRING,SUMMER,AUTUMN,WINTER;
}

混淆的目的

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

枚举原理是什么

Java中Enum的本质其实是在编译时期转换成对应的类的形式。

首先,为了探究枚举的原理,我们先简单定义一个枚举类,这里以季节为例,类名为Season,包含春夏秋冬四个枚举条目.

public enum Season {
    SPRING,
    SUMMER,
    AUTUMN,
    WINTER
}

然后我们使用javac编译上面的类,得到class文件.

javac Season.java

然后,我们利用反编译的方法来看看字节码文件究竟是什么.这里使用的工具是javap的简单命令,先列举一下这个Season下的全部元素.

company javap Season
Warning: Binary file Season contains com.company.Season
Compiled from "Season.java"
public final class com.company.Season extends java.lang.Enum<com.company.Season> {
  public static final com.company.Season SPRING;
  public static final com.company.Season SUMMER;
  public static final com.company.Season AUTUMN;
  public static final com.company.Season WINTER;
  public static com.company.Season[] values();
  public static com.company.Season valueOf(java.lang.String);
  static {};
}

从上反编译结果可知

  • java代码中的Season转换成了继承自的java.lang.enum的类
  • 既然隐式继承自java.lang.enum,也就意味java代码中,Season不能再继承其他的类
  • Season被标记成了final,意味着它不能被继承

然后我们使用反编译,看看这段代码到底是怎么实现的,反编译(Java的反编译)后代码内容如下:

如何开启混淆

  • 通常我们需要找到项目路径下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是一个集文件压缩,优化,混淆和校验等功能的工具
  • 它检测并删除无用的类,变量,方法和属性
  • 它优化字节码并删除无用的指令.
  • 它通过将类名,变量名和方法名重命名为无意义的名称实现混淆效果.
  • 最后它还校验处理后的代码

static代码块

使用javap具体反编译class文件,得到静态代码块相关的结果为

static {};
    Code:
       0: new           #4                  // class com/company/Season
       3: dup
       4: ldc           #7                  // String SPRING
       6: iconst_0
       7: invokespecial #8                  // Method "<init>":(Ljava/lang/String;I)V
      10: putstatic     #9                  // Field SPRING:Lcom/company/Season;
      13: new           #4                  // class com/company/Season
      16: dup
      17: ldc           #10                 // String SUMMER
      19: iconst_1
      20: invokespecial #8                  // Method "<init>":(Ljava/lang/String;I)V
      23: putstatic     #11                 // Field SUMMER:Lcom/company/Season;
      26: new           #4                  // class com/company/Season
      29: dup
      30: ldc           #12                 // String AUTUMN
      32: iconst_2
      33: invokespecial #8                  // Method "<init>":(Ljava/lang/String;I)V
      36: putstatic     #13                 // Field AUTUMN:Lcom/company/Season;
      39: new           #4                  // class com/company/Season
      42: dup
      43: ldc           #14                 // String WINTER
      45: iconst_3
      46: invokespecial #8                  // Method "<init>":(Ljava/lang/String;I)V
      49: putstatic     #15                 // Field WINTER:Lcom/company/Season;
      52: iconst_4
      53: anewarray     #4                  // class com/company/Season
      56: dup
      57: iconst_0
      58: getstatic     #9                  // Field SPRING:Lcom/company/Season;
      61: aastore
      62: dup
      63: iconst_1
      64: getstatic     #11                 // Field SUMMER:Lcom/company/Season;
      67: aastore
      68: dup
      69: iconst_2
      70: getstatic     #13                 // Field AUTUMN:Lcom/company/Season;
      73: aastore
      74: dup
      75: iconst_3
      76: getstatic     #15                 // Field WINTER:Lcom/company/Season;
      79: aastore
      80: putstatic     #1                  // Field $VALUES:[Lcom/company/Season;
      83: return
}

其中

  • 0~52为实例化SPRING, SUMMER, AUTUMN, WINTER
  • 53~83为创建Season[]数组$VALUES,并将上面的四个对象放入数组的操作.
public final class T extends Enum
{
    private T(String s, int i)
    {
        super(s, i);
    }
    public static T[] values()
    {
        return (T[])$VALUES.clone();
    }

    public static T valueOf(String s)
    {
        return (T)Enum.valueOf(T, s);
    }

    public static final T SPRING;
    public static final T SUMMER;
    public static final T AUTUMN;
    public static final T WINTER;
    private static final T $VALUES[];
    static
    {
        SPRING = new T("SPRING", 0);
        SUMMER = new T("SUMMER", 1);
        AUTUMN = new T("AUTUMN", 2);
        WINTER = new T("WINTER", 3);
        $VALUES = (new T[] {
            SPRING, SUMMER, AUTUMN, WINTER
        });
    }
}

混淆的常见配置

values方法

values方法的的返回值实际上就是上面$VALUES数组对象

通过反编译后代码我们可以看到,public final class T extends
Enum,说明,该类是继承了Enum类的,同时final关键字告诉我们,这个类也是不能被继承的。当我们使用enmu来定义一个枚举类型的时候,编译器会自动帮我们创建一个final类型的类继承Enum类,所以枚举类型不能被继承,我们看到这个类中有几个属性和方法。
我们可以看到:

-keep

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

  • -keep
  • -keepclassmembers
  • -keepclasseswithmembers

澳门新葡萄京官网注册 ,swtich中的枚举

在Java中,switch-case是我们经常使用的流程控制语句.当枚举出来之后,switch-case也很好的进行了支持.

比如下面的代码是完全正常编译,正常运行的.

public static void main(String[] args) {
        Season season = Season.SPRING;
        switch(season) {
            case SPRING:
                System.out.println("It's Spring");
                break;

            case WINTER:
                System.out.println("It's Winter");
                break;

            case SUMMER:
                System.out.println("It's Summer");
                break;
            case AUTUMN:
                System.out.println("It's Autumn");
                break;
        }
    }

不过,通常情况下switch-case支持类似int的类型,那么它是怎么做到对Enum的支持呢,我们反编译上述方法看一下字节码的真实情况.

public static void main(java.lang.String[]);
    Code:
       0: getstatic     #2                  // Field com/company/Season.SPRING:Lcom/company/Season;
       3: astore_1
       4: getstatic     #3                  // Field com/company/Main$1.$SwitchMap$com$company$Season:[I
       7: aload_1
       8: invokevirtual #4                  // Method com/company/Season.ordinal:()I
      11: iaload
      12: tableswitch   { // 1 to 4
                     1: 44
                     2: 55
                     3: 66
                     4: 77
               default: 85
          }
      44: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
      47: ldc           #6                  // String It's Spring
      49: invokevirtual #7                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      52: goto          85
      55: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
      58: ldc           #8                  // String It's Winter
      60: invokevirtual #7                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      63: goto          85
      66: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
      69: ldc           #9                  // String It's Summer
      71: invokevirtual #7                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      74: goto          85
      77: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
      80: ldc           #10                 // String It's Autumn
      82: invokevirtual #7                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      88: return

注意上面代码块有这样的一段代码

8: invokevirtual #4                  // Method com/company/Season.ordinal:()I

事实果真如此,在switch-case中,还是将Enum转成了int值(通过调用Enum.oridinal()方法)

public static final T SPRING;
public static final T SUMMER;
public static final T AUTUMN;
public static final T WINTER;
private static final T $VALUES[];
static
{
    SPRING = new T("SPRING", 0);
    SUMMER = new T("SUMMER", 1);
    AUTUMN = new T("AUTUMN", 2);
    WINTER = new T("WINTER", 3);
    $VALUES = (new T[] {
        SPRING, SUMMER, AUTUMN, WINTER
    });
}

一些例子

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

-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使用,可以参考官方文档

枚举与混淆

在Android开发中,进行混淆是我们在发布前必不可少的工作,混下后,我们能增强反编译的难度,在一定程度上保护了增强了安全性.

而开发人员处理混淆更多的是将某些元素加入不混淆的名单,这里枚举就是需要排除混淆的.

在默认的混淆配置文件中,已经加入了关于对枚举混淆的处理

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

关于为什么要保留values()方法和valueOf()方法,请参考文章读懂 Android
中的代码混淆 关于枚举的部分

都是static类型的,因为static类型的属性会在类被加载之后被初始化,当一个Java类第一次被真正使用到的时候静态资源被初始化、Java类的加载和初始化过程都是线程安全的。所以,创建一个enum类型是线程安全的。

-dontwarn

dontwarn是一个和keep可以说是形影不离,尤其是处理引入的library时.

引入的library可能存在一些无法找到的引用和其他问题,在build时可能会发出警告,如果我们不进行处理,通常会导致build中止.因此为了保证build继续,我们需要使用dontwarn处理这些我们无法解决的library的警告.

比如关闭Twitter sdk的警告,我们可以这样做

-dontwarn com.twitter.sdk.**

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

使用proguard优化

使用Proguard进行优化,可以将枚举尽可能的转换成int。配置如下

-optimizations class/unboxing/enum

确保上述代码生效,需要确proguard配置文件不包含-dontoptimize指令。

当我们使用gradlew打包是,看到类似下面的输出,即Number of unboxed enum classes:1代表已经将一个枚举转换成了int的形式。

Optimizing...
  Number of finalized classes:                 0   (disabled)
  Number of unboxed enum classes:              1
  Number of vertically merged classes:         0   (disabled)
  Number of horizontally merged classes:       0   (disabled)

为什么用枚举实现的单例是最好的方式

哪些不应该混淆

枚举单例

单例模式是我们在日常开发中可谓是最常用的设计模式.

然后要设计好单例模式,无非考虑一下几点

  • 确保只有唯一实例,不多创建多余实例
  • 确保实例按需创建.

因此传统的做法想要实现单例,大致有一下几种

  • 饿汉式加载
  • 懒汉式synchronize和双重检查
  • 利用java的静态加载机制

相比上述的方法,使用枚举也可以实现单例,而且还更加简单.

public enum AppManager {
    INSTANCE;

    private String tagName;
    public void setTag(String tagName) {
        this.tagName = tagName;
    }

    public String getTag() {
        return tagName;
    }
}

调用起来也更加简单

AppManager.INSTANCE.getTag();

在单例模式的七种写法中,我们看到一共有七种实现单例的方式,其中,Effective
Java作者Josh
Bloch提倡使用枚举的方式,既然大神说这种方式好,那我们就要知道它为什么好?

反射中使用的元素

如果一些被混淆使用的元素(属性,方法,类,包名等)进行了混淆,可能会出现问题,如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文件进行反编译一探究竟.推荐一下这个在线反编译工具 

注意,使用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的引用,已经在调用出替换成了混淆后的名称。

枚举如何确保唯一实例

因为获得实例只能通过AppManager.INSTANCE

下面的方式是不可以的

AppManager appManager = new AppManager(); //compile error

关于单例模式,可以阅读单例这种设计模式了解更多。

1. 枚举写法简单

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

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

(Android中)该不该用枚举

既然上面提到了枚举会转换成类,这样理论上造成了下面的问题

  • 增加了dex包的大小,理论上dex包越大,加载速度越慢
  • 同时使用枚举,运行时的内存占用也会相对变大

关于上面两点的验证,秋百万已经做了详细的论证,大家可以参考这篇文章《Android
中的 Enum
到底占多少内存?该如何用?》

关于枚举是否使用的结论,大家可以参考

  • 如果你开发的是Framework不建议使用enum
  • 如果是简单的enum,可以使用int很轻松代替,则不建议使用enum
  • 另外,如果是Android中,可以使用下面介绍的枚举注解来实现。
  • 除此之外,我们还需要对比可读性和易维护性来与性能进行衡量,从中进行做出折中

  写法简单这个大家看看单例模式的七种写法里面的实现就知道区别了。

为什么呢?

  • 因为反序列化创建对象本质还是利用反射,会根据json字符串的key作为属性名称,value则对应属性值.

在Android中的替代

Android中新引入的替代枚举的注解有IntDef和StringDef,这里以IntDef做例子说明一下.

public class Colors {
    @IntDef({RED, GREEN, YELLOW})
    @Retention(RetentionPolicy.SOURCE)
    public @interface LightColors{}

    public static final int RED = 0;
    public static final int GREEN = 1;
    public static final int YELLOW = 2;
}
  • 声明必要的int常量
  • 声明一个注解为LightColors
  • 使用@IntDef修饰LightColors,参数设置为待枚举的集合
  • 使用@Retention(RetentionPolicy.SOURCE)指定注解仅存在与源码中,不加入到class文件中

比如我们用来标注方法的参数

private void setColor(@Colors.LightColors int color) {
        Log.d("MainActivity", "setColor color=" + color);
}

调用的该方法的时候

setColor(Colors.GREEN);

关于Android中的枚举,可以参考探究Android中的注解

以上就是我对Java中enum的一些深入的剖析,欢迎大家不吝赐教。

public enum EasySingleton{
    INSTANCE;
}

如何解决

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

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

  • 序列化的结果中,指定该属性key为parameter的值.
  • 反序列化生成的对象中,用来匹配key与parameter并赋予属性值.

一个简单的用法为

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

你可以通过EasySingleton.INSTANCE来访问。

枚举也不要混淆

枚举是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.

关于调用轨迹,感兴趣的可以自己研究一下源码,不难.

2. 枚举自己处理序列化

四大组件不建议混淆

Android中四大组件我们都很常用,这些组件不能被混淆的原因为

  • 四大组件声明必须在manifest中注册,如果混淆后类名更改,而混淆后的类名没有在manifest注册,是不符合Android组件注册机制的.
  • 外部程序可能使用组件的字符串类名,如果类名混淆,可能导致出现异常

  我们知道,以前的所有的单例模式都有一个比较大的问题,就是一旦实现了Serializable接口之后,就不再是单例得了,因为,每次调用readObject()方法返回的都是一个新创建出来的对象,有一种解决办法就是使用readResolve()方法来避免此事发生。但是,为了保证枚举类型像Java规范中所说的那样,每一个枚举类型极其定义的枚举变量在JVM中都是唯一的,在枚举类型的序列化和反序列化上,Java做了特殊的规定。原文如下:

注解不能混淆

注解在Android平台中使用的越来越多,常用的有ButterKnife和Otto.很多场景下注解被用作在运行时反射确定一些元素的特征.

为了保证注解正常工作,我们不应该对注解进行混淆.Android工程默认的混淆配置已经包含了下面保留注解的配置

-keepattributes *Annotation*

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

  Enum constants are serialized differently than ordinary serializable
or externalizable objects. The serialized form of an enum constant
consists solely of its name; field values of the constant are not
present in the form. To serialize an enum constant, ObjectOutputStream
writes the value returned by the enum constant’s name method. To
deserialize an enum constant, ObjectInputStream reads the constant name
from the stream; the deserialized constant is then obtained by calling
the java.lang.Enum.valueOf method, passing the constant’s enum type
along with the received constant name as arguments. Like other
serializable or externalizable objects, enum constants can function as
the targets of back references appearing subsequently in the
serialization stream. The process by which enum constants are serialized
cannot be customized: any class-specific writeObject, readObject,
readObjectNoData, writeReplace, and readResolve methods defined by enum
types are ignored during serialization and deserialization. Similarly,
any serialPersistentFields or serialVersionUID field declarations are
also ignored–all enum types have a fixedserialVersionUID of 0L.
Documenting serializable fields and data for enum types is unnecessary,
since there is no variation in the type of data sent.

其他不该混淆的

  • jni调用的java方法
  • java的native方法
  • js调用java的方法
  • 第三方库不建议混淆
  • 其他和反射相关的一些情况

翻译如下(限于本人的英文水平,翻译不好的地方请见谅):
枚举常数的序列化方式不同于普通Serializable或者Externalizable对象的序列化(相关博文:K:java中序列化的两种方式—Serializable或Externalizable)。枚举常数的序列化表单仅由其name属性构成;常数的字段值不存在于表单中。在序列化一个枚举常量时,ObjectOutputStream对象会写入由枚举常量的name方法所返回的值(一般也就是name属性的值,也就是枚举常量的名称)。在反序列化枚举常量时,通过ObjectInputStream对象从相关的流中读取枚举常量的name属性值(也就是枚举常量的名称),然后通过调用java.lang.Enum对象的valueOf方法,将从流中获取的枚举常量的枚举类型(指的是继承了Enum类的相关的子类)及其常量名称(也就是枚举常量的名称)一起作为该方法的参数传递给该valueOf方法。像其它Serializable或者Externalizable对象一样,枚举常量可以作为随后出现的序列化流的反向引用。枚举常量被序列化的过程无法自定义:在序列化和反序列化期间将忽略由枚举类型定义的任何特定于该类的writeObject,readObject,readObjectNoData,writeReplace和readResolve方法。类似地,任何serialPersistentFields或serialVersionUID字段声明也被将被忽略。所有的枚举类型的fixedserialVersionUID值都是0L。记录枚举类型的可序列化字段其相关的数据是不必要的,因为发送的数据类型并没有发生变化。
以下代码演示说明枚举常量的序列化只保存了其枚举常量的name属性值

stacktrace的恢复

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

示例代码:

retrace脚本

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

package other.serial;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

/**
 * 该类用于测试枚举类的序列化是不是只是序列化了name属性的值,也就是枚举常量的常量名
 * 
 * @author 学徒
 * 
 */
public enum AS implements Serializable
{
    H("sss");
    private String names;

    private AS(String names)
    {
        this.names = names;
    }

    public void setNames(String names)
    {
        this.names = names;
    }

    public String getNames()
    {
        return this.names;
    }
}

class AB
{
    public static void main(String args[]) throws Exception
    {
        // 设置枚举对象的names属性值为as
        AS.H.setNames("as");
        System.out.println("原始枚举对象的属性值为" + AS.H.getNames());
        // 对枚举对象进行序列化,如果其还序列化了相关的域值,则其应该保留了names属性域的值为as
        new ObjectOutputStream(new FileOutputStream("H:\as.txt"))
                .writeObject(AS.H);
        System.out.println("序列化成功");
        // 修改枚举对象中的names属性域的属性值
        AS.H.setNames("SS");
        // 对已序列化的对象进行反序列化处理,若其还序列化了枚举对象的相关的域值,则其反序列化出来的names的属性值应当为as
        AS a = (AS) new ObjectInputStream(new FileInputStream("H:\as.txt"))
                .readObject();
        System.out.println("反序列化成功");
        // 通过输出结果判断
        System.out.println("反序列化的对象的值" + a.getNames());
        System.out.println(a == AS.H);
    }
}


输出结果:
原始枚举对象的属性值为as
序列化成功
反序列化成功
反序列化的对象的值SS
true

mapping映射表

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

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

  通过分析其相应的输出结果可以看出枚举类型对象在序列化的时候,枚举常数的序列化仅序列化了其name属性值,也就是枚举常量名称

一个崩溃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

关于混淆,我的一些个人经验总结就是这些.希望可以对大家有所帮助.

  概括起来就是说,在序列化的时候Java仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过java.lang.Enum的valueOf方法来根据名字查找枚举对象。同时,编译器是不允许任何对这种序列化机制的定制的,因此禁用了writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法。

我们看一下这个valueOf方法:

public static <T extends Enum<T>>T valueOf(Class<T> enumType,String name) 
{  
    T result = enumType.enumConstantDirectory().get(name);  
    if (result != null)  
        return result;  
    if (name == null)  
        throw new NullPointerException("Name is null");  
    throw new IllegalArgumentException(  "No enum const " + enumType +"." + name);  
}

  从代码中可以看到,代码会尝试从调用enumType这个Class对象的enumConstantDirectory()方法返回的map中获取名字为name的枚举对象,如果不存在就会抛出异常。再进一步跟到enumConstantDirectory()方法,就会发现到最后会以反射的方式调用enumType这个类型的values()静态方法,也就是上面我们看到的编译器为我们创建的那个方法,然后用返回结果填充enumType这个Class对象中的enumConstantDirectory属性。所以,JVM对序列化有保证。

回到目录|·(工)·)

发表评论

电子邮件地址不会被公开。 必填项已用*标注