Kotlin Upgrade to 1.6.0,NullPointerException in kotin-android-extensions

450 阅读3分钟

问题背景及现象

在历史项目中,开发者为避免编写大量的findViewById,引入了kotlin-android-extensions插件,该插件可以直接用布局中声明的控件ID访问控件。举例如下:

 <!-- custom_layout.xml -->
 <?xml version="1.0" encoding="utf-8"?>
 <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     xmlns:app="http://schemas.android.com/apk/res-auto">
 ​
     <TextView
         android:id="@+id/hello_text"
         app:layout_constraintBottom_toBottomOf="parent"
         app:layout_constraintTop_toTopOf="parent"
         app:layout_constraintLeft_toLeftOf="parent"
         app:layout_constraintRight_toRightOf="parent"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"/>
 </androidx.constraintlayout.widget.ConstraintLayout>

布局如上,如果需要在java代码中使用上述布局的TextView对象,代码如下:

 import kotlinx.android.synthetic.main.custom_layout.view.*
 ​
 hello_text.setOnClickListener{
     Toast.makeText(context,"Hello",Toast.LENGTH_SHORT).show()
 }

在kotlin 1.5.31上,上述代码正常运行,但升级到1.6.0后,同样的代码报错了,异常堆栈如下:

24-1-1

问题分析

24-1-2

从问题堆栈可以看出在CustomView的b函数中调用Map.get方法所使用的Map对象为空,导致空指针异常,堆栈很明确,那我们只需要找到CustomView的b函数,查看Map初始化过程即可,打开CustomView类,代码如下:

 package com.ams.myapplication
 ​
 import android.content.Context
 import android.util.AttributeSet
 import android.widget.FrameLayout
 import android.widget.Toast
 import kotlinx.android.synthetic.main.custom_layout.view.*
 ​
 class CustomView @JvmOverloads constructor(
     context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
 ) : FrameLayout(context, attrs, defStyleAttr) {
 ​
     init {
         inflate(context, R.layout.custom_layout, this)
         initAttrs(attrs)
     }
 ​
     private fun initAttrs(attrs: AttributeSet?) {
         // kotlin-android-extensions插件中提供的根据空间id访问控件对象的能力
         hello_text.setOnClickListener{
             Toast.makeText(context,"Hello",Toast.LENGTH_SHORT).show()
         }
     }
 }

内容很简单,完全没有b函数的定义,怎么回事?

再次观察代码可以发现,这里使用了kotlin-android-extensions提供的能力替换了findViewById的操作,根据经验,我们知道一般情况下,这种能力是通过构造控件id和控件对象之间的映射关系来实现的,而承载映射关系最典型的数据结构就是Map,正好对上了堆栈中的数据结构,不妨大胆假设kotlin-android-extensions插件中也是这样实现的,那么怎么验证这一想法呢?查看CustomView的字节码即可。

CustomView字节码分析

将前文中编译出的有问题的apk拖入Android Studio中,Android Studio会自动反编译该apk,如下图所示:

24-1-3

随后打开classes.dex,进入com/ams/myapplication,选中CustomView,右键弹出菜单,选择Show Bytecode就可以看到错误码了,如下图所示:

24-1-4

打开CustomView的字节码,可以看到如下截图:

24-1-8

其中红色区域为发生异常的b函数调用流程,从右到左看红色区域,可以得出如下调用流程:

kotlin_android_extensions

进一步结合右图1,可以看出V0地址的Map初始化发生在c函数调用之后,这种情况下b处的Map.get必然空指针异常呀,示意图如下:

kotlin_android_extensions.drawio

至此我们确认1.6.0版本对应的kotlin-android-extensions插件存在异常,初始化Map代码没有在构造函数刚开始就执行

解决方案

互联网搜索关键词kotlin android extensions 1.6.0 crash,如下图所示:

24-1-10

可以看到截图部分强关联kotlin-android-extensions的导入包名,点击进入

24-1-11

可以看到KT-49799的描述和我们遇到的问题完全一致,打开该链接

24-1-12

可以看到堆栈也是发生在Map.get方法上,在底下的讨论区给予了我们解决方案,升级kotlin到1.6.10

24-1-13

按照评论升级kotlin-gradle-plugin版本到1.6.10,问题解决,代码如下:

 classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.10"

KT-49799链接:youtrack.jetbrains.com/issue/KT-49…