Kotlinのinner ClassでVerifyErrorが発生する条件と回避方法

  • 9
    いいね
  • 0
    コメント

この投降は、Kotlin Advent Calendar 2016の20日目の投稿です。

はじめに

Kotlin使ってますかー?もちろん使ってますよね!?
まずは私のKotlin経験について、軽く紹介したいと思います。

Kotlin歴

  • 2016年初頭からKotlinに触れ始める
  • 最初はKotlin公式のチュートリアルを一通りこなす
  • 「いいぞ、これ」
  • でもまだ製品版に入れるのは怖い
  • だからKotlin + Spring Bootで社内用のWebアプリ作る
  • 「やっぱりKotlin最高や!」
  • でもまだ既存Androidアプリ(64K超え+依存モジュール&ライブラリテンコ盛り)に入れるの怖い
  • だから純Kotlinで製品版のAndroidアプリを作ってリリース
  • 「よし、問題ない」
  • 本丸の既存AndroidアプリにKotlinをゴリゴリ追加
  • 「やっふー、Kotlin最高や!!」

Kotlinの使用感

導入してみての感想は「あれも(Null安全)、これも(lambda式)、それも(リスト関数やスコープ関数)、全部Kotlin導入するだけでできる!」ってことに尽きますね。
Java混合アプリなのでJavaコードのメンテもするのですが、「Kotlinなら簡単なのにな」って場面がたくさんあります。
しかし、そういったことについてはいろんな人がたくさん紹介してくれているので、私は実際に開発時に遭遇した実行時エラー(VerifyError)とその回避方法について紹介したいと思います。1

VerifyErrorに遭遇

その日も気持ちよくKotlinコードを書いて、実機でテストして、社内テストチームにリリースして~ってしてました。
そんな風に、のほほんとしていたら、テストチームから連絡がありました。
「このボタン触ったらアプリが落ちるんだけど?」
え!?こっちでは落ちないぞ!?ってなって、よくよく調べてみるとAndroid OS のVersionに依存するみたいでした。
発生したエラーはjava.lang.VerifyErrorで、なんだこれ?って調べてみると「クラス・ファイルが適切な形式でも、ある種の内部矛盾またはセキュリティ上の問題があることを「ベリファイア(verifier)」が検出した場合にスローされます」らしい
でも発生箇所はKotlinコードだし、生成されるclassファイル見ても正直よくわからん。って思いながら調べました。

発生条件

いろいろ試してみると、次のような条件を全て満たしてしまうとビルドは通りますが実行時エラー(VerifyError)が発生します。2

  1. inner classのコンストラクターにデフォルト引数がある
  2. inner classからouter classを参照している(プロパティ参照やメソッド呼び出し)箇所がある
  3. 1のデフォルト引数にオブジェクト定義があり、その中に2(outer classを参照)がある
  4. 実行環境がAndroid OS 5未満

コード例(最小構成)

条件の羅列だけではぱっと見わからないのでコード例で見てみましょう。3

class Outer {
    fun test() {
        Inner()// ここでjava.lang.VerifyError発生
    }

    // inner から参照されるプロパティ
    private val property = "dummy"

    // 条件1:inner class定義
    inner class Inner(
            // 条件2:デフォルト引数でオブジェクト生成(ラムダ式もオブジェクト生成と等価)
            val lambda: () -> Unit = {
                // 条件3:生成したオブジェクト内でouter classのプロパティを参照
                android.util.Log.i("TAG", property)
            })
}

実際に近いコード例:純Kotlin

たとえばコールバックを渡すクラスをinner classで継承して、そのコールバック内からouter classのメソッドを呼び出すような場面では、以下のように書くことがあると思います。

class Outer2 {
    fun test() {
        val c = Inner()// ここでjava.lang.VerifyError発生
        c.execute()
    }

    // このメソッドをコールバックから呼び出してもらう
    private fun onCallback() {
        android.util.Log.i("TAG", "onCallback")
    }

    // 条件を満たすinner class定義
    inner class Inner() : Base2({ onCallback() })
}

// コールバックを必要とする基底クラス
open class Base2(val callback: () -> Unit) {
    fun execute() {
        // なにかしてからコールバックを呼び出す
        callback()
    }
}

このようにコールバックをラムダ式で受け取って、その中でouter classのメソッド呼び出しを行うとエラーが発生してしまいます。

実際に近いコード例:Java共存環境

