Gson混淆后报AbstractMethodError

以前的项目中使用Gson没有直接用到JsonAdapter(指TypeAdapter,TypeAdapterFactory, JsonSerializer, JsonDeserializer子类或实现类), 但Gson内置有多种类型的TypeAdapter在解析时生效。当使用JsonAdapter并开启混淆运行后抛出莫名其妙的异常:

     Caused by: java.lang.AbstractMethodError: abstract method "java.lang.Object c.c.b.J.a(c.c.b.d.b)"
        at c.c.b.I.a(SourceFile:5)

以前项目也有开启混淆代码跑起来都很正常,可能和kotlin的使用有关(下文进行验证,读者也可以尝试进行验证)。定位到mapping.txt中:

com.google.gson.stream.JsonReader -> c.c.b.d.b:
com.google.gson.TypeAdapter -> c.c.b.J:
    java.lang.Object read(com.google.gson.stream.JsonReader) -> a
com.google.gson.TypeAdapter$1 -> c.c.b.I:
    5:5:java.lang.Object read(com.google.gson.stream.JsonReader):199:199 -> a

结合异常信息说明调用的TypeAdapter.read(JsonReader):Object是一个抽象方法。

又见TypeAdapter

为什么说"又"
使用了TypeAdapter, 先看下这个类的几个方法:

public abstract class TypeAdapter {
  public abstract void write(JsonWriter out, T value) throws IOException;
  public abstract T read(JsonReader in) throws IOException;

  public final TypeAdapter nullSafe() {
    return new TypeAdapter() {
      @Override public void write(JsonWriter out, T value) throws IOException {
        if (value == null) {
          out.nullValue();
        } else {
          TypeAdapter.this.write(out, value);
        }
      }
      @Override public T read(JsonReader reader) throws IOException {
        if (reader.peek() == JsonToken.NULL) {
          reader.nextNull();
          return null;
        }
        return TypeAdapter.this.read(reader);
      }
    };
  }
}

TypeAdapter源码中唯一一个内部类就在nullSafe()中产生, 这是一个局部内部类, read最后调用return TypeAdapter.this.read(reader); , 联系到java的泛型具有类型擦除的性质(参考笔者另一篇文章 关于Gson的TypeToken ), 局部类TypeAdapter$1的类型是擦除的. 假如有TypeAdapter的实现类, 类型擦除后调用原始版本的Object read(JsonReader)是可以说得通的. 反编译这个内部类的read方法这一行的调用:

invoke-virtual {v0, p1}, Lc/c/b/J;->a(Lc/c/b/d/b;)Ljava/lang/Object;

果然调用的是Object read(JsonReader)的方法. 同时该方法也是virtual的, 而java支持多态, 因此最终调用的还是实现类的方法, 这里看不出有什么问题, 合乎情理. 不过我们还是对比下混淆前后的代码:

混淆前后nullSafe()对比

左边没有混淆, 如果将mapping对应上几乎和右边没什么两样了, 最关键的一处是混淆后的泛型信息被删除了!

这里混淆后泛型信息被删除,使用的是原始类型,因此子类的实现方法变成没有被任何代码引用的无用代码, 混淆时直接将这些没用代码删除以精简apk.

没用混淆时泛型信息保留着可以引用到这些实现的方法, 而且这些方法也没有被删除掉, 可以正常运行.

定制混淆规则(proguard rule)

既然问题在nullSafe()中, 不混淆这个方法可以确保泛型信息保留:

-keepclassmembers class com.google.gson.TypeAdapter{
    nullSafe();
}

泛型保住了, 而TypeAdapter子类的实现方法依然被无情的删除了, 拿BooleanAdapter(参看 Gson序列化那些事 )这个子类来对比下混淆前后

BooleanAdapter混淆前

BooleanAdapter混淆后

可以看到混淆后依然是继承了TypeAdapter(混淆后是super Lc/c/b/J;), 因此有两个抽象方法, 但是我们自己写的两个实现方法却被删除了(上面的截图就是该类的所有汇编代码, 没有截断), 所以子类中只剩下类的签名信息了. 根据java的多态性, 保留我们实现的方法应该就可以正常运行了, 于是在混淆文件中加入以下代码:

-keepclassmembers class * extends com.google.gson.TypeAdapter {*;}

TypeAdapter子类的成员都保留下来(类名允许混淆了). 这是再看下打包后的BooleanAdapter

BooleanAdapter混淆后

我感觉很好, 和没有混淆的差不多。

