ネットワーク通信用ライブラリVolleyを使いこなす


Androidネットワークプログラミング用ライブラリ「Volley」を解説します。

モバイルアプリを開発するにあたってネットワーク通信の知識は欠かせないものとなっている一方、ネットワークプログラミングの世界にはキャッシュや高速化、データ取得やキャンセル処理などプログラミングテクニックが多数存在してます。これらの課題を効率的に解決する方法がVolleyライブラリです。

mhidaka-network

Volleyの機能紹介とともにキャッシングやキャンセル処理などネットワークプログラミングに欠かせない処理をVolleyの実装をつかって順番に解説していきます。

非常に長い記事ですので始めに理解を深めるための内部処理を紹介します。APIなど詳細は記事の途中で随時解説します。

ネットワークへのリクエスト送信

volley_request
アプリケーション(ライブラリ利用者)がリクエストする場合、URLなどが入ったRequestを生成してRequestQueueに追加します。その際、Requestは優先度付FIFOでキューイングされます。標準で5MBのディスクキャッシュを備え、キャッシュにあればネットワークへ問い合わることはありません。ネットワークアクセスは並列化されています(4スレッドが最も適当)。

ネットワークからレスポンス受信

volley_response
サーバーからのレスポンスはキャッシュが有効であれば登録してから、ライブラリ利用者へコールバックを返します。Volleyライブラリではリクエストのコールバック処理はUIスレッドでの実行が保証されています。返却されたデータをパースしたり、画像であればViewを更新したりします。

Volleyの詳細、APIの使い方は続きをどうぞ

Volleyライブラリの利点と使い方

Android SDKでは非同期処理(またはマルチスレッド化)のための機構が用意されています。AsyncTaskService、それらを拡張したAsyncTaskLoader、IntentServiceが代表的です。さらに汎用的に使えるHandler、ApacheのHttpClient、JavaのExecutorService、Threadなど非同期処理のための仕組みが豊富に存在していますが非同期でのキャンセル処理や取得済みデータのキャッシュ方法など周辺技術と組み合わせて設計しないといけません。

Android SDKの標準APIは応用力が高いのですが、習得するまでの難しさ、エラー処理、各バージョンごとの実装差異が存在しています。これらの課題を解消するためにVolleyライブラリが作られました。

全てのユースケースで有効というわけではありませんが、アプリケーションで最も汎用的なワークフローをカバーできるように設計されています。次の図をみてみましょう。
mhidaka-network2
例えばニュース表示アプリ、Twitterアプリのように、XMLファイルやJSONなどのデータ取得、内容を解析したのち画像など詳細情報を再取得、ユーザーへ表示するといった処理フローを少ない労力で簡単に実現できるように作られています。

ただし、リクエストの途中結果を知る手段がないため、ストリーミングには利用できません。キャッシングやダウンロードの多重化などの問題があるため、大きいファイルのダウンロードにも向いていません。それぞれ適切なフレームワークが用意されているため(Mediaフレームワーク、DownloadManagerがあります)、別の手段で実装することをお勧めします。

Volleyライブラリにはネットワーク通信の優先度制御、並列処理数、キャンセル処理、プロキシ設定など多くの機能があります。利用にあたって有用なクラス、考え方を解説します。

  • RequestとRequestQueue
  • ImageLoarderやNetworkImageView

Step1.リクエストの発行と受け取り

RequestRequestQueueは主にデータ取得のため使います。WebAPIを経由して(ニュース情報など)
取得できたデータを解析すると表示すべき画像や文字列などが得られます。更にVolleyライブラリではリクエストの優先順位制御も可能(Requestクラスのサブクラスとして実装が必要)です。

Step2.表示データの取得とキャッシュ処理による高速化

通常、データの中には画像や文字列のための情報がたくさん入っています。画像が対象であればVolleyのImageLoarderNetworkImageViewを利用します。多くの画像を読み込み、メモリをたくさん消費することが分かっている場合、キャッシュやキャンセル処理が重要です。Volleyライブラリには様々な仕組みが導入されており、たとえばImageLoarderであればメモリキャッシュが利用でき、パフォーマンスを向上できます。

RequestとRequestQueueを使う

