GlideのバックエンドをOkHttp3にしてカスタムインスタンスを与える

github.com

GlideのバックエンドをOkHttp3にするには、 okhttp3-integration を使います。

dependencies {
    compile 'com.github.bumptech.glide:okhttp3-integration:1.4.0@aar'
}

これだけで、GlideのバックエンドHTTPクライアントがOkHttp3になります。

しかし、カスタムOkHttp3インスタンスを与えるにはさらに一工夫必要で、具体的には、アプリケーション用のGlideModuleを実装してAndroidManifestで宣言する必要があります。

まず、GlideModuleを実装したクラスを定義します。

public class OkHttp3GlideModule implements GlideModule {

    OkHttpClient client = ...; // 何らかの方法でOkHttpClientのインスタンスを得る

    @Override
    public void applyOptions(Context context, GlideBuilder builder) {
    }

    @Override
    public void registerComponents(Context context, Glide glide) {
        Log.d("OkHttp3GlideModule", "registerComponents");

        // glideに登録する。
        // OkHttpUrlLoaderはokhttp3-integrationに定義されたクラス
        glide.register(GlideUrl.class, InputStream.class,
                new OkHttpUrlLoader.Factory(client));
    }
}

これをAndroidManifestで宣言します。

<application>
    <meta-data
        android:name="com.example.app.OkHttp3GlideModule"
        android:value="GlideModule"
        />
</application>

これでGlideでカスタムOkHttp3インスタンスが使われるようになります。

See Also

JRubyをライブラリとして使う(Android編)

三行まとめ

  • JRuby 1.7.24 (Ruby 1.9.3相当) はAndroidから普通に使える
  • JRuby 9.1.0.0 (Ruby 2.3相当) はOracle JDK固有のクラスを使っていてAndroidでは使えない
  • JRuby on Androidに興味のある開発チームはいるようなので、しばらくすると使えるようにはなるかもしれない

解説

Re:VIEW のソースを直接レンダリングするAndroidアプリがあったら面白いかなと思い、かといってRe:VIEWをJavaで再実装するのも面倒なのでまずJRubyをAndroidで動かせるかどうか試してみました。といっても、すでにJRubyを処理系として使うRubotoというAndroid用フレームワークがあるので、ある程度は使えることが分かっていました。

このJRubyを試しているリポジトリは https://github.com/gfx/HelloJRuby にあります。

依存関係

まず依存は次のように指定するだけです。ただし、JRubyは9000系と1.7系があり、相当するRubyのバージョンが違います。

// JRuby 9000 (Ruby 2.x相当)
dependencies {
   compile 'org.jruby:jruby:9.1.0.0'
}

// JRuby 1.7 (Ruby 1.9相当)
dependencies {
   compile 'org.jruby:jruby:1.7.24'
}

実行

JRuby wiki に簡単な使い方が書いてありるので、それにしたがってRubyスクリプトを評価するコードを書きます。JITを無効化するのを忘れないように。

from MainActivity.java:

Log.d(TAG, "Ruby starting");

// Rubyランタイムの設定
RubyInstanceConfig rubyInstanceConfig = new RubyInstanceConfig();
// JITはOFFにする
rubyInstanceConfig.setCompileMode(RubyInstanceConfig.CompileMode.OFF);
// Rubyラインタイムを生成する
Ruby runtime = Ruby.newInstance(rubyInstanceConfig);

Log.d(TAG, "Ruby instantiated (" + (System.currentTimeMillis() - t0) + "ms)");

// Rubyスクリプト。android.util.Log.d()を呼んだあと、BuildConfig.APPLICATION_IDを返す
String script = "require 'java'\n"+
        "Java::AndroidUtil::Log.d %Q{Ruby}, %Q{This is JRuby (Ruby #{RUBY_VERSION})}\n"+
        "Java::ComGithubGfxAndroidHellojruby::BuildConfig::APPLICATION_ID";

// Rubyスクリプトを実行する
IRubyObject result = runtime.evalScriptlet(script);

Log.d(TAG, "Ruby evaluated (" + (System.currentTimeMillis() - t0) + "ms)");

