Edited at

KotlinでProGuardを使う時にapplyなどのスコープ関数内で無名関数のネストが深くなるとビルドに失敗するバグ

なんか辛い現象を踏んでしまって地獄を見たので共有します。

結論だけ先に書くと、対象のクラスのあるpackageを-dontwarnすることで回避できます。

ぼくの場合は作業していたプロジェクトでproguardルールが反映されないという別の問題が同時に発生していたため、コードを書き換える事で対処しようとしてハマってしまいました。

さて、タイトルどおりなのですが、Kotlinを使ったAndroidプロジェクトでProGuardを有効にしたところ、ビルドに失敗してしまうようになりました。

原因が全然掴めなくて頭を抱えたのですが、いろいろ検証した結果、現象についてざっくりとわかりました。


再現コード

class MainActivity : AppCompatActivity() {

private val compositeDisposable = CompositeDisposable()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
hogehogeMethod()
}

override fun onDestroy() {
compositeDisposable.clear()
super.onDestroy()
}

private fun hogehogeMethod() {
compositeDisposable.apply {
add(Observable.just("hogehoge")
.subscribe {
AlertDialog.Builder(this@MainActivity)
.setNegativeButton(android.R.string.cancel) { dialog, which -> dialog.dismiss() }
.show()
})
}
}
}

こちらが現象が再現するコードです。

これをgradlew assembleReleaseすると、以下のようなwarningが出てビルドに失敗します。

Warning: kirimin.me.myapplication.MainActivity$hogehogeMethod$1$1$1: can't find referenced class kirimin.me.myapplication.MainActivity$hogehogeMethod$1$1

Warning: kirimin.me.myapplication.MainActivity$hogehogeMethod$1$1$1: can't find referenced class kirimin.me.myapplication.MainActivity$hogehogeMethod$1$1

中略

> Task :app:transformClassesAndResourcesWithProguardForRelease FAILED
Request to incrementing alive workforce from 0. Current workforce (dead or alive) 8
thread-pool size=12

FAILURE: Build failed with an exception.

 hogehogeMethodからhogehogeMethodが参照できないというよく分からないメッセージが出ています。

上記のコードからapplyを削るとビルドに成功します。

また、applyletに変えてitを使用しても同じ現象が発生しました。

この問題(バグ?)はKotlinのIssueにも上がっていましたが、放置されているようです。

https://youtrack.jetbrains.com/issue/KT-16084

なお、上記のIssueではletで再現するもっとシンプルなコードもコメントで書かれていました。

class Test {

fun test() {
let {
noInlineFun {
noInlineFun { }
}
}
}

private fun noInlineFun(lambda: () -> Unit) = lambda()
}

この現象自体は発生したクラスのあるpackageを以下のように-dontwarnに指定することで回避できました。

-dontwarn kirimin.me.myapplication.**

多分大丈夫ですが、もし心配ならば-keepも同時に書いておけばより安心かもしれません。


検証

現象と回避策はわかりましたが、具体的にはどういう状況で発生するのか気になったので検証してみました。

まず、以下のコードはビルドが通ります。

    private fun hogehogeMethod2() {

let {
noInlineFun {

}
}
}

private fun noInlineFun(lambda: () -> Unit) = lambda()

以下のコードは失敗します。

    private fun hogehogeMethod2() {

let {
textView.onClickListener {
textView.onClickListener { }
}
}
}

これを無名関数を使わずに変数に切り出すと、再現しなくなります。

    private fun hogehogeMethod2() {

let {
textView.onClickListener {
textView.setOnClickListener(listener)
}
}
}

private val listener: View.OnClickListener = View.OnClickListener {
}

どうやら、スコープ関数の中で無名関数(クロージャ?)をネストして利用すると再現するようです。

ではこれはスコープ関数以外では再現するのでしょうか?

試してみました。

    @ExperimentalContracts

private fun hogehogeMethod2() {
hoge {
textView.onClickListener {
textView.onClickListener { }
}
}
}
}

@ExperimentalContracts
public inline fun <T, R> T.hoge(block: (T) -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return block(this)
}

これはletの実装をそのままコピペしたhoge関数を使ってみた例です。

失敗しました。

そこでこの関数からinlineを消してみると...

    @ExperimentalContracts

private fun hogehogeMethod2() {
hoge {
textView.onClickListener {
textView.onClickListener { }
}
}
}
}

@ExperimentalContracts
public fun <T, R> T.hoge(block: (T) -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return block(this)
}

ビルド成功しました。。。


まとめ


  • KotlinでProGuardを使う時にapplyなどのスコープ関数内で無名関数のネストが深くなるとビルドに失敗する

  • ProGuardの-dontwarnで回避できる

  • スコープ関数じゃなくても同じような関数を定義して使用している場合は再現する

  • 原因はinline

疲れたのでとりあえず検証はこのくらいにしておきます。