さっそくリクエストを発行し、JSONやXMLデータを取得してみます。VolleyライブラリにはJSONのようなデータやテキストファイル、画像を取得するなど用途に合わせてリクエスト(Request)が定義されています。
それらリクエストを管理するキューがRequestQueueです。

実際に利用しているサンプルコードで確認してみましょう。

private RequestQueue mQueue;

@Override
protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_main);

  // 東京都の天気情報
  String url =
    "http://weather.livedoor.com/forecast/webservice/json/v1?city=130010";

  mQueue = Volley.newRequestQueue(this);
  mQueue.add(new JsonObjectRequest(Method.GET, url, null,
    new Listener<JSONObject>() {
        @Override
        public void onResponse(JSONObject response) {
            // JSONObjectのパース、List、Viewへの追加等
        }
    },

    new Response.ErrorListener() {
        @Override public void onErrorResponse(VolleyError error) {
            // エラー処理 error.networkResponseで確認
            // エラー表示など
        }
    }));
}

リクエストを管理するキューは原則として1つだけ生成します。ActivityのonCreateメソッドでVolleyクラスのnewRequestQueueメソッドを呼び出すとRequestQueueのインスタンスを取得できます。
ディスクキャッシュのディレクトリパス、ネットワーク設定などデフォルト値で設定されます。RequestQueueのコンストラクタを利用しても同じことができますがnewRequestQueueメソッドを使うほうが便利です。

リクエストを追加するためにはRequestQueueクラスのaddメソッドを使います。このサンプルではJSON形式のデータを処理するためJsonObjectRequestがリクエストにあたります。
リクエスト結果はリスナー経由で通知され、onResponseメソッドやErrorListenerメソッドでViewへの反映やJSONのパースを行います。onResponseメソッドは成功時のresponseが渡されます。

RequestQueueとJsonObjectRequestのAPIリファレンス(一部抜粋)は次の通りです。

Volley#newRequestQueueメソッドによるリクエストキューの作成

メソッド名 説明
Volley#newRequestQueue(Context context) RequestQueueデフォルト設定で生成する
Volley#newRequestQueue(Context context, HttpStack stack) ネットワーク設定(プロキシ等)を反映したRequestQueueを取得する


リクエストキューはキャッシュ機能を備えています。Volley#newRequestQueueメソッドを使ってインスタンスを取得した場合、RequestQueueのディスクキャッシュに5MBが割り当てられます(DiskBasedCache.java参照)。

またVolley#newRequestQueueメソッドの第2引数のネットワーク設定の方法は
Volleyのネットワーク設定を変更する でプロキシ設定方法が解説されていますので参考にしてください。

JsonObjectRequestは名前の通り、JSONデータを扱うことに特化しています。

  • JsonObjectRequest(int method, String url, JSONObject jsonRequest, Listener<JSONObject> listener, ErrorListener errorListener)
  • JsonObjectRequest(String url, JSONObject jsonRequest, Listener<JSONObject> listener, ErrorListener errorListener)

JsonObjectRequestの引数一覧

引数 説明
method Method.POST、Method.GETを指定
url リクエスト先のURL文字列
jsonRequest リクエスト先へ一緒に引き渡すJSON。なければnullを指定
listener レスポンスを受け取るリスナーを登録
errorListener 処理の失敗を受け取るリスナーを登録。処理を省略する場合はnullを指定


最も利用頻度が高いJsonObjectRequestクラスを紹介しましたが、他のリクエストの場合でも引数が多少変わるだけで処理の流れは変わりません。Volleyはまだまだ日本語ドキュメントが少ないため、代表的な使い方から試して吸収していくとよいでしょう。

リクエストの種類

前述の解説では追加するリクエストにJsonObjectRequestクラスを利用しました。Requestのサブクラスは他にもたくさんあり、Requestクラスを継承することで自分でリクエストをカスタマイズできます。

リクエスト名 説明
ClearCacheRequest キャッシュのクリア要求
ImageRequest Bitmap用
JsonRequest JSON処理用の抽象クラス
JsonArrayRequest JSON Array用
JsonObjectRequest JSONオブジェクト用
StringRequest 文字列用