TextView textView = (TextView) findViewById(R.id.text);
assert textView != null;
// RubyオブジェクトのもつJavaオブジェクトを取り出す
textView.setText(result.toJava(String.class).toString());

さて、これをJRuby 9.1.0.0で動かすと、java.lang.invoke.SwitchPoint が存在しないという実行時例外で落ちてしまいました。これはAndrodには実装されていないようです。そこで一旦JRubyのバージョンを1.7.24に落としてみると、無事に実行できました。

Re:VIEWの最小サポートバージョンがRuby 2.0 なので Ruby 2.2に相当するJRuby 9.1.0.0を使いたいところです。現在は実行できない以上、当初の目的であったRe:VIEWの実行はできませんね。残念…。

Kotlin Coroutines の様子を眺める

先日、JetBrainsのブログでKotlinにコルーチンが導入されるという発表がありました。

以下のリポジトリで先行事例の調査や仕様の検討が行われています。

https://github.com/Kotlin/kotlin-coroutines

先行事例の調査は以下のissueです。

https://github.com/Kotlin/kotlin-coroutines/issues/2

Kotlinにおける仕様の草稿(と思われるもの)は以下の文書です。

https://github.com/Kotlin/kotlin-coroutines/blob/master/kotlin-coroutines-informal.md

examples/ にあるのは、coroutinesがどのように実装されるのかのスケッチにみえます。つまり、coroutinesを使ったコードがコンパイルされる結果の例で、標準ライブラリに含まれるであろうヘルパーコードの実装も含みます。

https://github.com/Kotlin/kotlin-coroutines/tree/master/examples

検討は始まったばかりであり、Kotlinへのコルーチンの実装はまだまだ先になりそうですが、このリポジトリをみるとコルーチンの振る舞いをある程度予想できるようになっているため、少し解説してみましょう。


さて、コルーチンは、あるメソッドを、ローカル変数などの状態を維持したまま中断・再開させる機能です。C# の yieldasync/await はコルーチンの一種です。最近はJavaScriptにも yieldasync/await が導入され、babelなどのトランスパイラで利用できます。

さて、ここでは yield (ジェネレータ)を見てみましょう。

Kotlinの場合、 yield は次のように使います。gen() の戻り値は [1, 2] という反復子を表す Iterator<Integer> となります。

fun gen() = generate {
    println("yield(1)")
    yield(1)
    println("yield(2)")
    yield(2)
    println("done")
}

次の f()gen() を使うメソッドです。

fun f() {
    val iter = gen() 
    iter.next(); // "yield(1)" を出力したあと 1 を返す
    iter.next(); // "yield(2)" を出力したあと 2 を返す
}

まず、gen() を呼び出すと即座にジェネレータを生成して呼び出し元の f() に返します。次の iter.next() により gen() の実行が再開して println("yield(1)") が呼ばれたあと、 yield(1) によってまた gen() の実行が中断され、 f() に戻り、 iter.next() の戻り値として 1 を得ます。

そして次の iter.next() によって再び gen() の実行が再開され、 yeild(2) によって一時的に中断して呼び出しもとにもどり、 iter.next()2 を返します。

iterIterable<T> なので、拡張for文で反復することもできると思われます。 Iterator<T> と異なり、関数内のコンテキストを維持したまま値を生成することができるため、可読性の高い Iterator<T> を定義できるといえます。


さてこの yeild ですが、JVMやDalvikで実行する以上はバイトコードレベルの特別な機能は期待できません。したがって、Kotlinそのものでも表現できるようなコード変換が行われるだけだと思われます。

この変換結果は、たとえば次のファイルにあります。コメント内が yield を使ったコード、その他の部分がヘルパー実装とコンパイル後の実装と思われます。日本語のコメントは筆者によります。

https://github.com/Kotlin/kotlin-coroutines/blob/dd07d35/examples/yield.kt

// ...

fun main(args: Array<String>) {
/*
    fun gen() = generate {
        println("yield(1)")
        yield(1)
        println("yield(2)")
        yield(2)
        println("done")
    }
*/
    // 上記コメントのコードが変換されるのが class __anonymous__ となる
    fun gen() = generate({__anonymous__()})

    // gen() を使うコード。戻り値は普通の `Iterator<T>` として使える
    println(gen().joinToString())

    val sequence = gen()
    println(sequence.zip(sequence).joinToString())
}

