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について発表しました。

まだ出力がイマイチな部分はありますが、とりあえず仕事上の自分のプロジェクトで使い始めました。実際、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") と型を正確に合わせないといけない

Javassistは便利だが、黒魔術なので基本的にはデバッグ用途で使うのがいいと思う。

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があれば製品コードでも検討してもいいと思う。

*1:ただし、このバインディングは複数のDBインスタンスを想定していないなどクオリティが低いので、製品コードで使えるレベルではない

JavassistでAndroidメタプログラミングする

三行まとめ

詳細

Gradleはビルドプロセスのカスタマイズがしやすい。たとえば、クラスファイルを生成したあと、dexファイルにコンパイルする前にクラスファイルを編集するということも簡単にできる。これをJavassistでやってみた。

まず、project rootに buldSrc/ をつくり、それをビルドスクリプトのプロジェクトとする。

buildSrc/build.gradleで、buildSrc/ がGradle pluginであることと、javassistを使用することを宣言する。

apply plugin: 'groovy'

repositories {
&#160;&#160;&#160;&#160;mavenCentral()
}

dependencies {
&#160;&#160;&#160;&#160;compile gradleApi()
&#160;&#160;&#160;&#160;compile localGroovy()

&#160;&#160;&#160;&#160;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-Androidasyn.js のように、ネストしたコールバック呼び出しをウォーターフォールで記述できるライブラリである。

コールバックのネストをウォーターフォールで記述できるというのもありがたいが、処理をTaskに分割することで、非同期リクエストを組み合わせて使うことも簡単になっている。

Boltsを使わない、ネストした一連のHTTPリクエストは以下のようになる。ここではandroid-async-httpを使った。

https://github.com/gfx/Android-BoltsExample/blob/master/app/src/main/java/com/github/gfx/boltsexample/app/MainActivity.java

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;
    }
  });
}

これなら見通しがいいし、順番を入れ替えることも簡単にできる。