VolleyではBitmap、String、JSONオブジェクトのリクエストが実装されています。WebAPIのレスポンスにはXMLフォーマットが採用されてものも多くあります。この場合、独自にXMLリクエストを実装することになりますが、Volleyを使ってXMLを処理する が参考になります。

リクエストに共通して用意されているメソッドは次の通りです。

  • Request#setRetryPolicy() : 通信のリトライ条件を指定
  • Request#setShouldCache() : キャッシュ可否の指定
  • Request#getPriority() : リクエストの優先度取得(com.android.volley.Request.javaではNORMAL)

ImageLoarderとNetworkImageViewを使う

Volleyライブラリでは用途に応じていくつかの画像ファイルの取得方法があります。リクエスト単位であればImageRequestクラスが使えますが画像取得に特化した効率的な方法としてImageLoarderNetworkImageViewが用意されています。

単一のViewに表示する用途ではNetworkImageViewが便利です。反対にGridViewやListViewなど複数の要素を表示するにはImageLoarderを使うことになります。NetworkImageViewの内部でもImageLoarderが動いているのですが使用時には気にしなくていいように作られています。

サンプルコードを使ってNetworkImageViewの使い方を解説します。

■activity_niv.xml – レイアウトファイル

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:tools="http://schemas.android.com/tools"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  tools:context=".NivActivity" >

  <com.android.volley.toolbox.NetworkImageView
    android:id="@+id/network_image_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    />

</RelativeLayout>

まず初めにレイアウトファイルにNetworkImageView要素を追加します(もちろんActivityでインスタンス生成しても問題ありません)。このサンプルではRelativeLayoutを使って画面いっぱいにNetworkImageViewを表示します。

■NetworkImageViewへ画像を読み込む

@Override
protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_niv);

  RequestQueue queue = Volley.newRequestQueue(this);
  String url = "https://techbooster.org/wp-content/uploads/2013/08/densi.png";

  NetworkImageView view = (NetworkImageView) findViewById(R.id.network_image_view);
  view.setImageUrl(url, new ImageLoader(queue, new LruCacheSample()));
}

画像を読み込むにはNetworkImageViewクラスのsetImageUrlメソッドで読み込み対象のURLとキャッシュを指定します。画像は自動的にネットワークへアクセスしてダウンロードが終わった段階で表示されます。

NetworkImageViewクラスは、その名前の通りネットワークから画像を取り込む用途に特化しています。読み込み完了までのローディング画面やエラー画面で表示するリソースも指定できます。

NetworkImageViewクラスの主なメソッド

メソッド名 説明
setImageUrl ネットワークから画像を取得する
setDefaultImageResId 画像表示までの表示リソースを指定する
setErrorImageResId エラー時の表示リソースを指定する


NetworkImageViewはキャンセル処理も含めてネットワークへのアクセスを隠ぺいしています。Viewの表示・非表示に合わせて挙動を変えており、レイアウト上で非表示(detach)になればリクエストを自動的にキャンセルしています。また画面回転でレイアウトが破棄されるような場合もキャッシュがあるため、素早く復帰できます。

ImageLoarderを使って複数画像を取得する

ImageLoaderクラスはVolleyライブラリの中でも最も実践的なクラスです。前述のNetworkImageViewでは1つのViewに対する画像読み込みでしたが情報の一覧表示などの用途ではGridViewやListViewが使われます。ImageLoaderクラスを使えば、ListViewの各要素(Item)の挙動に合わせたロジックも簡単に作れます。ImageLoaderでは画像をネットワークから順次読み込み、さらに一度読み込んだ画像はキャッシュされるためパフォーマンス面でも大きなメリットがあります。

サンプルコードでImageLoaderの使い方を確認してみましょう。

■ListViewActivity – ListViewにImageLoarderAdapterを登録する

RequestQueue mQueue;

@Override
protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_main);

  // 画像取得できるURLのリスト
  ArrayList<String> urlList = new ArrayList<String>();
  ListView listView = (ListView) findViewById(R.id.listView);

  // ImageLoaderをもっているアダプタを設定
  mQueue = Volley.newRequestQueue(this);
  ImageLoaderAdapter adapter = new ImageLoaderAdapter(this, urlList, mQueue);
}

サンプルではImageLoaderAdapterクラスを作成してListViewに登録します。
ImageLoaderはAdapterクラス内で実装しています。