// LIBRARY CODE
// Note: this code is optimized for readability, the actual implementation would create fewer objects
// yieldの実装を担うライブラリコード(可読性のため多少いじってあり、本物のコードではない、と書いてある)

// ジェネレータ生成用メソッド
fun <T> generate(@coroutine c: () -> Coroutine<GeneratorController<T>>): Sequence<T> = object : Sequence<T> {
    override fun iterator(): Iterator<T> {
        val iterator = GeneratorController<T>()
        iterator.setNextStep(c().entryPoint(iterator))
        return iterator
    }
}

// ジェネレータヘルパの実体で、 `Iterator<T>` を実装している
class GeneratorController<T>() : AbstractIterator<T>() {
    private lateinit var nextStep: Continuation<Unit>

    override fun computeNext() {
        nextStep.resume(Unit)
    }

    fun setNextStep(step: Continuation<Unit>) {
        this.nextStep = step
    }


    @suspend fun yieldValue(value: T, c: Continuation<Unit>) {
        setNext(value)
        setNextStep(c)
    }

    @operator fun handleResult(result: Unit, c: Continuation<Nothing>) {
        done()
    }
}

// GENERATED CODE
// メソッド内でyeildを使うと次のようなクラスに変換される

class __anonymous__() : Coroutine<GeneratorController<Int>>, Continuation<Any?> {
    private lateinit var controller: GeneratorController<Int>

    override fun entryPoint(controller: GeneratorController<Int>): Continuation<Unit> {
        this.controller = controller
        return this as Continuation<Unit>
    }

    override fun resume(data: Any?) = doResume(data, null)
    override fun resumeWithException(exception: Throwable) = doResume(null, exception)

    /*
        generate {
            println("yield(1)")
            yield(1)
            println("yield(2)")
            yield(2)
            println("done")
        }
     */
    // gen() が中断・再開しているように見えるのは、
    // ジェネレータの実体がステートをもつオブジェクトであり
    // label (現在のメソッドの位置) により移動できるように振る舞うから
    private var label = 0
    private fun doResume(data: Any?, exception: Throwable?) {
        when (label) {
            0 -> {
                if (exception != null) throw exception
                data as Unit
                println("yield(1)")
                label = 1 // 次の「位置」を更新
                controller.yieldValue(1, this) // yield(1) に相当
                // このあとdoResume() は中断して、再開後はlabel=1を処理する
            }
            1 -> {
                if (exception != null) throw exception
                data as Unit
                println("yield(2)")
                label = 2
                controller.yieldValue(2, this) // yield(2) に相当
                // このあとdoResume() は中断し、再開後は label=2 を処理する
            }
            2 -> {
                if (exception != null) throw exception
                data as Unit
                println("done")
                label = -1 // ジェネレータの終了
                controller.handleResult(Unit, this)
            }
            else -> throw UnsupportedOperationException("Coroutine $this is in an invalid state")
        }
    }
}

このように、 yieldIterator<T> を実装する方法の一つですが、素のままだと非常に煩雑なコードをシンプルに書けることが期待できます。Kotlinに実装されるのが楽しみですね。

コルーチンについては C# に一日の長があります。以下の書籍は2012年のものですが、このようなコルーチンをつかったコードの変換についても触れており、参考になります。

ViewPagerを使った読書画面の基本的な実装

電子書籍関係者で勉強会をやったので資料を公開します*1

speakerdeck.com

追記: Fragmentの状態の復帰はFragment#onViewStateRestred(Bundle)) でできるのではという指摘をいただきました。試したところ想定通り動いたのでコードの方は修正済みです。

デモ実装は https://github.com/gfx/TinyPdfReader で、以下のようなことが実装されています。詳細は資料をご覧ください。

  • ピンチイン・ピンチアウトでのズーム(PhotoViewを使用)
  • 画面の左右タップでの移動
  • Landscape時の見開き(1画面2ページ)
  • 見開きでも表紙は1ページだけでセンタリングする
  • 画面回転時の読書位置の保存
  • ページロード時の非同期画像読み込み +ふわっと表示
  • 全画面表示(immersive mode) & 画面中央タップでトグル

