読者です 読者をやめる 読者になる 読者になる

Volleyについて調べる(1)

  • Volleyのベターな使い方

久しぶりにAndroidの仕事(他チームのヘルプ)を会社でちょびっとだけやり、その際、Volleyのベターな使い方について調査をしたので自分用にメモとして記録します。

Volleyとは

VolleyはAndroidの通信処理フレームワークです。 https://android.googlesource.com/platform/frameworks/volley/

Androidでの通信処理は、通信処理のキューイング、非同期でのデータ取得からのUIスレッドへの連携、キャッシュ(メモリーキャッシュ・ローカルキャッシュ)の扱い(特に画像を扱う場合)通信失敗時のリトライ機構、といった様々な課題を効率的に解決する必要があります。

その解決方法はAndroid固有の機構を各実装者が独自に組みあわせた「オレオレ設計」(悪い意味じゃないです)によって様々で、それが故にベストプラクティスが定まらない状況でした。

そんな時、Google I/O 2013で通信処理用のフレームワークが発表・公開されました。 基本コンセプトは「簡単で高速な通信フレームワーク」とのこと。 個人的にはGoogleの中の人が「通信する時は、これ使えば楽だよ」って言っているので、余程の事情が無い限りはこれ使った方が良いと思います。

Volleyの仕組み

ソースコードを見る限り、キャッシュと通信の各スレッドをプールさせておいて各通信処理をリクエストという単位でキューイングして処理を捌かせ、結果をUIスレッドにコールバックさせています。

  1. リクエストをキャッシュキューに登録
  2. ローカルキャッシュ上にリクエストがあればそれを返す (画像の場合は最初にメモリーキャッシュを捜索することもできる)
  3. キャッシュに無ければリクエストを通信処理のキューに登録
  4. 通信を実行、結果をキャシューに保存
  5. コールバックでUIスレッドに結果を返却

(ザックリ過ぎてごめんなさい。あと、間違っていたらごめんなさい)

基本的な扱い方

本質的にあまり意味ないので、割愛します。

RequestQueueのインスタンスはシングルトンで扱う

原則、RequestQueueのインスタンスはアプリ上でシングルトンで扱うのが望ましいと思います。

例えば4画面持つアプリ(各画面で通信が実行されるとする)があるとして、その各画面で下記のように通信を実行していたとします。

  1. 画面遷移時にRequestQueueのインスタンスを作成
  2. タスク(通信処理)をそのRequestQueueのインスタンスに投入
  3. 2のRequestQueueでタスクを実行

各画面で投入された通信タスクが実行終了する前に、次の画面へ遷移して、新しいRequestQueueのインスタンスに通信タスクが登録されて実行、そして、またその通信タスクが終わる前に次の画面・・・となった場合、各画面上にあるRequestQueueのインスタンス内部でThread(各RequestQueue内部はデフォルトで設定されるThreadの数は5つ 内訳→ 通信4:キャッシュ監視1 です)が生成されて処理を実行することになりますが、通信で画像をDLするといった比較的コストの高いタスクの場合は、平行して実行されるThreadの処理が4画面としたら、最大20 個近くとなり、仮にその各Thread内部でBitmapを扱うとなると、その先にOutOfMemoryErrorの発生が待ち構えていることは容易に想像できるでしょう。

(全ての画面がピンタレストライクなタイムライン画面だったらと想像すると「あー」とうなづけるかと思います)

というわけで、最初にシングルトンでRequestQueueのインスタンスを1つ作っておき、各画面でタスク(通信処理)を実行する場合に、そのRequestQueueインスタンスに逐一タスクを投入する感じが適切なのかなと思います。(随時動いているThreadの数は5つのまま)

  1. HelperクラスでReqestQueueを作成
  2. 各画面で1.のQueueインスタンスに通信タスクを投入
  3. 1.のQueueインスタンスで通信タスクが実行される

ヘルパークラスを作ってRequestQueueのインスタンスを管理するのが一般的なようです。

public class VolleyHelper{
    
    public static final Object lock = new Object();
    
    public static RequestQueue requestQueue;
    
    /**
     * RequestQueueのシングルトン生成
     * @param context アプリケーションコンテキスト
     * @return
     */
    public static RequestQueue getRequestQueue(final Context context) {
        synchronized (lock) {
            if (requestQueue == null) {
                requestQueue = Volley.newRequestQueue(context);
            }
            return requestQueue;
        }
    }

    /** 以下省略**/

}

タスクをキャンセルする

アプリ内の通信処理を全て1つのRequestQueueで捌くとなると、当然キュー上での処理待ちが発生します。 リモートの画像を通信で取得する頻度が高く、また画面数が多い場合には、処理待ちによる画像の表示遅延を招きユーザビリティーが損なわれてしまうでしょう。

その対策としては、画面遷移やスクロールといったユーザ導線の要所要所でRequestQueue上のタスクをキャンセルすることが挙げられます。

タスクキャンセルについてはcancelAll でできます。

cancelAll(RequestFilter filter)
cancelAll(final Object tag))

cancelAllはRequestFilterとtagによりオーバーロードされていて、この2種類の引数の粒度でキャンセルする処理をコントロールすることができます。 わかり易い粒度で考えるならばtag指定(特定tagのRequestだけキャンセルする)、キャンセルの細かい条件を定義したい場合などは、RequestFilter(Interface)をオーバーライドしたインスタンスで指定といった感じでしょうか。

 /**
     * A simple predicate or filter interface for Requests, for use by
     * {@link RequestQueue#cancelAll(RequestFilter)}.
     */
    public interface RequestFilter {
        public boolean apply(Request<?> request);
    }