■ImageLoaderAdapter.java – ImageLoaderAdapterを生成する

public class ImageLoaderAdapter extends ArrayAdapter<String> {

  private RequestQueue mQueue;
  private ImageLoader mImageLoader;
  private LayoutInflater mInflater;

  public ImageLoaderAdapter(Context context, List<String> objects,
      RequestQueue queue) {
    super(context, 0, objects);
    mInflater = (LayoutInflater) context.
        getSystemService(Context.LAYOUT_INFLATER_SERVICE);

    mImageLoader = new ImageLoader(queue, new LruCacheSample());
  }
  ...省略...
}

ImageLoaderの生成はアダプタのコンストラクタで行います。ImageLoaderを生成するにはリクエストキューとキャッシュを引数で与えます。キャッシュは画像の一次的な保存領域として利用します。開発者はメモリキャッシュを実装する必要がありますがここではImageLoaderの解説を中心に行い、キャッシュについてはあとで解説します。

次のサンプルコードではgetViewメソッドのなかでImageLoaderを使ってネットワークから画像を取得しています。
ListViewの各Itemの表示内容(View)はアダプタのgetViewメソッドで生成します。
getViewメソッドが呼び出されるタイミングはListViewの初回表示時(1番目からn番目のItem分、連続して呼び出されます)や、スクロールで新しいItemを表示するときです(追加分のItemのみ呼び出されます)。

■ImageLoaderAdapter.java – ネットワークから画像を取得する

static class ViewHolder {
  ImageView image;
  TextView body;
}

@Override
public View getView(int position, View convertView, ViewGroup parent) {
  ViewHolder holder;

  if (convertView == null) {
    convertView = mInflater.inflate(R.layout.listview_item, parent, false);

    holder = new ViewHolder();
    holder.body = (TextView) convertView.findViewById(R.id.body);
    holder.image = (ImageView) convertView.findViewById(R.id.image);

    convertView.setTag(holder);
  } else {
    holder = (ViewHolder) convertView.getTag();
  }

  String url = getItem(position);
  holder.body.setText(url);

  ImageListener listener = ImageLoader.getImageListener(holder.image,
      android.R.drawable.spinner_background /* 表示待ち時の画像 */,
      android.R.drawable.ic_dialog_alert /* エラー時の画像 */);

  mImageLoader.get(url, listener); /* URLから画像を取得する */

  return convertView;
}

getViewメソッドではListViewの各要素であるItemの表示内容を作ります。サンプルではレイアウトファイル(R.layout.listview_item)からconvertViewに展開しています。
ImageLoaderクラスのgetImageListenerメソッドでリスナーを作成、getメソッドでurlから画像を取得します。

ここでは、RequestQueueはImageLoaderAdapterの中で生成せず、コンストラクタで受け取っていますが
実際にはActivity側でRequestを発行して取得したデータを元にListViewのItemを登録するはずです。

そのときは、RequestQueueをActivityから受け取るほうがよいでしょう。
VolleyライブラリではRequestQueueをシングルトンとして扱うように設計されているため、内部構造を理解しないまま複数利用するとRequestQueueのネットワーク設定やリクエスト制御が混線してしまいます(詳細は後述)。

ImageLoaderの主なメソッドは次の通りです。

ImageLoaderクラスの主なメソッド

メソッド名 説明
getImageListener ImageListenerの取得。ダウンロード待ち画像、エラー画像を設定可能
get 画像の取得。最大サイズ(縦、横)を指定したら縮小してくれる
isCached 既にキャッシュされている画像か判定できる
setBatchedResponseDelay 標準100ms、実行タイミングを変更できる

リクエストの開始と終了

リクエストはAsyncTaskのように撃ちっ放しの処理を行うこともありますが、Volleyではリクエストキューに貯めて(FIFO,リクエストの優先度を考慮して)処理しています。サンプルコードではライフサイクルのonPauseとonResumeメソッドでリクエストの開始と終了を行っています。

■Activityでのリクエスト開始/終了処理

private RequestQueue mQueue = null;

@Override
protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_listview);
  mQueue = Volley.newRequestQueue(this);
}

