Gson混淆后报AbstractMethodError

作者: cntlb | 来源:发表于2019-07-17 23:49 被阅读4次

    以前的项目中使用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<T> {
      public abstract void write(JsonWriter out, T value) throws IOException;
      public abstract T read(JsonReader in) throws IOException;
    
      public final TypeAdapter<T> nullSafe() {
        return new TypeAdapter<T>() {
          @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对应上几乎和右边没什么两样了, 最关键的一处是混淆后的泛型信息<TT;>被删除了!

    这里混淆后泛型信息被删除,使用的是原始类型,因此子类的实现方法变成没有被任何代码引用的无用代码, 混淆时直接将这些没用代码删除以精简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 <init>() -> <init>
        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 <init>() -> <init>
        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> CLASS = new TypeAdapter<Class>() {
       @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 <init>() -> <init>
        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

        本文链接:https://www.haomeiwen.com/subject/sdsclctx.html