現在JavaScriptの設定が無効になっています。このページを利用するにはJavaScriptの設定を有効にしてください。2016の11日目の記事です。一覧はこちらこんにちは、久々に記事を書きます。Androidエンジニアの森です。ヤフオク!アプリの開発を行う傍ら、ヤフーの黒帯制度にてAndroidの黒帯としても活動しています。皆さんはライブラリを作っていますか?年末年始は休みも長いので、普段は作れない、何かちょっとしたものを開発するチャンスです。普段の開発を振り返って、面倒くさいところ、定型的な作業があれば、それを自動化するライブラリを書いて、GitHubにあげてみるのもいいかもしれません。弊社の田中が書いた、Rollout.ioというiOS向けのHot-patchツールに関する記事を読まれたでしょうか。リリースしたアプリに不具合が見つかった場合、特にその不具合が致命的なものだと、できるだけ素早くユーザーに修正版を届けたいと思うはずです。iOSでは、Appleの審査を経なければ修正版をリリースできない、という制約があります。一刻を争っている状況では、その審査期間こそが課題となります。Rolloutは、Hot-patchという、アプリのリリースとアップデートを伴わないコード修正を可能にする技術によって、こうした課題を解決するためのツールです。どのようなアプリを世に出すか、またどのように使ってもらうかも大切なのですが、リリース後のユーザーへのケアも同じように重要なことです。そこで、Androidでも同じようなことができるように、ちょっとしたライブラリを作ってみましたので、そこで使っている技術と合わせてご紹介したいと思います。実用レベルのものではありませんが、こういうこともできるんだ、という参考にしていただければと思います。メタプログラミングある処理を実現したい場合に、直接そのためのコードを書くのではなく、そうしたコードを生成するためのコードを書き、それを使って機械的に生成する手法のことを、メタプログラミングと呼びます。ある1箇所にログを出力するコードを記述することは容易ですが、プロジェクトの全メソッドの冒頭に、受け取った引数の値をログに出力させるとなると大変です。手作業で入れていくと、日が暮れてしまいます。いう作業に時間を費やしたくはありません。した場合に、「メソッドの冒頭にログ出力処理を挿入するよう修正する」プログラムをつくり、コンパイル時に動作させる等を行うことによって、自動的に目的の処理を埋め込むことができます。これによる利点は手間の削減だけではありません。ログ出力のように、本質的な処理とは異なる処理がメソッド内に紛れ込んでしまうと、そのメソッドが本来何をするためのものなのか、理解しづらくなってしまいます。そのような処理をコンパイル時に挿入するようにし、開発時にコード上では見えなくすることによって、可読性を向上させることができます。一定の処理の強制という意味でも効果があるでしょう。メタプログラミングは強力です。コード全体に容易に影響を与えることができます。行いすぎると逆にコードを理解することも、デバッグすることも難しくなります。下手に設計されたライブラリを使用すると、思わぬ不具合を引き起こしてしまい、それを探るために「このライブラリ内では何を行っているのだろう」と開発者が探らなければならない事態に陥ってしまいます。EventBusやRealm、ButterKnifeなど、優れたOSSライブラリのなかには、コードの自動生成やバイトコードの修正を上手に利用しているものが多くあります。AbstractProcessorを実装したクラスを作成し、モジュールレベルのbuild.gradleのなかで指定しておくと、コンパイル時に呼び出されます。指定したアノテーションをつけたクラスやメソッドなどの要素が渡されてきますので、それらの情報を元にJavaファイルを生成し、生成したファイルも含めてコンパイルした結果を、最終生成物(apkファイル)に含めることができます。Javaで使用されてきたjavax.annotation.processingパッケージのクラス群で、これをAndroidで使用できるようにするために、hvisser/android-aptのようなOSSプラグインが使われてきました。2.2からは、この機能は公式でサポートされるようになり、別途プラグインを入れる必要もなくなりました。Androidの新しいツールチェイン/ビルド環境であるJackでもサポートされています。Processingは将来に渡って使用可能であろうと思われます。Processingでは、新しいクラスを生成することはできるのですが、既存のクラスに修正を加えることは、(Lombokのように特殊なことをしない限り)基本的にはできません。そのため、上に挙げた「既存のメソッドにログ出力処理を挿入する」といったようなことは、通常はできません。libraryモジュールとセットにする構成をとることが多いようです。Processingは多くのライブラリで使用されています。ButterKnifeやDaggerなどとの相性はすばらしく、Reflectionを使わないために高速に動作します。APIは、コンパイルされたクラスファイルへの修正に特化した機能を提供します。API単体で使用されることはあまりなく、通常はJavassistなどのバイトコード操作用のライブラリと併用されるようです。APIを利用して実現しています。APIは、現状ではJackツールチェインには対応していません。そもそもJackはアプリのコンパイル中に中間コードを生成しないため、中間コードを操作するツールは併用できないのです。Runが機能しなくなる理由はここにあります。いってサポートが断念されたというわけではなく、AOSPのIssuesを見る限り、対応するつもりはあるようです。この発言も5月のことですので、そろそろ続報を聞きたいところですが...。APIを使用した場合、新規にクラスを作成するだけでなく、既存のクラスやメソッドに修正を加えることができます。Processingと比べると、かなり柔軟にコードを組み替えることができますし、アプリにライブラリのクラスを呼び出して貰う必要もないため、作ることのできる機能の幅も広がります。APIはその有用さの割には、それほど多くのOSSライブラリで使用されているわけではありません。APIを使用し、Javassistも併用しています。APIを使用してみましょう。サンプルコードはこちらに置いてあります。APIは、com.android.build.api.transform.Transformクラスを実装し、モジュールレベルのbuild.gradle内で、そのインスタンスを登録します。すると、Transform#transform()メソッドが呼ばれますので、そこで渡されたメタデータをもとにバイトコードを操作し、修正後のファイルを書き出します。修正後のファイルは、かならず出力しなければならないことに注意して下さい。全体構成サンプル全体は、以下のような構成になっています。・・・111~4は、プロジェクト内で実装したクラスのメソッドに、ログ出力処理を挿入するライブラリと、それを使ったサンプルアプリです。2のloggerがそのライブラリで、プラグインとTransformクラス、アノテーション定義が入っています。これをパッケージしたものをローカルリポジトリにアップロードして使用するのですが、そのファイルの置き場所が4のarchivesです。3はこのライブラリを実際に使用しているサンプルアプリです。5~8のサンプルも同じ構成になっています。サーバからパッチをダウンロードしてきて、実行時に任意のメソッドの内容を置き換えるというものなのですが、6がそのライブラリ、8がローカルリポジトリで、7が実際に使用しているサンプルアプリです。本当は分割すればよかったのですが、7にはパッチをダウンロードしたり、スクリプトを実行したりするためのAndroidライブラリモジュールも含まれています。9~11は、2つ目のサンプルで使用するための、簡易的なファイルサーバです。エミュレータ上からアクセスするために、localhost:8080で動作します。テスト用途のために作ったものですので、ローカルで動かすにとどめておいてください。10がそのサーバプログラムです。main.go./mainで実行できます。11はパッチファイルの置き場所です。使えるよう、すでにパッチファイルを置いています。この2つのサンプルは、どちらも基本的な仕組みは同じです。APIが使用可能かを確認した上で、バイトコード操作を行うTransformクラスのインスタンスを登録します。APITransform#transform()内でJavassistを使用して、バイトコード操作を行います。その後、クラスファイルを出力します。Library2つ目のサンプルの、SampleApp2のプロジェクト内に含まれています。本当は分けたほうが良かったですね。PatchServerと通信してスクリプトをダウンロードする機能と、RhinoというJavaで書かれたJavaScript実装を利用して、スクリプトを実行する機能が用意されています。IDEAを使用して開発しました。サンプル1ソースコードはまるっと載せるには長すぎるため、重要なところだけ抜き出していきます。Pluginを作成して、この後に登場するLoggerTransformerのインスタンスを、バイトコード操作を行うクラスとして登録します。複数のTransformを登録する場合や、他のライブラリでもTransformが使用されている場合、適用される順序を決めることはできません。プラグインの内容を以下に示します。サンプル2でも、登録するTransformが異なることを除いて、全く同じことを行っています。}}Androidアプリ、またはAndroidライブラリであることを確認します。ログ出力にandroid.util.Logを使用しているため、通常のJavaプログラムでは使用できません。Pluginが2.0以降であることを確認します。API自体は1.5以降で使用可能なのですが、従来のTransform#transform()メソッドが2.0でdeprecatedになってしまったため、こうなっています。バイトコード操作を行うTransform(ここではLoggerTransformer)のインスタンスを登録します。実際にコード修正を行ってくれるのは、このLoggerTransformerクラスです。logger/LoggerTransformer.groovyの、transform()でバイトコード操作を行っています。具体的には、modify()がそれに当たります。それ以外の部分に関しては、他のライブラリを作成する場合でも同じことをするでしょうから、使いまわしが可能です。}}以下がmodify()の内容です。プロジェクト内で実装しているクラスの名前の一覧から、@Loggingというアノテーションのついたクラスを探し、そのクラスで実装されているメソッドの一覧から、native、interface、abstractを除外した上で、ログ出力処理をメソッドの冒頭と最後に追加しています。似たようなことを行う、より優れたOSSライブラリとして、JakeWharton/Hugoがあります。このように、バイトコード操作を行うと言っても、実際には文字列でコードを記述し、バイトコードに変換・挿入するところはJavassistが行ってくれるため、容易に実装することができます。CtMethod#insertAfter()で対象メソッドの最後に処理を追加できるのですが、これはfinally節として実行されるので、戻り値のことを心配する必要もありません。Transformの段階では、もうimport文を追加することはできませんので、java.langパッケージのようにimport不要のクラス以外は、パッケージをつけたかたちで参照しましょう。