@Override
protected void onPause() {
  super.onPause();
  if(mQueue != null){
    mQueue.stop();
  }
}

@Override
protected void onResume() {
  super.onResume();
  if(mQueue != null){
    mQueue.start();
  }
}

他のアプリが起動するまえに呼び出されるonPauseメソッドのタイミングでリクエストキューの処理を停止し、再描画されるonResumeメソッドのタイミングで再開します。VolleyライブラリではサンプルコードのようにRequestQueueクラスのstartメソッド、stopメソッドで明示する以外にも、VolleyクラスのnewRequestQueueメソッドでは暗黙的にリクエストキューを開始しています(com.android.volley.toolbox.Volleyクラスの66行目にてリクエストキューを制御してます)

Volleyライブラリの場合、リクエストキューはBlockingQueueとして実装されています。UIスレッドではリクエストをキューに追加してワーカースレッド側ではキューを読み取り、順次リクエストを処理しています。
ワーカスレッドはネットワーク通信を行う、またはキャッシュから探すなど役割ごと存在しています。

VolleyライブラリではリクエストのレスポンスはすべてUIスレッドに送られており、リクエストの結果(onResponseやErrorListener)についてもUIスレッドで処理されます

リクエストのキャンセル

Requestをキャンセルする

Volleyではリクエスト単位でキャンセルを発行できます。まずはサンプルコードでRequestのキャンセル処理を確認してみましょう。

■リクエストのキャンセル処理例

  private void cancelRequest(JsonObjectRequest requset){
      requset.cancel();
  }

holderなどを使ってリクエスト発行側でリクエストのインスタンスを保存している場合は直接Requestクラスのcancelメソッドが使えます。

キャンセルされた場合は、リクエストの応答が返ってこないことが保証されています。
大げさな処理があるわけではなくリクエストの内部ではフラグ(mCanceledにtrueを)設定しているだけです。

mhidaka-RequestCancel

フラグを使うだけ、というシンプルな作りになっている理由はUIスレッド側での処理を最小限にするためです(ワーカースレッドの応答を待っていてはUIスレッド側での動作が滞ってしまうためです)。

ImageLoaderをキャンセルする

ImageLoaderを使っている場合のキャンセル方法は次の通りです。

■ImageLoaderAdapter.java -ImageLoaderのキャンセル処理

public class ImageLoaderAdapter extends ArrayAdapter<String> {
  private ImageContainer mImageContainer;
  ...省略...

  public View getView(int position, View convertView, ViewGroup parent) {
    ...省略...
    holder.container = mImageLoader.get(url, listener);

    //キャンセル時(Item単位のキャンセル)
    holder.container.cancelRequest();

    return convertView;
  }
}

前述のサンプルにならってImageLoaderAdapterでの実装例です。ImageLoaderから直接のキャンセルは用意されていませんが、ImageLoaderのgetメソッドの返り値であるImageContainerを利用します。
ImageContainerではリクエスト(画像のダウンロード)を管理しており、cancelRequestメソッドを実行したタイミングでリクエストがInFlight状態であればキャンセルを実施します(飛行中状態と表現しています。通信処理中またはアクティブな状態を指します)。

RequestQueueでRequestをキャンセルする

最後にRequestQueueのキャンセル処理を解説します。もっとも使い勝手が良い方法ですがキャンセルしたいリクエストを特定する仕組みに特徴があります。

■RequestQueueのキャンセル処理(1)

private void cancel(Object tag){
  //request.setTag(Object tag)と同じオブジェクトを探す
  mQueue.cancelAll(tag);
}

タグを指定して同じオブジェクトを持つリクエストをすべてキャンセルする方法です。リクエストキューに追加する前にリクエストに何らかの目印を付けておきます。

目印となるタグはRequestクラスのsetTagメソッドで設定できますが、オブジェクト型の比較でキャンセル対象のリクエストを探すのでsetTag(new String(“MARKED”));のように参照を保持できない渡し方をすると(equalsメソッドを使ってくれるわけではないので)キャンセルしてるつもりでできていない事態が発生します。
勘違いがないように気を付けてください。

実際、使い勝手を良くする方法も用意されています。次のサンプルコードはリクエストのキャンセル可否を判断するフィルターを用意しています。

■RequestQueueのキャンセル処理(2)

