Androidで最新のJavaを使えない問題に向き合ってみた

Speee技術顧問の id:gfx です。

もうかなり時間が立ってしまいましたが、2月に行われたSpeeeKaigi #2 で基調講演をやらせてもらいました。 SpeeeKaigi #2の様子はこちらです。

tech.speee.jp

社内でこういうイベントがあるのはとてもいいですね。今回は自由テーマだったので、各人の興味分野が知れて大変楽しい一日でした。 f:id:mogmog2:20170508145255j:plain

私も普段の仕事とはあまり関係ない自由研究として、Retropilerというツールを開発して発表しました。当日のプレゼンテーションはQiitaにあります。

Retropiler: AndroidでJava8の機能を使うもう一つの方法

Retropiler: https://github.com/retropiler/retropiler

この資料はwhatにフォーカスしているので、それを補間すべくwhyとhowをこのエントリで書きます。

このretropilerを開発した動機は、「Androidで最新のJavaを使えない」という問題に対して何かできないかと思ったためです。鳴かぬなら鳴かせてみせようホトトギスの精神というわけです。

Retropilerでできること

Retropilerを使うと、次のようなコードを minSdkVersion=15 なアプリでも実行できます。

retropiler/MainActivity.java

// forEachは本来 API version 24 が必要
Arrays.asList("foo", "bar").forEach(s -> {
    Log.d("RetropilerExample", s); // => "foo", "bar"
});

// Optionalも本来 API version 24 が必要
Optional.of("foo").map(String::toUpperCase).ifPresent(s -> {
  Log.d("RetropilerExample", s); // => FOO
});

詳細はQiitaにポストした資料をご覧ください。

Androidで最新のJavaを使えないとはどういうことか

さて本題です。まず、Androidの開発環境における「Javaのバージョン」はいくつかの考え方があり、自明ではありません。ここでは3つに分けて考えます。

ひとつはAndroid端末にインストールされているAndroid OS、それにバンドルされているDalvik処理系(仮想マシン)がどのJVMバージョンに相当するかというものです。Androidの公式ドキュメントで「Javaの言語機能(language feature)」として言及される場合はこのバージョンです。

この言語機能のバージョンですが、Android 7.xまではJava 6相当と考えられています。2017年にリリース予定であるAndroid 8.xについては定かではありませんが、いまのところビルドツールはJava 6相当のDalvikバイトコードを生成するようです。

もうひとつは、Android端末にインストールされているAndroid OS、それにバンドルされているJava標準ライブラリのバージョンです。これはJDKのものとはだいぶ違うので「だいたいどのJDKに相当するか」としか言えないのですが、Android 7未満ではほぼJava7相当、Android 7以降はほぼJava8相当といえます。これはAndroidの公式ドキュメントでは「JavaのAPI」のバージョンと呼ばれています。

最後に、Androidアプリケーションを開発する際にJavaコンパイラに与えるバージョンです。

これは前二つと違ってAndroid OSのバージョンとある程度独立しているため、ビルドツールの進歩により改善する余地があります。実際、Android Studio 2.4 (Android Gradle Plugin 2.4) からは、minSdkVersionを古いまま(たとえば15)、開発時バージョンを Java8 (JavaVersion.VERSION_1_8) にしてアプリケーションをビルドできます。

ここでdesugarとよばれるツールにより、一部のJava8の言語機能、たとえばラムダ式をminSdkVersionに関わらず使うことができます*1。しかしdesugarは今のところアプリが動作する環境(=Android OS)に機能を追加できるわけではありません。つまりAndroid 7未満のバージョンで、Java8で追加された java.util.Optionaljava.util.function パッケージ、streamなどの標準ライブラリは使えません。これらをAndroidアプリで使いたければ、minSdkVersion=24 にする必要があります。

これがAndroid開発の現状であり、「Androidで最新のJavaを使えない」という問題です。

Retropilerが示したもの

さて、現状はこのとおりです。Android SDKに含まれる予定のdesugarは、ラムダなどの一部の言語機能をJava6のJVM向けに変換し、それをDalvikバイトコードに変換しますが、標準ライブラリに関しては何もしません。

しかし、原理的にはdesugarのようにバイトコードを編集し、Android OSにバンドルされていない新しいJava標準ライブラリ相当のものを差し込むことはできるはずなのです。このコンセプトを証明するためにRetropilerを開発してみた結果、ビルドフェーズにおける標準ライブラリの差し替えが可能であることを実証できました。

これは、アプリケーションコードで工夫するのではなく、ビルドシステムに干渉することで動作環境、つまりAndroid OSに足りないものを足してしまおうという発想です。世界のありようが気に入らなければ世界のほうを変えましょう。

実装

Retropilerは、Javassistを使ってバイトコードを編集するツールです。desugarと同様に、Android Gradle pluginのTransform APIを使って実装しています。

Javassistを使えばバイトコードをかなり自由に変更できます。たとえばRetropilerは Optional を使っている箇所を retropiler runtime の提供する _Optional に置き換えたり、 list.forEach(cb) の呼び出しを _Iterable.forEach(list, cb) に置き換えたりします。バイトコードの知識は必要になりますが、Javassistができることはまさに魔法です。

さてそういうわけで、RetropilerはJava8のクラスライブラリをランタイムライブラリとして提供する必要があります。たとえばこのように。

https://github.com/retropiler/retropiler/blob/master/runtime/src/main/java/io/github/retropiler/runtime/java/util/_Optional.java

なお、このファイルはAOSPからコピーしてきたものを改編したものですが、AOSPのファイルはOpen JDKからコピーしてきたもので、そのライセンスはGPL v2 + CE (classpath exception) となっています。Androidの場合CEについて定説がないようなので、今のところRetropilerを使うアプリケーションのライセンスをGPL v2(+CE)にしてソースコードを公開しなければなりません。製品で使う場合はその点にご注意ください。

まとめ

本エントリではRetropilerを紹介しました。まだやりたいことを全て実現したわけではないので、しばらくは開発を継続するつもりです。

もっとも実際には、Retropilerを実用的なレベルにするのは難しいかもしれません。差し込むためのライブラリを保守するのは一人では手に余るほど高コストです。しかし、なんといっても技術的に可能だということは示せました。また今回のGoogleによるdesugarの開発という事実もあります。もしかしたら数年後には公式でサポートもあり得るかもしれませんね。

という小難しいことは置いといて、 コードを書くのは楽しい とうことが伝わっていれば幸いです。