用过 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 上,其 returnTypekotlin.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)
复制代码

好了,Kotlinjson 字段的非空检查完成。


如果就这么轻易搞定,那也不辛苦来码这篇文章。

混淆问题

调试的时候,到上面的确都 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\b20B\b00¢J0HÆJ\t\f0HÆJ\r02\n\b02\b\b0HÆJ02\b0HÖJ\t0HÖJ02\b0J\t0HÖR0¢\b\n\bR0X¢\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\b20B\b00¢J0HÆJ\t\f0HÆJ\r02\n\b02\b\b0HÆJ02\b0HÖJ\t0HÖJ02\b0J\t0HÖR0¢\b\n\bR0X¢\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.**{*;}
复制代码

好了,又可以开心の玩耍了。