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<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
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,470评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,393评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,577评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,176评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,189评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,155评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,041评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,903评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,319评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,539评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,703评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,417评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,013评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,664评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,818评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,711评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,601评论 2 353

推荐阅读更多精彩内容