以前的项目中使用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支持多态, 因此最终调用的还是实现类的方法, 这里看不出有什么问题, 合乎情理. 不过我们还是对比下混淆前后的代码:
左边没有混淆, 如果将mapping对应上几乎和右边没什么两样了, 最关键的一处是混淆后的泛型信息
<TT;>
被删除了!
这里混淆后泛型信息被删除,使用的是原始类型,因此子类的实现方法变成没有被任何代码引用的无用代码, 混淆时直接将这些没用代码删除以精简apk.
没用混淆时泛型信息保留着可以引用到这些实现的方法, 而且这些方法也没有被删除掉, 可以正常运行.
定制混淆规则(proguard rule)
既然问题在nullSafe()
中, 不混淆这个方法可以确保泛型信息保留:
-keepclassmembers class com.google.gson.TypeAdapter{
nullSafe();
}
泛型保住了, 而TypeAdapter
子类的实现方法依然被无情的删除了, 拿BooleanAdapter
(参看 Gson序列化那些事 )这个子类来对比下混淆前后
可以看到混淆后依然是继承了
TypeAdapter(混淆后是super Lc/c/b/J;)
, 因此有两个抽象方法, 但是我们自己写的两个实现方法却被删除了(上面的截图就是该类的所有汇编代码, 没有截断), 所以子类中只剩下类的签名信息了. 根据java的多态性, 保留我们实现的方法应该就可以正常运行了, 于是在混淆文件中加入以下代码:
-keepclassmembers class * extends com.google.gson.TypeAdapter {*;}
将TypeAdapter
子类的成员都保留下来(类名允许混淆了). 这是再看下打包后的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反编译成汇编码,省了不少事!一起来体验下吧。
- as中双击apk文件或者将apk拖到as,选中一个类
单击右键
单击右键 -
Show Bytecode
Show Bytecode -
Find Usages
Find Usages -
Generate Proguard keep rule
Generate Proguard keep rule
网友评论