その他、TinyPdfReaderでは有効にしていませんが、RVP: ReversibleViewPagerというライブラリを作ったのでその紹介もしています。

読書画面はいろいろ工夫しがいがあって面白いですね。

*1:勉強会自体は非公開ですが、資料は公開しても問題ないように作りました。

Android N preview-2 でJava8 Streamが実装された!

Android N preview-2 がきましたね。

Android N Developer Preview 2, out today! | Android Developers Blog

AOSPにはまだtagがきていませんが、masterブランチをみるとStream APIが実装されています。

追記: android-n-preview-2 tagがきてました android-n-preview-2 - platform/libcore - Git at Google

ojluni/src/main/java/java/util/stream - platform/libcore - Git at Google

もちろんOptionalもきてます。preview-1のときになかったのは、単に移植が間に合わなかったというだけみたいですね。

ojluni/src/main/java/java/util/Optional.java - platform/libcore - Git at Google

ただし、これらが使えるのは minSdkVersion=24 に設定したときだけです。とはいえいずれ使えるようになることが明らかになった以上、Lightweight-Stream-API などのbackport libraryなどで慣れておくのがいいでしょう!

新しいAndroid FrameworkのAPIもOptionalに対応しているといいのですが、そこまではまだ調べていません。とりいそぎ速報でした。

See Also

gfx.hatenablog.com

Google Maps Android APIでsetMyLocation()を正しく設定する

AndroidアプリでGoogle Mapをライブラリとして使うGoogle Maps Android APIというのがGoogle Play servicesにあるのですが、こいつの setMyLocation() まわりがここ1年でずいぶん変わりました。

Android 6.0 / Google Play services 8.4.0現在、これを正しく設定する方法を調べたので記録しておきます。

なおこれらのtipsは半径Nキロメートルという物件検討用メモアプリを作る際に調べたもので、このアプリにはもう適用済みです。

実装については、「半径N」では当初PermissionDispatcher を使おうとしたのですが、パーミッションチェックのコード量が減るわけではないので結局すべて自前で実装することにしました。


さて、まず問題は2つ。それぞれ見ていきます。

  1. GoogleMap#setMyLocationEnabled() のruntime permissions対応
  2. GoogleMap#getMyLocation()GoogleMap#setOnMyLocationChangeListener() がdeprecatedになった対応

GoogleMap#setMyLocationEnabled() のruntime permissions対応

GoogleMap#setMyLocationEnabled() はruntime permissionsを要求するようになりました。ACCESS_FINE_LOCATION または ACCESS_COARSE_LOCATION (以降は総称してlocation permissionsとします)が必要です。

なお GoogleMap自体はlocation permissionsがなくても動作する ので、まず本当にMyLocation(=デバイスの位置情報)が必要かどうかを検討してください。「半径N」の場合はMyLocationは必須ではないため、パーミッションの要求が拒否されても動作するようにしました。

これの対応の概要は以下のとおりです。

  • setMyLocationEnabled() を呼び出す前にパーミッションのチェックと requestPermissions() の呼び出しを行う
  • Activity#onRequestPermissionsResult()requestPermissions() の結果を受け取って、GRANTEDであればもう一度 setMyLocationEnabled() の呼び出しを行う

今回は shouldShowRequestPermissionRationale() は使いませんでした。

requestPermissions()

runtime permissionsのフローは Android 6.0 の Runtime Permissions (M Permissions) に対応するためのアクティビティ図 - visible true がよくまとまっています。

requestPermissions() は、パーミッションが必要なタイミングでコントローラが行います。パーミッションの必要なメソッドを呼ぶとIDEがlint errorを出し、IDEにしたがってコードテンプレートを生成すればだいたい合ってます。

今回は、以下のようなテンプレートが生成されます。

if (ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION)
        != PackageManager.PERMISSION_GRANTED
        && ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION)
        != PackageManager.PERMISSION_GRANTED) {
    // TODO: Consider calling
    //    ActivityCompat#requestPermissions
    // here to request the missing permissions, and then overriding
    //   public void onRequestPermissionsResult(int requestCode, String[] permissions,
    //                                          int[] grantResults)
    // to handle the case where the user grants the permission. See the documentation
    // for ActivityCompat#requestPermissions for more details.
    return;
}
map.setMyLocationEnabled(true);

