Kotlin 1.1.2-5くらいからSAM変換がちょっと変わってcrashが起きた件
Kotlinのバージョンあげたら既存コードがcrashしたのでちょっと調べてみたメモ。
何が起こったのか
kotlin のversionを1.0.5-3から1.1.2-5にあげたら既存コードがcrashするようになった。具体的には、こんな感じのIllegalStateExceptionが吐かれる。
java.lang.IllegalStateException: invoke(...) must not be null
何が悪かったか
Kotlin側に渡すJavaで定義したFunction0
のオブジェクトでinvoke()
をoverrideした際にreturn nullしていたのが悪かった。Javaでいうvoidに当たるUnitのインスタンスをreturnすることで落ちなくなる。
new SamConversionExperimentalKt().invokeFunctionFromKotlin(new Function0<Unit>() { @Override public Unit invoke() { doSomething(); return Unit.INSTANCE; // return nullするとcrashになるよ } });
Android Studio(v2.3)だとinvoke()メソッドを補完するときにreturn nullで補完してくれるからいちいち手で書き換えないといけない_:(´ཀ`」 ∠):
Javaでいうvoidはkotlinのnull許容・非許容と離れた概念だからどっち返しても大丈夫でしょ、とか思ってたら思わぬところで痛い目をみる。
で、問題はなんだったの?
実はこのcrashが発生するのにいくつか条件があって、
- JavaでFunctionN系のinvoke()メソッドをreturn nullしてoverrideする
- 1で定義したインスタンスをkotlinのコードに渡す
- 2のkotlinのコードからさらにjavaのコードに渡して、invoke()ではなくSAM変換後のメソッドとして実行する
3がちょっとなんて言ったらわからないのでコードを交えて説明。
適当なAndroidのコードで試したものです。まずはFunction0
public class SamConversionExperimental { public void invokeFunction() { new SamConversionExperimentalKt().invokeFunctionFromKotlin(new Function0<Unit>() { @Override public Unit invoke() { doSomething(); return Unit.INSTANCE; // return nullするとcrashになるよ } }); } private void doSomething() { } }
その後、受け取ったkotlinコード側でSAM変換がかかるような感じで実行してやればcrash. 手元だとRunnable#run() が走るようにしてcrashさせた。
(Runnableは単一メソッドrun()だけを持つインターフェイス)
fun invokeFunctionFromKotlin(function: () -> Unit) = Handler(Looper.getMainLooper()).post(function)
SAM変換変わったね?
エラー文から察するに、この辺りのチェックコードがversion上がって入ってきたっぽい。 github.com
public static void checkExpressionValueIsNotNull(Object value, String expression) { if (value == null) { throw sanitizeStackTrace(new IllegalStateException(expression + " must not be null")); } }
で、このメソッドがどこから呼ばれているかというと、この辺り。
private fun analyzeTypesAndRemoveDeadCode(): Map<AbstractInsnNode, Type> { val insns = methodNode.instructions.toArray() val frames = analyze(internalClassName, methodNode, OptimizationBasicInterpreter()) val checkedReferenceTypes = HashMap<AbstractInsnNode, Type>() for (i in insns.indices) { val insn = insns[i] val frame = frames[i] if (insn.isInstanceOfOrNullCheck()) { checkedReferenceTypes[insn] = frame?.top()?.type ?: continue } else if (insn.isCheckParameterIsNotNull() || insn.isCheckExpressionValueIsNotNull()) { // ここにチェックが入っている checkedReferenceTypes[insn] = frame?.peek(1)?.type ?: continue } } val dceResult = DeadCodeEliminationMethodTransformer().removeDeadCodeByFrames(methodNode, frames) if (dceResult.hasRemovedAnything()) { changes = true } return checkedReferenceTypes }
なんかこの辺りのcommitでcheck増えてそうな気配を感じるのでこれかなぁ