用过 Kotlin
的小伙伴都已经知道 Kotlin
非空检查写法超级简单。但是,处理 json 时,使用 gson 做解析封装时,你会发现 Kotlin
的非空检查不是那么好用。
先定义一个 json 实体类:
data class KotlinData(
var testNullable: String?,
val testNooNull: String
)
两个字段,一个可以空,一个不可以空。如果你直接创建这个对象,kt 保证了对非空的检查和错误警告。接着,我们看看使用 gson 封装会怎样。
val fromJson = Gson().fromJson(
"{\n" +
"\t\"testNullable\":null,\n" +
"\t\"testNooNull\":null\n" +
"\t}"
, KotlinData::class.java
)
assertNotNull(fromJson.testNullable)
上面的代码结果能够正确封装 KotlinData
对象, kt 的非空检查就会欺骗你,然后空指针就找上门来。
如果我们想要规避这个问题,Gson
就需要稍微修改一下。自定义我们 kt 的 TypeAdapter
,然后在 Adapter
的 read 方法中进行相关的非空判断并抛出异常。write 方法就不管了。
Kotlin 的非空标记
在 kt 的反射包中,提供了 isMarkedNullable
的属性,用于判断对应的 class 是否被标记为可空。
private fun nullCheck(kClass: KClass<KotlinData>) {
try {
kClass.annotations.forEach {
Log.e("KTNullCheck", "annotation:$it")
}
kClass.declaredMemberProperties.forEach { prop ->
prop.isAccessible = true
Log.e("KTNullCheck", "prop:${prop},returnType>>>${prop.returnType}")
val markedNullable = prop.returnType.isMarkedNullable
Log.e("KTNullCheck", "${prop.name} is nullable>>>>>>>>>>>:$markedNullable")
Log.e("KTNullCheck", ">>>>>>>>>>>>>>>>>>>>>>>>>>>>")
}
} catch (e: Exception) {
e.printStackTrace()
}
}
这个方法最后的打印结果为:
com.lovejjfg.proguard E/KTNullCheck: prop:val com.lovejjfg.proguard.model.KotlinData.testNooNull: kotlin.String,returnType>>>kotlin.String
com.lovejjfg.proguard E/KTNullCheck: testNooNull is nullable>>>>>>>>>>>:false
com.lovejjfg.proguard E/KTNullCheck: >>>>>>>>>>>>>>>>>>>>>>>>>>>>
com.lovejjfg.proguard E/KTNullCheck: prop:var com.lovejjfg.proguard.model.KotlinData.kotlin.String?: kotlin.String?,returnType>>>kotlin.String?
com.lovejjfg.proguard E/KTNullCheck: testNullable is nullable>>>>>>>>>>>:true
com.lovejjfg.proguard E/KTNullCheck: >>>>>>>>>>>>>>>>>>>>>>>>>>>>
结果灰常完美,根据打印信息还可以看到,在标记为可空的字段 testNullable
上,其 returnType
为 kotlin.String?
,感觉这个 ?
很能说明一切。
接下来就是干货(C V)时间,如何运用到我们的 gson 解析封装中。
Gson 优化
摒弃默认的 Gson()
创建方式,创建我们自定义的 KotlinAdapterFactory
。
private val defaultGson = GsonBuilder()
.registerTypeAdapterFactory(KotlinAdapterFactory())
.create()
KotlinAdapterFactory
应该只对 kt 对象做非空判断等逻辑,那怎么区分是 kt 还是 Java 对象呢?毕竟最后他们都被转成字节码,脱了衣服,一个样儿。这里又要说到另外一个注解 Metadata
。
Kt 的元数据信息统统保存在这个注解头中。所以判断是否有这个注解,就能知晓是否是 kt 文件。
class KotlinAdapterFactory : TypeAdapterFactory {
private fun Class<*>.isKotlinClass(): Boolean {
return this.declaredAnnotations.any {
// 只关心 kt 类型
it.annotationClass.qualifiedName == "kotlin.Metadata"
}
}
override fun <T : Any> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T>? {
return if (type.rawType.isKotlinClass()) {
val kClass = (type.rawType as Class<*>).kotlin
val delegateAdapter = gson.getDelegateAdapter(this, type)
KotlinAdapter<T>(delegateAdapter, kClass as KClass<T>)
} else {
null
}
}
}
class KotlinAdapter<T : Any>(
private val delegateAdapter: TypeAdapter<T>,
private val kClass: KClass<T>
) : TypeAdapter<T>() {
override fun read(`in`: JsonReader?): T? {
return delegateAdapter.read(`in`)?.apply {
nullCheck(this)
}
}
override fun write(out: JsonWriter?, value: T) {
delegateAdapter.write(out, value)
}
private fun nullCheck(value: T) {
kClass.declaredMemberProperties.forEach { prop ->
prop.isAccessible = true
if (!prop.returnType.isMarkedNullable && prop(value) == null)
throw JsonParseException(
"Field: '${prop.name}' in Class '${kClass.java.name}' is marked nonnull but found null value"
)
}
}
}
接着再添加一个测试代码:
@Test
fun testBuilder() {
val fromJson = GsonBuilder()
.registerTypeAdapterFactory(KotlinAdapterFactory())
.create()
.let {
it.fromJson(json, KotlinData::class.java)
}
assertNotNull(fromJson.testNullable)
}
异常如期而至:
com.google.gson.JsonParseException: Field: 'testNooNull' in Class 'com.lovejjfg.proguard.model.KotlinData' is marked nonnull but found null value
at com.lovejjfg.proguard.gson.KotlinAdapter.nullCheck(KotlinAdapter.kt:35)
at com.lovejjfg.proguard.gson.KotlinAdapter.read(KotlinAdapter.kt:23)
at com.google.gson.Gson.fromJson(Gson.java:927)
好了,Kotlin
对 json
字段的非空检查完成。
如果就这么轻易搞定,那也不辛苦来码这篇文章。
混淆问题
调试的时候,到上面的确都 OK ,结果混淆 release 时,又出现各种问题。首先还是看看最上面 nullCheck(kClass: KClass<KotlinData>)
方法在混淆时候的打印情况。
结果是方法抛出异常:
java.lang.IllegalStateException: No BuiltInsLoader implementation was found.
Please ensure that the META-INF/services/ is not stripped from your application
and that the Java virtual machine is not running under a security manager
在一番 Google 之后,更新混淆文件添加如下:
-keep class kotlin.reflect.jvm.internal.**{*;}
终于,这个方法成功打印出相关信息:
E/KTNullCheck: prop:var com.lovejjfg.proguard.a.a.a: kotlin.String!,returnType>>>kotlin.String!
E/KTNullCheck: a is nullable>>>>>>>>>>>:false
E/KTNullCheck: >>>>>>>>>>>>>>>>>>>>>>>>>>>>
E/KTNullCheck: prop:val com.lovejjfg.proguard.a.a.b: kotlin.String!,returnType>>>kotlin.String!
E/KTNullCheck: b is nullable>>>>>>>>>>>:false
E/KTNullCheck: >>>>>>>>>>>>>>>>>>>>>>>>>>>>
但是,这他么完全就是不正确的啊,所有的字段都成非空类型。kt 这是在开玩笑吗?混淆了至于这样吗?一番冷静之后,必须的思考为什么会这样呢,这个时候就必须反编译看一下 apk 最后生成的文件。
之前说过的 @Metadata 注解居然也被混淆,成了这个样子:
@m(a = {1, 1, 13}, b = {"\u0000(\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0000\n\u0002\u0010\u000e\n\u0002\b\u000b\n\u0002\u0010\u000b\n\u0002\b\u0002\n\u0002\u0010\b\n\u0000\n\u0002\u0010\u0002\n\u0002\b\u0003\b�\b\u0018\u00002\u00020\u0001B\u0017\u0012\b\u0010\u0002\u001a\u0004\u0018\u00010\u0003\u0012\u0006\u0010\u0004\u001a\u00020\u0003¢\u0006\u0002\u0010\u0005J\u000b\u0010\u000b\u001a\u0004\u0018\u00010\u0003HÆ\u0003J\t\u0010\f\u001a\u00020\u0003HÆ\u0003J\u001f\u0010\r\u001a\u00020\u00002\n\b\u0002\u0010\u0002\u001a\u0004\u0018\u00010\u00032\b\b\u0002\u0010\u0004\u001a\u00020\u0003HÆ\u0001J\u0013\u0010\u000e\u001a\u00020\u000f2\b\u0010\u0010\u001a\u0004\u0018\u00010\u0001HÖ\u0003J\t\u0010\u0011\u001a\u00020\u0012HÖ\u0001J\u0010\u0010\u0013\u001a\u00020\u00142\b\u0010\u0015\u001a\u0004\u0018\u00010\u0000J\t\u0010\u0016\u001a\u00020\u0003HÖ\u0001R\u0011\u0010\u0004\u001a\u00020\u0003¢\u0006\b\n\u0000\u001a\u0004\b\u0006\u0010\u0007R\u001c\u0010\u0002\u001a\u0004\u0018\u00010\u0003X�\u000e¢\u0006\u000e\n\u0000\u001a\u0004\b\b\u0010\u0007\"\u0004\b\t\u0010\n¨\u0006\u0017"}, c = {"Lcom/lovejjfg/proguard/model/KotlinData;", "", "testNullable", "", "testNooNull", "(Ljava/lang/String;Ljava/lang/String;)V", "getTestNooNull", "()Ljava/lang/String;", "getTestNullable", "setTestNullable", "(Ljava/lang/String;)V", "component1", "component2", "copy", "equals", "", "other", "hashCode", "", "testData", "", "data", "toString", "app_release"})
// 转码之后
@m(a = {1, 1, 13}, b = {"(\n���\n��\n\n���\n�\b�\n���\n�\b�\n��\b\n\n���\n�\b�\b�\b�2�0�B��\b������0�������0�¢����J�������0�HÆ�J\t�\f��0�HÆ�J��\r��02\n\b�������0�2\b\b�����0�HÆ�J�����0�2\b������0�HÖ�J\t����0�HÖ�J�����0�2\b������0J\t����0�HÖ�R�����0�¢�\b\n��\b���R�������0�X��¢��\n��\b\b��\"�\b\t�\n¨��"}, c = {"Lcom/lovejjfg/proguard/model/KotlinData;", "", "testNullable", "", "testNooNull", "(Ljava/lang/String;Ljava/lang/String;)V", "getTestNooNull", "()Ljava/lang/String;", "getTestNullable", "setTestNullable", "(Ljava/lang/String;)V", "component1", "component2", "copy", "equals", "", "other", "hashCode", "", "testData", "", "data", "toString", "app_release"})
我们对比一下不混淆的注解:
@Metadata(bv = {1, 0, 3}, d1 = {"\u0000(\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0000\n\u0002\u0010\u000e\n\u0002\b\u000b\n\u0002\u0010\u000b\n\u0002\b\u0002\n\u0002\u0010\b\n\u0000\n\u0002\u0010\u0002\n\u0002\b\u0003\b�\b\u0018\u00002\u00020\u0001B\u0017\u0012\b\u0010\u0002\u001a\u0004\u0018\u00010\u0003\u0012\u0006\u0010\u0004\u001a\u00020\u0003¢\u0006\u0002\u0010\u0005J\u000b\u0010\u000b\u001a\u0004\u0018\u00010\u0003HÆ\u0003J\t\u0010\f\u001a\u00020\u0003HÆ\u0003J\u001f\u0010\r\u001a\u00020\u00002\n\b\u0002\u0010\u0002\u001a\u0004\u0018\u00010\u00032\b\b\u0002\u0010\u0004\u001a\u00020\u0003HÆ\u0001J\u0013\u0010\u000e\u001a\u00020\u000f2\b\u0010\u0010\u001a\u0004\u0018\u00010\u0001HÖ\u0003J\t\u0010\u0011\u001a\u00020\u0012HÖ\u0001J\u0010\u0010\u0013\u001a\u00020\u00142\b\u0010\u0015\u001a\u0004\u0018\u00010\u0000J\t\u0010\u0016\u001a\u00020\u0003HÖ\u0001R\u0011\u0010\u0004\u001a\u00020\u0003¢\u0006\b\n\u0000\u001a\u0004\b\u0006\u0010\u0007R\u001c\u0010\u0002\u001a\u0004\u0018\u00010\u0003X�\u000e¢\u0006\u000e\n\u0000\u001a\u0004\b\b\u0010\u0007\"\u0004\b\t\u0010\n¨\u0006\u0017"}, d2 = {"Lcom/lovejjfg/proguard/model/KotlinData;", "", "testNullable", "", "testNooNull", "(Ljava/lang/String;Ljava/lang/String;)V", "getTestNooNull", "()Ljava/lang/String;", "getTestNullable", "setTestNullable", "(Ljava/lang/String;)V", "component1", "component2", "copy", "equals", "", "other", "hashCode", "", "testData", "", "data", "toString", "app_debug"}, k = 1, mv = {1, 1, 13})
// 转码之后
@Metadata(bv = {1, 0, 3}, d1 = {"(\n���\n��\n\n���\n�\b�\n���\n�\b�\n��\b\n\n���\n�\b�\b�\b�2�0�B��\b������0�������0�¢����J�������0�HÆ�J\t�\f��0�HÆ�J��\r��02\n\b�������0�2\b\b�����0�HÆ�J�����0�2\b������0�HÖ�J\t����0�HÖ�J�����0�2\b������0J\t����0�HÖ�R�����0�¢�\b\n��\b���R�������0�X��¢��\n��\b\b��\"�\b\t�\n¨��"}, d2 = {"Lcom/lovejjfg/proguard/model/KotlinData;", "", "testNullable", "", "testNooNull", "(Ljava/lang/String;Ljava/lang/String;)V", "getTestNooNull", "()Ljava/lang/String;", "getTestNullable", "setTestNullable", "(Ljava/lang/String;)V", "component1", "component2", "copy", "equals", "", "other", "hashCode", "", "testData", "", "data", "toString", "app_debug"}, k = 1, mv = {1, 1, 13})
默认的混淆之后, @Metadata
这个注解也被混淆了,所以,我们之前的 Kotlin
类型判断将失效。要解决这个问题,那就得把这个注解给保持住,最后的最后,还要注意,元数据中的字段等信息是没有被混淆的信息,所以,我们也应该保证 data 中每个字段不被混淆。
如果有对应的 model 没有被 keep ,app 会直接挂掉:
kotlin.reflect.jvm.internal.KotlinReflectionInternalError:
No accessors or field is found for property val com.lovejjfg.proguard.a.KotlinData.testNooNull: kotlin.String
总的来说,在处理混淆是需要添加如下混淆规则:
-keep class kotlin.reflect.jvm.internal.**{*;}
-keep class kotlin.Metadata { *; }
# 所有需要走 gson 封装的 model 实体类需要保证 membername 不混淆 这里请根据实际情况制定自己的规则
-keepclassmembernames class com.lovejjfg.proguard.model.**{*;}
好了,又可以开心の玩耍了。