コメントに書いてあるとおり、 requestPermissions() を呼んで onRequestPermissionsResult() で権限付与を処理しろとありますね。これにしたがうと以下のようなコードになります。

static final int RC_LOCATION_PERMISSIONS = 0x01;

static final String[] PERMISSIONS = {
        Manifest.permission.ACCESS_FINE_LOCATION,
        Manifest.permission.ACCESS_COARSE_LOCATION
};

void setMyLocationEnabled() {
    if (ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION)
            != PackageManager.PERMISSION_GRANTED
            && ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION)
            != PackageManager.PERMISSION_GRANTED) {
        ActivityCompat.requestPermissions(this, PERMISSIONS, RC_LOCATION_PERMISSIONS);
        return;
    }
    map.setMyLocationEnabled(true);
}

@TargetApi(Build.VERSION_CODES.M)
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    if (requestCode == RC_LOCATION_PERMISSIONS) {
        onRequestLocationPermissionsResult(permissions, grantResults);
    }
}

@DebugLog
void onRequestLocationPermissionsResult(String[] permissions, int[] grantResults) {
    int[] granted2 = {PackageManager.PERMISSION_GRANTED, PackageManager.PERMISSION_GRANTED};
    if (Arrays.equals(permissions, PERMISSIONS) && Arrays.equals(grantResults, granted2)) {
        // 権限を取得したのでもう一度setMyLocationEnabled()を呼び出す
        setMyLocationEnabled();
    } else {
        // 権限を取得できなかったので諦める
        Toast.makeText(this, "No location permissions granted", Toast.LENGTH_LONG).show();
    }
}

厄介なのは、パーミッションを要求するメソッドを呼び出すたびに checkSelfPermission() しなければならないことです。そうしないと、lint errorになります。したがって、パーミッションを要求するメソッドはすべて checkSelfPermission() を呼び出すコードでラップして、そのラッパーメソッドを呼び出すようにするのがいいでしょう。

GoogleMap#getMyLocation()GoogleMap#setOnMyLocationChangeListener() がdeprecatedになった対応

runtime permissionsと直接は関係ないと思いますが、GoogleMapでMyLocationを取得するメソッドが非奨励になりました。かわりにGoogleApiClientから取得しなければいけません*1。GoogleMapはたとえ setMyLocationEnabled(true) していても自動でMyLocationにカメラを移動したりはしないので、「カメラの初期位置をMyLocationにする」というだけのためにGoogleApiClientを使うことになります。

「半径N」の場合は位置情報まわりの操作をPlaceEngineというクラスにまとめているので少しごちゃごちゃしていますが、シンプルに実装すると以下のようになるでしょう。 getLastLocation() はlocation permissionsを要求するので、そのハンドルがまた必要です。「半径N」の場合はマップのカメラの初期位置を設定するだけなので、情報がとれなければそのまま諦めています*2

 void initiGoogleAPiClient() {
    googleApiClient = new GoogleApiClient.Builder(context)
            .addApi(LocationServices.API)
            .addConnectionCallbacks(new GoogleApiClient.ConnectionCallbacks() {
                @Override
                public void onConnected(@Nullable Bundle bundle) {
                    handleLastLocation();
                }

                @Override
                public void onConnectionSuspended(int i) {
                    Timber.i("GoogleApiClient connection suspended");
                }
            })
            .addOnConnectionFailedListener(new GoogleApiClient.OnConnectionFailedListener() {
                @Override
                public void onConnectionFailed(@NonNull ConnectionResult connectionResult) {
                    Timber.w("GoogleApiClient connection failed: %s", connectionResult.getErrorMessage());
                }
            })
            .build();
}

private void handleLastLocation() {
    if (ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION)
            != PackageManager.PERMISSION_GRANTED
            && ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION)
            != PackageManager.PERMISSION_GRANTED) {
        return;
    }

    Location currentLocation = LocationServices.FusedLocationApi.getLastLocation(googleApiClient);
    if (currentLocation != null) {
        // コントローラがLatLngを受け取ってくれることを期待してイベントを投げる
        castMyLocation(new LatLng(currentLocation.getLatitude(), currentLocation.getLongitude()), true);
    }
}