下記はRequestにtagを設定してRequestQueueに登録しています

//  RquestQueueインスタンスを取得
RequestQueue queue = VolleyHelper.getRequestQueue(getApplicationContext());

//  リクエストの作成
ImageRequest imageGetRequest =  new ImageRequest(
                url,
                new Response.Listener<Bitmap>() {
                    @Override
                    public void onResponse(final Bitmap response) {
                        targetImage.setImageBitmap(response);
                    }
                }
                ,0
                ,0
                ,Bitmap.Config.ARGB_8888
                ,new Response.ErrorListener() {
                    @Override
                    public void onErrorResponse(VolleyError error) {
                        error.printStackTrace();
                    }
        });

// 今回は通信を実行する画面のクラス名をtag指定
String TAG = TimeLineFragment.class.getName();
// リクエストにtagを設定する
imageGetRequest.setTag(TAG);
// リクエストをRequestQueueに登録する
queue.add(imageGetRequest);

上記で指定したRequestをキャンセルするには下記の通りです。

//  RquestQueueインスタンスを取得
RequestQueue queue = VolleyHelper.getRequestQueue(getApplicationContext());
// imageGetRequestに設定したtagインスタンスを指定
requestQueue.cancelAll(TAG);

ちなみにRequestQueueのソースコードを確認する限り、tagの場合も内部的にRequestFilterを使っていることがうかがえますね。

 /**
     * Cancels all requests in this queue for which the given filter applies.
     * @param filter The filtering function to use
     */
    public void cancelAll(RequestFilter filter) {
        synchronized (mCurrentRequests) {
            for (Request<?> request : mCurrentRequests) {
                if (filter.apply(request)) {
                    request.cancel();
                }
            }
        }
    }

 /**
     * Cancels all requests in this queue with the given tag. Tag must be non-null
     * and equality is by identity.
     */
    public void cancelAll(final Object tag) {
        if (tag == null) {
            throw new IllegalArgumentException("Cannot cancelAll with a null tag");
        }
        cancelAll(new RequestFilter() {
            @Override
            public boolean apply(Request<?> request) {
                return request.getTag() == tag;
            }
        });
    }

ImageLoaderもシングルトンで持つ

ImageLoaderはImageRequestをラッピングしたクラスです。 簡単に非同期で画像の取得&ImageViewへの反映を行うことができます。 メモリーキャッシュを設定することができるのでRequestQueueのローカルキャッシュと併せて使うと良い感じのキャッシュ機構が実現できます。 (メモリーキャッシュはdalvikVMから割り当てられるヒープの大体8分の1ぐらいが適当でしょうか)

ImageLoaderについても画面単位では持たず、シングルトンで扱うのが一般的なようです。

なるほど、ImageLoaderのメモリーキャッシュの扱いから考えると、画面遷移する度にImageLoader分のインスタンスを生成していたら、その度にメモリーキャッシュ分のヒープが増加してしまいますね。 そうなると理論上、8個インスタンスを作ったら、アプリに供給されたヒープ全てがメモリーキャッシュ用途となり、画像をメモリーキャッシュし続けているうちに、突然OutOfMemoryErrorが発生するでしょう。 (逆にメモリーキャッシュであるImageCache実装クラスをシングルトンで扱うという手もありますが・・・)

  /**
     * ImageLoaderのシングルトン生成
     * @param context アプリケーションコンテクスト
     * @return
     */
    public static ImageLoader getImageLoader(final Context context) {
        if (mImageLoader == null) {
            int memoryHeap = ((ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE))
                    .getMemoryClass();
            int cacheSize = 1024 * 1024 * memoryHeap / 8;
            mImageLoader = new ImageLoader(getInstance(context), new BitmapCache(cacheSize));
        }
        return mImageLoader;
    }

メモリーキャッシュは下記のようなLruCacneの拡張クラスを指定してあげました。 ImageCacheのインターフェースを必ず実装してあげます。

public class BitmapCache extends LruCache<String, Bitmap> implements ImageCache{

    public BitmapCache(int maxSize) {
        super(maxSize);
        
    }
    
     /**
      * {@inheritDoc}
      */
     @Override
     protected int sizeOf(String key, Bitmap value) {
             return value.getRowBytes() * value.getHeight() / 1024;
     }
     
     /**
      * {@inheritDoc}
      */
    @Override
    public Bitmap getBitmap(String url) {
        return get(url);
    }

     /**
      * {@inheritDoc}
      */
    @Override
    public void putBitmap(String url, Bitmap bitmap) {
         put(url, bitmap);
        
    }

    /**
      * {@inheritDoc}
      */
    @Override
    protected void entryRemoved(boolean evicted, String key, Bitmap oldBitmap, Bitmap newBitmap) {
        if(oldBitmap.isRecycled() == false && evicted) {
                oldBitmap = null;
        }
    }

}

ちょっと長くなったので、このエントリーはここで終わりにします。 (疲れました)

とりあえずまとめると

  • 通信処理はVolleyを使うと良い
  • RequestQueueはシングルトンで扱う
  • Requestはユーザビリティーを考慮して適宜キャンセルする
  • ImageLoaderもシングルトンで扱う

次のエントリーはRequestのプライオリティとかについてメモしていきます。