Javaコードと共存している環境ならコールバックはインターフェース定義がされていると思いますが、その場合は以下のコードだとエラーが発生します。

kotlin
class Outer3 {
    fun test() {
        val c = Inner()// ここでjava.lang.VerifyError発生
        c.execute()
    }

    // このメソッドをコールバックから呼び出してもらう
    private fun onCallback() {
        android.util.Log.i("TAG", "onCallback")
    }

    // 条件を満たすinner class定義
    inner class Inner() : BaseJava(Callback { onCallback() })
}
java
// コールバックを必要とする基底クラス
public class BaseJava {
    public interface Callback {
        void call();
    }

    private final Callback callback;

    public BaseJava(Callback callback) {
        this.callback = callback;
    }

    public void execute() {
        // なにかしてからコールバックを呼び出す
        callback.call();
    }
}

デフォルト引数でobject定義をしているので、これもエラーが発生します。

発生条件の注意点

エラーの発生条件のうち、1, 2, 4は結構当てはまる場合が多いと思います。
条件2はinner classを使う理由なので、これが無いならそもそもinner class使わないですし。
なのでinner classを使っていてAndroid OS5未満をサポートしているような、ほとんどのアプリは条件1, 2, 4を満たしてしまうのではないでしょうか。
なので、気を付けるのは条件3の「inner classでデフォルト引数を使い、そこでオブジェクトを定義してouterを参照している」ところです。
そういう場所では以下のような回避方法を使って、やり過ごしてください。

回避方法

問題となっているのは、inner classのconstructorでオブジェクト定義を行い、そこからouter classを参照していることなので、それを変えます。

純Kotlinの場合

class Outer4 {
    fun test() {
        val c = Inner()// もうエラーは発生しない
        c.execute()
    }

    // このメソッドでコールバックを生成する
    private fun createCallback(): () -> Unit = {
        android.util.Log.i("TAG", "onCallback")
    }

    // inner classのconstructorではouter classのメソッド呼び出しだけで
    // オブジェクト定義は無いので、条件を回避
    inner class Inner() : Base4(createCallback())
}

// コールバックを必要とする基底クラス
open class Base4(val callback: () -> Unit) {
    fun execute() {
        // なにかしてからコールバックを呼び出す
        callback()
    }
}

Java共存環境の場合

class Outer5 {
    fun test() {
        val c = Inner()// もうエラーは発生しない
        c.execute()
    }

    // このメソッドをコールバックから呼び出してもらう
    private fun onCallback() {
        android.util.Log.i("TAG", "onCallback")
    }

    // コールバッククラスをinner classで定義する
    inner class CallbackOuter5 : BaseJava.Callback {
        override fun call() {
            // コンストラクター以外からのouter classアクセスは普通にできる
            onCallback()
        }
    }

    // inner classのconstructorではouter classで定義したinner classのコンストラクター呼び出しだけで
    // オブジェクト定義は無いので、条件を回避
    inner class Inner() : BaseJava(CallbackOuter5())
}
java
// コールバックを必要とする基底クラス
public class BaseJava {
    public interface Callback {
        void call();
    }

    private final Callback callback;

    public BaseJava(Callback callback) {
        this.callback = callback;
    }

    public void execute() {
        // なにかしてからコールバックを呼び出す
        callback.call();
    }
}

補足

この問題は公式のissueに登録されていて、おそらく解決できるだろう、みたいなステータスっぽいです。4
またissueにはStack Overflowのリンクがあって、そこにはこの問題の詳しい原因や回避方法が載っています。
Stack Overflowで紹介されている回避方法は本記事とは違い、inner classのコンストラクターにouter classの参照を渡して、outer classのプロパティにアクセスするときは、それを使うって方法みたいです。5

さいごに

というわけで、Kotlinでinner classを使うときは、少しだけ注意しましょう!
ではみなさん、来年も楽しいKotlinライフを!


  1. ネタ被らないでくれー(祈り) 

  2. Kotlinのversionは2016/12月最新の1.0.5-2です。 

  3. これは再現用の最小構成なので、これと同じコードは書かないと思います。引数使ってないですし 

  4. 公式で修正されるのが先か、AndroidOS5未満が消え去るのが先か・・・ 

  5. issueにも書いてますが、この書き方だと、inner classから普通にouterは見えてるのに、なんでコンストラクターでわざわざouter classの参照渡すの?ってあたりが不思議に見えちゃいますね。なので今回は違う方法を紹介してみました。