さらなる工夫

ざっくり必要最低限の実装をしましたが、さらなる工夫もできます。

まず実サービスの場合、 shouldShowRequestPermissionRationale() を使ってパーミッションが必要な理由を説明すべきです。

また、location permissionsの場合、高精度位置情報(ACCESS_FINE_LOCATION)と低精度位置情報(ACCESS_COARSE_LOCATION)の いずれか が必要なのですが、パーミッション取得の説明画面で高精度位置情報を落とすオプションを選ばせることも、技術的には可能だと思います。実装は大変ですが、サービスの性質次第では必要かもしれません。

*1:Android Frameworkのandroid.locationtというのも存在しますが、奨励されません。参考: Making Your App Location-Aware | Android Developers

*2:実際には、SIMやlocaleなどから位置情報を取得しようとしますが、正確な情報はとれないので諦めているも同然です。

Hugoの @DebugLog アノテーションが @TargetApi を無効化するようにみえる件

Hugo便利ですね!ただtraceするだけのログを吐いたりメソッドの実行時間を測定するためにコードを書かなくてよくなるのでだいぶ使い勝手がいいデバッグ用ライブラリだと思います。

さて、Hugoを併用しているプロジェクトで@TargetApi をつけているのにlintがNewApiエラーを報告するという問題がありました。どうしたものかと思ったんですが、これはHugoの@DebugLogを同時につけているせいでした。Hugoがbytecode weavingでメソッドのバイトコードをいじり、その結果lintからみると@TargetApiなしで新しいAPIを呼んでいるようにみえるようです。@DebugLog を消せばlintエラーは消えます。

Android Orma v2.4.0でtype adapterを一つのクラスに集約できるようにした

Orma v2.4.0をリリースしました。差分は大きいですが、アノテーションプロセッサのリファクタがほとんどです。

Ormaのtype adapterは static type adapters というやつで、コンパイル時にtype adapterの検索と呼び出しの埋め込みを行うので動的に処理するものとくらべて高速でしかもコンパイル時にエラーを検出できるという特徴があります。

これについては以前少し説明したことがありました。

Android ORMでオブジェクトの埋め込みはどのように実装するか - Islands in the byte stream

Orma v2.4.0 では、このtype adapterを一つのクラスに集約できるようになりました。どのみちstatic methodしか使わないのですし、集約するほうが見通しがいいと思います。

構文としては、本当はJava8のrepeating annotationsができればいいのですが、それはできなかったので同じことをJava8以前の構文でやる方法をとりました。 MutableIntMutableLong のtype adapterを一つのクラスにまとめるとこんな感じです。補完をつかって書けないのがつらいところですが、それはstatic methodを使う以上は仕方ないですね。

@StaticTypeAdapters({
    @StaticTypeAdapter(
        targetType = MutableInt.class,
        serializedType = int.class,
        serializer = "serializeMutableInt",
        deserializer = "deserializeMutableInt"
    ),
    @StaticTypeAdapter(
        targetType = MutableLong.class,
        serializedType = long.class,
        serializer = "serializeMutableLong",
        deserializer = "deserializeMutableLong"
    )
})
public class TypeAdapters {

    public static int serializeMutableInt(@NonNull MutableInt target) {
        return target.value;
    }

    @NonNull
    public static MutableInt deserializeMutableInt(int deserialized) {
        return new MutableInt(deserialized);
    }

    public static long serializeMutableLong(@NonNull MutableLong target) {
        return target.value;
    }

    @NonNull
    public static MutableLong deserializeMutableLong(long deserialized) {
        return new MutableLong(deserialized);
    }
}

Gsonの処理を爆速にするStaticGsonをリリースした

コード生成でGsonをMoshiより高速化する - Islands in the byte stream の続きです。

GitHub - gfx/StaticGson: Static Gson binding library with annotation processing