赶紧跑代码看看, 出现一个类似的异常, 这次换成是JsonDeserializer类(项目中也有用到, 类似的方法再保留成员), 可以正常运行了.

gson.pro

综上所述, 对于gson如果使用了JsonAdapter, 应该添加混淆选项:

-keep class * extends com.google.gson.TypeAdapter
-keep class * implements com.google.gson.TypeAdapterFactory
-keep class * implements com.google.gson.JsonSerializer
-keep class * implements com.google.gson.JsonDeserializer

或者

-keepclassmembers class * extends com.google.gson.TypeAdapter {*;}
-keepclassmembers class * implements com.google.gson.TypeAdapterFactory {*;}
-keepclassmembers class * implements com.google.gson.JsonSerializer {*;}
-keepclassmembers class * implements com.google.gson.JsonDeserializer {*;}

后者混淆的程度更高.

nullSafe()方式

为了验证混淆后实现方法被删除与kotlin和java这两种语言是否有关,这里编写了BooleanAdapter的java版本BooleanAdapterJ, 看下混淆的结果:

com.common.entity.BooleanAdapter -> c.e.b.a.b:
com.common.entity.BooleanAdapterJ -> c.e.b.a.c:
com.google.gson.TypeAdapter -> c.c.b.J:
    119:119:void () -> 
    java.lang.Object read(com.google.gson.stream.JsonReader) -> a
    void write(com.google.gson.stream.JsonWriter,java.lang.Object) -> a
    186:186:com.google.gson.TypeAdapter nullSafe() -> a
    233:237:com.google.gson.JsonElement toJsonTree(java.lang.Object) -> a
com.google.gson.internal.bind.TypeAdapters$1 -> c.c.b.b.a.H:
    69:69:void () -> 
    69:69:java.lang.Object read(com.google.gson.stream.JsonReader) -> a
    69:69:void write(com.google.gson.stream.JsonWriter,java.lang.Object) -> a
    72:73:void write(com.google.gson.stream.JsonWriter,java.lang.Class) -> a
    77:77:java.lang.Class read(com.google.gson.stream.JsonReader) -> a

上面贴出来的是mapping.txt中所有关于这两个类的混淆结果。

作为参照把TypeAdapter和gson中一个已写好的实现类的混淆结果也贴了出来。TypeAdapters中内置了很多常用类型的适配器,TypeAdapters$1是第一个内部类(gson-2.8.5)。

public final class TypeAdapters {
 @SuppressWarnings("rawtypes")
 public static final TypeAdapter CLASS = new TypeAdapter() {
   @Override
   public void write(JsonWriter out, Class value) throws IOException {
     throw UnsupportedOperationException
   }
   @Override
   public Class read(JsonReader in) throws IOException {
     throw UnsupportedOperationException
  }
}.nullSafe();

可以看出笔者写的两个类均没能将实现的方法保留下来, 而自带的TypeAdapters$1保留下来了。依样画葫芦, 我们也使用nullSafe()的方式来注册一个看效果.

GsonBuilder()
            ...
            .registerTypeAdapter(Boolean::class.java, BooleanAdapter().nullSafe())
            .create()

再看下混淆结果:

com.common.entity.BooleanAdapter -> c.e.b.a.b:
    19:19:void () -> 
    19:19:java.lang.Object read(com.google.gson.stream.JsonReader) -> a
    19:19:void write(com.google.gson.stream.JsonWriter,java.lang.Object) -> a
    21:25:void write(com.google.gson.stream.JsonWriter,java.lang.Boolean) -> a
    28:32:java.lang.Boolean read(com.google.gson.stream.JsonReader) -> a

惊不惊喜,意不意外! 其实只要代码中有通过nullSafe()的调用使得实现的方法被引用到就可以保留下来。所以使用nullSafe()的方式来避免混淆问题也是行得通的。是选择写混淆规则还是nullSafe()方式,这就要看你是否接受nullSafe()中的默认处理方式。

TypeAdapters.nullSafe()中的泛型信息顺便也保留下来了。

参考

Gson序列化那些事
关于Gson的TypeToken
比对合并工具meld

彩蛋

Android Studio这个功能强大的IDE居然可以直接将apk反编译成汇编码,省了不少事!一起来体验下吧。

  1. as中双击apk文件或者将apk拖到as,选中一个类单击右键
    单击右键
  2. Show Bytecode


    Show Bytecode
  3. Find Usages


    Find Usages
  4. Generate Proguard keep rule


    Generate Proguard keep rule

你可能感兴趣的:(Gson混淆后报AbstractMethodError)