mQueue.cancelAll(filter);

RequestFilter filter = new RequestFilter() {

  @Override
  public boolean apply(Request<?> request) {
    // リクエストの内容に合わせて処理
    // キャンセル時は return true;
    return false;
    }
};

RequestFilterを使えばリクエストキューにあるリクエストをフィルタリングしてキャンセル可否を指定できます。
フィルタは汎用性が高く、文字列を比較したければ、リクエストにあるsetTag/getTagメソッドで操作して、String#equalsメソッドを使って比較する手順になります。

リクエスト/レスポンスのコンテキスト

Volleyライブラリの設計はリクエスト/レスポンスの操作性を意識しています。UIスレッドからいつでもリクエストキュー(RequestQueue)へのリクエスト追加、キャンセルを処理できる構造です。リクエストを操作する際に通信用のスレッドを意識する必要はありません。

実際、リクエストをサーバーに投げてレスポンスを受け取る通信用のスレッド(NetworkDispatcher)は標準で4スレッド用意されています。
NetworkDispatchrは最も汎用性の高いThreadとHandlerを利用しており、レスポンスは(ExecutorDeliveryにより)Handler経由で通知されるためUIスレッドで処理されます(UIスレッドなのでViewを変更しても問題ありません)。

ただし、VolleyライブラリではRequestQueueを複数生成しての利用は推奨されません
RequestQueueをVolleyクラスのnewRequestQueueメソッドで生成した場合は、ディスクキャッシュが”volley”ディレクトリと固定値で決まってしまうため、キャッシュ処理での排他処理ができていません。
この点に関しては回避方法もあり、RequestQueueを自分で生成すると、キャッシュの場所を変更できます

■RequestQueueの生成例

RequestQueue queue = new RequestQueue(new DiskBasedCache(cacheDir), network);
queue.start();

しかし、複数のリクエストキューを持つことでAsyncTaskの複数スレッド処理など、わずらわしい処理から解放されたのにもかかわらず、制御が複雑化してしまいます。
結果的にVolleyの良さを失うことにつながりかねません。それに通信処理のボトルネックはネットワークの帯域にあることが多いため、むやみにキューを増やして(負荷を上げて)処理を遅くする必要もないでしょう。

より細かな粒度でリクエストキューが必要な場合は、ライブラリを使わず独自に処理したほうがよいですがコードは複雑になりがちです。
ちなみに、より複雑なリクエスト処理が必要なケースでは、たいてい相手サーバー特有のプロトコルやシーケンスがあって通信部分をライブラリ化するのには向いていません。

RequestQueueにディスクキャッシュを設定する

キャッシュは使用頻度の高いデータを保存しておき、必要になった段階で再利用する機構です。一般的にネットワークアクセスよりも内部ストレージへアクセスするほうが速く、内部ストレージにデータを保存しておけば次に利用する際はネットワークアクセスよりも格段に素早くデータを取り出せます。

VolleyライブラリでもRequestQueueがディスクキャッシュ機構を利用しています。

mhidaka-RequestFlow

キャッシュにヒットしたデータであればネットワークへ問い合わせを行わず素早く取得するため、応答性の向上、ネットワークリソースを節約できます。キャッシュ制御をアプリケーションに組み込もうとした場合、大掛かりになりがちですがVolleyライブラリの利点はキャッシュの生成・破棄、データ検索などを完全に隠ぺいしていることです。

開発者は背景にある複雑なキャッシュ制御を意識することなく恩恵を受けられるため、シンプルかつ効率的に開発が行えます。
Volleyライブラリでは次の手順でリクエスト操作することでキャッシュ(メモリやストレージなど)とネットワークを両立させています。

  1. リクエストをキューから取り出し、キャッシュを検索する
  2. キャッシュの有効期限を超えていれば使用せず、ネットワーク経由でデータを取得する
  3. 取得したデータをキャッシュに保存する、リクエストの結果を通知する

キャッシュ機構を設計する上で特に重要な要素はキャッシュの保存期間です。
Volleyライブラリでは「サーバー側で指定された有効期限に従う」という解決策をとっています(HttpHeaderParser.javaの97行目やCacheDispatcher.javaの110行目付近、HTTPヘッダより取得しています)