三行まとめ

  • StaticGsonはannotation processingでコード生成してGsonを高速化する拡張で、結果はLoganSquareより少し遅い程度
  • 欠点はメソッド数+バイナリサイズのオーバーヘッド
  • 利点はGsonと互換性の高さ。モデルにアノテーションをつけてGson初期化でオプションを一つ与えるだけ

解説

リフレクションで処理していたところをコード生成にして高速化する手法は思ったより効果ありそうだな、ということでjcenterにリリースしました。以下のように依存指定すると使えます。

dependencies {
    apt 'com.github.gfx.static_gson:static-gson-processor:0.9.4'
    compile 'com.github.gfx.static_gson:static-gson:0.9.4'
}

使い方は簡単で、シリアライズ対象のモデルに @JsonSerializable をつけて:

@JsonSerializable
public class Book {
  // ...
}

GsonBuilderにtype adapter factoryを与えるだけ:

Gson gson = new GsonBuilder()
        .registerTypeAdapterFactory(StaticGsonTypeAdapterFactory.newInstance())
        .create();

setDateFormat()などのGsonBuilderのoptionはだいたい効きますが、FieldNamingPolicyなど一部のリフレクションの絡む機能は別途@JsonSerializableに与える必要があります。READMEにもあるとおりこんな感じ。

このシリアライズ名はコンパイル時に決定されるので、proguard避けのために@SerializedName を設定する必要はありません。速度よりもこっちのほうが個人的には嬉しい。

// LOWER_CASE_WITH_UNDERSCORES: 名前は各パーツをlower caseにしてアンダースコアで繋げるものとする
@JsonSerializable(
  fieldNamingPolicy = FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES
)
public class User {
    public String firstName; // serialized to "first_name"
    public Stirng lastName; // serialized to "last_name"
}

benchmarkはLoganSquare のものに手を加えてStaticGson対応させたもので測定してこんな感じ。Web APIのパースが多いだろうからパースに関する結果を載せています。Gsonよりも30%くらいは高速で、Jacksonより速くてLoganSquareより遅いという水準ですね。すでにGsonを使っているアプリに手軽に適用できるし、Gsonのオプションもほぼそのまま有効なので、導入が低コストという意味ではいいかもしれません。新規で使うならLoganSquareのほうが高速ではありますが、様々な場合にちゃんと対応できるかどうかまでは調べてないのでそこはよくわかりません。

I/O MultiplexingでAndroidのための効率のよい画像ローダをつくる検証

モバイルアプリのスレッドプールサイズの最適化(画像読み込み編) - クックパッド開発者ブログ

これに対する「I/O多重化すればスレッド数とか気にしなくていいんじゃないの」という意見を聞いて、それもそうだなと思ったので検証してみました。

前提知識

  • IO多重化にはjava.nio.channels.Selectorjava.nio.channels.SocketChannel を使う
  • Selector は Perlの IO::Select によく似たインターフェイスと機能を持つ
    • つまり select(2) ないし同等のシステムコールへのインターフェイス
    • Androidの場合に使われるのは epool(2) かも
  • SocketChannelSelector で多重化できるsocket
  • Androidのdocumentはあまりないが、Oracleのドキュメントがあるので参考になる
  • Oracle JDK7にはNIO.2というものがあってより多重化をやりやすいらしいけど、Androidにはないので関係ない

結果

とりあえず最低限HTTP 1.0で複数の画像のURLをリクエストしてレスポンスをパースしてviewに表示させるところまではできました。response headerのパースを真面目にやってないので、このままプロダクションで使うのは無理ですが、とりあえず検証はできるのでこれでよしとします。サポートしているプロトコルは HTTP/1.0 だけで、HTTPSは扱えません。

実機*1で検証したところと、シリアルにリクエストを送る場合は1000msくらいかかるところ、IO多重化したほうは300msくらいで済みます。この実装だと1つのスレッドを専有するうえにプライオリティも考慮しませんが、実験的なコードとしては十分でしょう。

というわけで、AndroidでもIO多重化は有効だとわかりました。最強の画像ローダを実装する場合は、画像のロードはIO多重化してシングルスレッドで行い、デコードはCPUの数だけスレッドを作って行うのが理想的だと思われます。

*1:Xperia Z4 / Android 5.0.2 / LTE