用過 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.**{*;}
好了,又可以開心の玩耍了。