リクエストキャッシュの実装として次の2つが用意されています。

  • com.android.volley.toolbox.DiskBasedCache : ディスクキャッシュ
  • com.android.volley.toolbox.NoCache : キャッシュ未使用

VolleyクラスのnewRequestQueueメソッドを利用してリクエストキューを生成した場合はディスクキャッシュが使われます。RequestQueueクラスのコンストラクタを使う場合のみディスクキャッシュかキャッシュ未使用か選ぶことができます。

ImageLoaderにメモリキャッシュを設定する

ImageLoaderの内部ではメモリキャッシュの操作とリクエスト発行が行われており、データがメモリキャッシュに格納されていなければRequestQueueにリクエストを追加します。

mhidaka-RequestFlowByImageLoader

ImageLoaderにはメモリキャッシュのためのインターフェイスが用意されており、開発者が自由に実装できます。
反対にファイル入出力など処理をブロックする可能性のある操作は設計上、禁止されています。つまりImageLoaderはディスクキャッシュを利用できません。Volleyではリクエストにはディスクキャッシュを使い、ImageLoaderでは更にメモリキャッシュを追加することで、より高いパフォーマンスが出せる仕組みを持っています。

LruCacheクラスを使ったキャッシュの実装例を解説します。
LruCache(android.util.LruCache)はAndroid 3.1(API level 12)から利用できます。Android 3.1以前の端末のためにSupport Libraryにも同様のandroid.support.v4.util.LruCacheクラスが用意されています。
LRU(Least Recently Used)の名前が示すとおり、利用順に基づいてキャッシュが作られます。メモリキャッシュなので有効期限はアプリケーションの起動~終了までの間です。
使用頻度の高いデータはキャッシュの先頭に、逆に使われていないデータはキャッシュの後方に配置されます。LruChacheクラスの内部はLinkedHashMapを使って実装しています。

実際にImageLoaderでの実装例をみていきましょう。

■ImageLoaderAdapter.java – コンストラクタにキャッシュを渡す

public class ImageLoaderAdapter extends ArrayAdapter<String> {

  private ImageLoader mImageLoader;
  private LayoutInflater mInflater;
  public ImageLoaderAdapter(Context context, List<String> objects,
      RequestQueue queue) {
    super(context, 0, objects);
    mInflater = (LayoutInflater) context.
        getSystemService(Context.LAYOUT_INFLATER_SERVICE);

    mImageLoader = new ImageLoader(queue, new LruCacheSample());
  }
  ...省略...
}

ImageLoaderの利用方法は前節での解説どおりです。コンストラクタにLruCacheSampleを渡しています。
ここで登場するLruCacheSampleはVolleyライブラリで用意されているImageCacheクラスを継承してキャッシュ機構を実装しています。

■LruCacheSample.javaの実装

public class LruCacheSample implements ImageCache {

    private LruCache<String, Bitmap> mMemoryCache;

    LruCacheSample(){
        int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
        int cacheSize = maxMemory / 8;       // 最大メモリに依存
        // int cacheSize = 5 * 1024 * 1024;  // 5MB

        mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
            @Override
            protected int sizeOf(String key, Bitmap bitmap) {
                // 使用キャッシュサイズ(KB単位)
                return bitmap.getByteCount() / 1024;
            }
        };
    }

    // ImageCacheのインターフェイス実装
    @Override
    public Bitmap getBitmap(String url) {
        return mMemoryCache.get(url);
    }

    @Override
    public void putBitmap(String url, Bitmap bitmap) {
        mMemoryCache.put(url,bitmap);
    }
}

サンプルコードではLruCacheのキャッシュサイズを最大メモリの1/8と定義して初期化しました。機種に依存することなく容量をスケールするためのテクニックです。画像のURLをキーにBitmapデータをキャッシュに保存/取得しています。

ImageLoaderでキャッシュを使わない場合

ImageLoaderの場合は、キャッシュを使わない場合の設定は用意されていませんが簡単に実装できます。

■NoImageCacheSample.java – ImageCacheを利用しない場合

public class NoImageCacheSample implements ImageCache {

  NoImageCacheSample(){
  }

  // ImageCacheのインターフェイス実装
  @Override
  public Bitmap getBitmap(String url) {
      return null;
  }

