Android用Gradle pluginのテンプレをつくった
こんな感じの構成だと、実際のAndroid app/lib moduleで `apply plugin` をしつつプラグインの開発ができる。
実際に、android-power-assert-pluginは上記構成をとっている、というかgradle-plugin-templateはandroid-power-assert-pluginをcp -rして余計なものを削ぎ落しただけ。
potatotips #7 at DeNA で Power Assert について発表しました
potatotips #7 at DeNA を開催しました! 今回も面白い話が多く大変勉強になりました。
さて!私はここ二週間ほど開発していたAndroid Power Assertについて発表しました。
- https://speakerdeck.com/gfx/jun-hapower-assertwozhi-tuteiruka-number-potatotips
- https://github.com/gfx/android-power-assert-plugin
まだ出力がイマイチな部分はありますが、とりあえず仕事上の自分のプロジェクトで使い始めました。実際、assertが信頼できるというのはかなり安心感があります。これはもっと使えるように、これからも改善していきたいですね。
Maven Centralに代わるAndroid Archive Networkが必要だ
Android のライブラリプロジェクトを爆速で Maven Central にデプロイする をやってみたが、手順が複雑すぎて心が折れる。しかも、初回だからPGPの鍵を作ったり設定をいろいろ調べたりと作業は多かったが、それを差し引いても手作業でしなければならないことが多すぎる。しかも、フェーズごとに無視できない待ち時間がある。Androidのエコシステムが発展するためには、もっと簡単にライブラリをリリースできる仕組みが必要だと感じる。
Androidハッカーが息をするようにライブラリを書けるようにならなければ、エコシステムの発展は望めない。もっと光を!
Javassistのメモ
- Javassistはバイトコードを操作するもの
- 細かな操作はExprEditorで行う。この時操作できるのは、以下の式だけ。これらは副作用を起こしうる式にほぼ相当する。言い換えると、副作用を起こさない
int z = x + y
のような式はExprEditorの対象にできない- MethodCall
- ConstructorCall
- FieldAccess
- NewExpr
- NewArray
- Instanceof
- Cast
- Expr#replace(src) では、Java風の構文でコードを掛けるが、autoboxがなったり暗黙の型変換がなかったりする。たとえば、
void f(Object)
をf("str")
で呼び出すことはできない。f((Object)"str")
と型を正確に合わせないといけない
Android的には、LevelDBはSharedPreferencesの高速な代替として使えそう
AndroidにおけるLevelDB bindingは複数あるようだけど、今回はMaven Centralに上がっていてかつ読み書きAPIがわかりやすい以下のものを使った*1。
dependencies {
compile 'im.amomo.leveldb:leveldb:1.0.+@aar'
}
KVSなのでSharedPreferencesのように使える。速度はopen/closeにSharedPreferencesよりコストが掛かるものの、読み書きは非常に高速にできるようだ。
サンプルアプリのソースコード:
使っているところはこんな感じ:
private void performLevelDB(int n) { long t0 = System.currentTimeMillis(); LevelDB db = DBFactory.open(this); for (int i = 0; i < n; i++) { if (db.exists("foo")) { long value = db.getLong("foo"); db.put("foo", value + 1L); } else { db.put("foo", 1L); } } db.close(); textView.append("LebelDB open, (get, put)*" + n + ", and close: " + (System.currentTimeMillis() - t0) + "ms\n"); }
その他のLevelDB bindingとしては以下のものがある。IteratorやWriteBatchなどひと通り実装済みだが、get/putが byte[] でしかできず、Javaで使うのはかなり大変そうだ。高速なのは確かなので、まともなbindingがあれば製品コードでも検討してもいいと思う。
JavassistでAndroidメタプログラミングする
三行まとめ
詳細
Gradleはビルドプロセスのカスタマイズがしやすい。たとえば、クラスファイルを生成したあと、dexファイルにコンパイルする前にクラスファイルを編集するということも簡単にできる。これをJavassistでやってみた。
まず、project rootに buldSrc/ をつくり、それをビルドスクリプトのプロジェクトとする。
buildSrc/build.gradleで、buildSrc/ がGradle pluginであることと、javassistを使用することを宣言する。
apply plugin: 'groovy' repositories {     mavenCentral() } dependencies {     compile gradleApi()     compile localGroovy()     compile 'org.javassist:javassist:3.18.+' }
つぎに、buildSrc/src/main/groovyにgroovyでコードを書く。ちゃんとしたpluginにはあとでするとして、今はエントリポイントをひとつ書けば十分だ。
内容は、とりあえずMainActivity#onResume()の最後でログを吐くコードを注入するだけにした。このように、Javaのソースコードを注入するとJavassistはそれをバイトコードにコンパイルして、それを操作対象のバイトコードに注入する。それを再びクラスファイルに書き戻す。
package com.github.gfx.javassistexamp import javassist.ClassPool import javassist.CtClass import javassist.CtMethod public class JavassistExample { public static void process(String buildDir) { ClassPool classes = ClassPool.getDefault() classes.appendClassPath("/usr/local/opt/android-sdk/platforms/android-19/android.jar") classes.appendClassPath(buildDir) CtClass c = classes.getCtClass("com.github.gfx.javassistexample.app.MainActivity") CtMethod m = c.getDeclaredMethod("onCreate") m.insertAfter("android.util.Log.d(\"XXX\", \"hoge\");") // コードを注入する c.writeFile(buildDir) } }
このJavasistExample.process()を、app/build.gradleから呼び出す設定をして実行すると、注入したコードが実行される様子を観察できるはず。
task('processWithJavassist') << { //String path = file('build/classes/debug/com/github/gfx/javassistexample/app/MainActivity.class') String classPath = file('build/classes/debug') com.github.gfx.javassistexamp.JavassistExample.process(classPath) } android { // ... applicationVariants.all { variant -> variant.dex.dependsOn << processWithJavassist } }
実行可能なコードはgithubに置いた。
JavassitExampleはコードが雑だったりandroid.jarのパスをハードコードしていたりして課題はあるが、とりあえずクラスファイルの操作はうまく行って、AndroidでもJavassistを使えることを確認できたのでよしとしよう。
Bolts-Androidで連続したHTTPリクエストをウォーターフォールにする
Facebook (Parse) が開発している Bolts-Android は asyn.js のように、ネストしたコールバック呼び出しをウォーターフォールで記述できるライブラリである。
コールバックのネストをウォーターフォールで記述できるというのもありがたいが、処理をTaskに分割することで、非同期リクエストを組み合わせて使うことも簡単になっている。
Boltsを使わない、ネストした一連のHTTPリクエストは以下のようになる。ここではandroid-async-httpを使った。
private void nestedRequests() { final TextView textView = (TextView) findViewById(R.id.body); textView.append("nested\n"); final long t0 = System.currentTimeMillis(); client.get(kApiBase, new RequestParams("q", "apple"), new AsyncHttpResponseHandler() { @Override public void onSuccess(int statusCode, Header[] headers, byte[] content) { assert statusCode == 200; // skip error handling textView.append("apple : " + new String(content) + "\n"); client.get(kApiBase, new RequestParams("q", "banana"), new AsyncHttpResponseHandler() { @Override public void onSuccess(int statusCode, Header[] headers, byte[] content) { assert statusCode == 200; // skip error handling textView.append("banana : " + new String(content) + "\n"); client.get(kApiBase, new RequestParams("q", "beef"), new AsyncHttpResponseHandler() { @Override public void onSuccess(int statusCode, Header[] headers, byte[] content) { assert statusCode == 200; // skip error handling textView.append("beef : " + new String(content) + "\n"); client.get(kApiBase, new RequestParams("q", "xxx"), new AsyncHttpResponseHandler() { @Override public void onSuccess(int statusCode, Header[] headers, byte[] content) { assert statusCode == 200; // skip error handling textView.append("xxx : " + new String(content) + "\n"); textView.append("elapsed (nested) : " + (System.currentTimeMillis() - t0) + "ms\n"); waterfallRequests(); } }); } }); } }); } }); }
これはREST APIを使うAndroidアプリだと決して誇張ではない。ネストそのものはメソッドを分割することで減らすことができるが、それだと呼び出し順が固定されるので柔軟性に乏しく本質的な改善とはいえない。
これが、Boltsをつかうと以下のようになる。まず、非同期リクエストを呼んで Task
を生成するメソッドをつくる。この Task は Future と概念的には同じで、生成された当初は空で非同期処理が終わるって中身がセットされると状態が変化するコンテナのようなオブジェクトである。このTaskを continueWith() / continueWithTask() でつなげて実行すれば、非同期リクエストをウォーターフォールで実行できるというわけだ。
private Task<String> getApiAsync(String word) { final Task<String>.TaskCompletionSource taskSource = Task.create(); RequestParams params = new RequestParams(); params.put("q", word); client.get(this, kApiBase, params, new AsyncHttpResponseHandler() { @Override public void onSuccess(int statusCode, Header[] headers, byte[] responseBody) { String s = new String(responseBody); if (statusCode == 200) { taskSource.setResult(s); } else { taskSource.setError(new HttpResponseException(statusCode, s)); } } }); return taskSource.getTask(); } private void waterfallRequests() { final TextView textView = (TextView) findViewById(R.id.body); textView.append("waterfall\n"); final long t0 = System.currentTimeMillis(); getApiAsync("apple").continueWithTask(new Continuation<String, Task<String>>() { @Override public Task<String> then(Task<String> task) throws Exception { textView.append("apple : " + task.getResult() + "\n"); return getApiAsync("banana"); } }).continueWithTask(new Continuation<String, Task<String>>() { @Override public Task<String> then(Task<String> task) throws Exception { textView.append("banana : " + task.getResult() + "\n"); return getApiAsync("beef"); } }).continueWithTask(new Continuation<String, Task<String>>() { @Override public Task<String> then(Task<String> task) throws Exception { textView.append("beef : " + task.getResult() + "\n"); return getApiAsync("xxx"); } }).continueWith(new Continuation<String, Void>() { @Override public Void then(Task<String> task) throws Exception { textView.append("xxx : " + task.getResult() + "\n"); textView.append("elapsed (serial): " + (System.currentTimeMillis() - t0) + "ms\n"); return null; } }); }
これなら見通しがいいし、順番を入れ替えることも簡単にできる。