nullable 是个好东西,但可惜 Java 没有呀~
0x00 Background
前段时间在fix 一个 Google Play 发现的一个issue的时候发现了一个非常古怪的问题。
java.lang.IllegalArgumentException
一个很简单的 Exception,但是在代码中怎么找怎么找,都找不到对应的到底是谁的参数有问题,看 crash stack 也非常的不明确,只是模糊的告诉我们,在一个 PhoneStateListener
的实现中。仔细检查了我们的实现以后发现:
override fun onCallStateChanged(state: Int, phoneNumber: String) {
// codes
}
这里面的代码非常的简单,几乎没有可能的地方会抛这个问题。
0x01 Root cause
主要的原因在于,Kotlin 在实现或继承 Java 层的接口/方法的时候,如果一个参数被加上了 @Nullable
的注解,则会被自动实现成 Type?
的形式,但是如果原本的接口不带这个注解,那么实现的时候,Kotlin中无论是 Type
or Type?
这两种写法都是被接受的,因此,在这个例子中,
Java 的接口是这样定义的:
// android/telephony/PhoneStateListener.java
public void onCallStateChanged(int state, String phoneNumber) {
// default implementation empty
}
而我们的实现中,这个 phoneNumber
的类型被强制变成了 @NonNull String
,因此当调用方传入 null
之后,Java层是正确的,但是回调到 Kotlin 的时候, String?
并不能强制转换成 String
, 因此会抛出 java.lang.IllegalArgumentException
。
0x02 Solution
找到了原因之后,fix 起来就非常的简单了,将原有接口的实现变更为:
override fun onCallStateChanged(state: Int, phoneNumber: String?) {
// codes
}
然后在内部进行 null check, 以确保不会发生其它error。
0x03 Afterwards
Kotlin/Java 混用,在现在的 Android Project 中应该是非常常见的场景了,平时开发过程中可能一个不注意就踩到了一些坑,因此需要格外小心一些隐式的转换之类,这类问题编译器无法发现,被抓到的时候可能栈也不是特别清楚,需要仔细甄别。
说到这里,实名表扬 Firebase crashlytics, Google Play中只能看到栈,但是 crashlytics 中能够看到
Fatal Exception: java.lang.IllegalArgumentException
Parameter specified as non-null is null: method kotlin.jvm.internal.Intrinsics.checkParameterIsNotNull, parameter phoneNumber
CallPhoneStateReceiver$onReceive$1.onCallStateChanged
非常实用了。