  @Override
  public void putBitmap(String url, Bitmap bitmap) {

  }
}

サンプルコードのようにImageCacheインターフェイスの実装(putBitmapメソッドとgetBitmapメソッド)を空にするだけでキャッシュを無効化できます。

Volleyのパフォーマンスを最適化する

最後にではVoleyライブラリを使いこなすためのTipsを紹介します。

デバッグを有効化する

次のコマンドでVolleyライブラリそのもののデバッグ機能を有効化できます。adbでAndroid端末につなぎ、shellから入力してください。

adb shell setprop log.tag.Volley VERBOSE

スレッドプールの数を変更する

Volleyライブラリにおける通信スレッドの数は通常、4つです。
RequestQueueのコンストラクタRequestQueue(Cache cache, Network network, int threadPoolSize)でスレッド数を指定できます。細かい制御が必要であればこちらを使いましょう。基本的には4スレッドで十分ですが低速な、またはWi-Fiなどより高速な通信環境にあわせてカスタマイズが可能です。

■com.android.volley.RequestQueue.java

public class RequestQueue {
  ...省略...
  /** Number of network request dispatcher threads to start. */
  private static final int DEFAULT_NETWORK_THREAD_POOL_SIZE = 4;
  ...省略...
}

ImageLoaderでレスポンスのタイミングをチューニングする

通常、Volleyライブラリではリクエストを処理し終えればすぐに呼び出し元へ応答(レスポンス)を通知しています。
ただし、ImageLoaderではリクエストのレスポンス通知を遅延させる処理が入っており、初回のレスポンスの到着から100ms分をまとめて処理しています。

この遅延はコンテキストスイッチのコストを加味しており、スレッドを切り替えて実行を繰り返すより、まとめて描画を更新して反映することで効率化しようという試みです。
特にListViewやGridViewでサムネイルを表示するなどリクエストが多い場合に有効な仕組みです。

遅延量はImageLoaderクラスのsetBatchedResponseDelayメソッドで指定できます。パフォーマンスチューニングでは覚えておくとうれしい項目です。

ImageLoaderでレスポンス遅延を担当するbatchResponseメソッドを紹介します

■com.android.volley.toolbox.ImageLoader.java

...省略...
private void batchResponse(String cacheKey, BatchedImageRequest request) {
  mBatchedResponses.put(cacheKey, request);
  // すでにRunnableを配送済みの場合、batchResponseメソッドが呼び出されてもなにもしない
  if (mRunnable == null) {
    mRunnable = new Runnable() {
      @Override
      public void run() {
        for (BatchedImageRequest bir : mBatchedResponses.values()) {
          for (ImageContainer container : bir.mContainers) {
            // 既にキャンセルされたリクエストを無視する
            if (container.mListener == null) {
              continue;
            }
            if (bir.getError() == null) {
              // レスポンスを通知する
              container.mBitmap = bir.mResponseBitmap;
              container.mListener.onResponse(container, false);
            } else {
              container.mListener.onErrorResponse(bir.getError());
            }
          }
        }
        mBatchedResponses.clear();
        mRunnable = null;
      }
    };
    // 指定ミリ秒の遅延つきでポストする
    mHandler.postDelayed(mRunnable, mBatchResponseDelayMs);
  }
}
...省略...

batchResponseメソッドではmRunnableインスタンスがnullのときのみ処理を行っています。Handlerへの多重ポストを避けるためでバッチ処理が終わればmRunnableにはnull代入しています。少しテクニカルですが十分シンプルで分かりやすい実装になっています。

さいごに

Volleyライブラリについて解説しましたがいかがだったでしょうか。アプリケーションにおいてネットワーク通信は付帯的な作業であることがほとんどです。Volleyのようなライブラリを使うことで本当に必要な機能に注力できます。

また本記事は電子書籍Effective Android (正式版、9/30にアップデートがあります)収録予定のものをTechbooster用に再編集しています。本記事以外にも一線級のAndroid開発者が書き下ろしたノウハウ・テクニックが収録されています。興味がありましたら是非。

Effective Android
TechBooster
達人出版会
発行日: 2013-08-29
対応フォーマット: PDF, EPUB
2 Comments