用過 Kotlin
的小夥伴都已經知道 Kotlin
非空檢查寫法超級簡單。可是,處理 json 時,使用 gson 作解析封裝時,你會發現 Kotlin
的非空檢查不是那麼好用。java
先定義一個 json 實體類:json
data class KotlinData(
var testNullable: String?,
val testNooNull: String
)
複製代碼
兩個字段,一個能夠空,一個不能夠空。若是你直接建立這個對象,kt 保證了對非空的檢查和錯誤警告。接着,咱們看看使用 gson 封裝會怎樣。app
val fromJson = Gson().fromJson(
"{\n" +
"\t\"testNullable\":null,\n" +
"\t\"testNooNull\":null\n" +
"\t}"
, KotlinData::class.java
)
assertNotNull(fromJson.testNullable)
複製代碼
上面的代碼結果可以正確封裝 KotlinData
對象, kt 的非空檢查就會欺騙你,而後空指針就找上門來。jvm
若是咱們想要規避這個問題,Gson
就須要稍微修改一下。自定義咱們 kt 的 TypeAdapter
,而後在 Adapter
的 read 方法中進行相關的非空判斷並拋出異常。write 方法就無論了。ide
在 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?
,感受這個 ?
很能說明一切。ui
接下來就是乾貨(C V)時間,如何運用到咱們的 gson 解析封裝中。this
摒棄默認的 Gson()
建立方式,建立咱們自定義的 KotlinAdapterFactory
。google
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\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.**{*;}
複製代碼
好了,又能夠開心の玩耍了。