PicassoとGlideのどちらを使うべきか?

  • 299
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

はじめに

最近、新しい市場を求めてサービスを海外展開しようとしている会社が増えてきています。サービスを海外展開するにあたって気にしなければならないことがいくつかありますが、そのうちの一つに通信環境があります。私は先週までインドネシアにいましたがAkamai Technologiesによるとのインドネシアの通信速度は日本の6分の1程度で、実行環境に厳しい制約があります。またFacebookによると、Facebookアプリの通信量のうち85%は画像が占めているというデータがあります。そこで、画像の読み込みを改善すると通信量をグッと減らせると思い、画像読み込みライブラリの比較をしました。

Picasso vs Glide

Androidでは、Squareが開発しているPicassoと、Bumptechが開発しているGlideというライブラリが有名で、使っている方も多いと思います。次のコードをご覧ください。

Picasso.with(this)
    .load(url)
    .fit()
    .transform(new CircleTransform())
    .into(imageView);
Glide.with(this)
    .load(url)
    .fitCenter()
    .transform(new CircleTransform())
    .into(imageView);

上のコードがPicasso、下のコードがGlideです。インタフェースが良く似ています。この二つのライブラリにはそれぞれどんな特徴があり、どちらを採用すべきなのかを検討しました。ちなみにPicassoとGlideのバージョンはそれぞれ2015年1月にcloneしてきたものになっています。

そもそも画像を読み込むとは

シンプルに実装すると下のようになると思います。

  • HTTPクライアントに画像のURLを指定してInputStreamを取得する
  • InputStreamをデコードしてBitmapにする
  • ImageViewにBitmapをセットする

これに加えて、ネットワーク通信をするので非同期で行いますが、リクエスト毎にワーカースレッドを生成するのはもったいないので使いまわしたい、同じURLから画像を読み込むのに毎回リクエストをしたりデコードしたくないのでメモリにキャッシュしたい、ディスクにもキャッシュしたい、画像を読み込んでいる間はプレースホルダーを表示したい、リクエストには優先度を付けたい、などの一般的なよく使われる機能を抽象化したものがPicassoやGlideなどの画像読み込みライブラリになっています。

PicassoとGlideの機能の比較

ではPicassoとGlideにはどのような機能があるのか、私の主観でインタフェースをグルーピングしました。

スクリーンショット 2015-02-15 15.44.09.png

Picassoは、開発者が「すべてのユースケースに対して機能を追加していたらAPIはひどいものになる」と言っている通り、画像を読み込むということに対してシンプルでfluentなAPIを提供することにフォーカスしています。
Glideは、デコードやサンプリング周りの機能が豊富で、レスポンスをバイナリと見なしてどのようにハンドリングするかというところにフォーカスしているようです。

次に、パフォーマンスに影響を与えそうなThread PoolとBitmap Poolの実装を見ていきます。

Thread Pool

非同期で処理を行うライブラリはProducer Consumerパターンを採用しているものが多いです。チャネルのスケジューリング、ワーカースレッドの設定、HTTP通信を行うものであればHTTPクライアントの設定、キャッシュ戦略などでパフォーマンスに差が出ます。

スクリーンショット 2015-02-15 15.19.54.png

Thread Poolはワーカースレッドを使い回すための仕組みです。JavaにはThreadPoolExecutorというクラスがあり、そのコンストラクタでThread Poolの設定をします。

new ThreadPoolExecutor(
        corePoolSize, // アイドル状態でもキープしておく最低限のスレッド数
        maximumPoolSize, // スレッド数の最大値
        keepAliveTime, // アイドル状態になってからスレッドを終了するまでの時間
        timeUnit,
        workQueue, // スレッドが処理するためのタスクのキュー
        threadFactory // 新しいスレッドを作るときのファクトリー
        );

Picassoの場合

Picassoは corePoolSize == maximumPoolSizekeepAliveTime == 0 の Fixed Thread Poolですが、コアスレッド数はネットワークの接続状況に依存します。

switch (info.getType()) {
    case ConnectivityManager.TYPE_WIFI:
    case ConnectivityManager.TYPE_WIMAX:
    case ConnectivityManager.TYPE_ETHERNET:
        setThreadCount(4);
        break;
    case ConnectivityManager.TYPE_MOBILE:
        switch (info.getSubtype()) {
            case TelephonyManager.NETWORK_TYPE_LTE:  // 4G
            case TelephonyManager.NETWORK_TYPE_HSPAP:
            case TelephonyManager.NETWORK_TYPE_EHRPD:
                setThreadCount(3);
                break;
            case TelephonyManager.NETWORK_TYPE_UMTS: // 3G
            case TelephonyManager.NETWORK_TYPE_CDMA:
            case TelephonyManager.NETWORK_TYPE_EVDO_0:
            case TelephonyManager.NETWORK_TYPE_EVDO_A:
            case TelephonyManager.NETWORK_TYPE_EVDO_B:
                setThreadCount(2);
                break;
            case TelephonyManager.NETWORK_TYPE_GPRS: // 2G
            case TelephonyManager.NETWORK_TYPE_EDGE:
                setThreadCount(1);
                break;
            default:
                setThreadCount(DEFAULT_THREAD_COUNT);
        }
        break;
    default:
        setThreadCount(DEFAULT_THREAD_COUNT);
}

Glideの場合

GlideもFixed Thread Poolになっていますが、コアスレッド数はCPUコア数に依存します。

Runtime.getRuntime().availableProcessors()

端末、環境とスレッド数の例

Picasso Glide
Nexus 6 in Tokyo 3 (LTE) 4 (Quad-core 2.7 GHz Krait 450)
Galaxy ace 3 in Jakarta 2 (3G) 2 (Dual-core 1 GHz Cortex-A9)
MiTO Impact (Android One) in Jakarta 2 (3G) 4 (Cortex A7 1.3 GHz Quad-Core)

どこで、どの端末で、どんなデータを読み込むかによって変わってくるのでこれが最適というのは難しいです。幸いどちらのライブラリもExecutorServiceを外部から渡すことができ、エミュレータにはメモリとネットワークスピードとレイテンシを変える設定があるので、アプリ毎にチューニングしていくしかないと思います。(たとえばこのように: モバイルアプリのスレッドプールサイズの最適化

Screen Shot 2015-02-11 at 21.09.35.png

たとえば3G環境で試したいならUMTS(Universal Mobile Telecommunications System)に設定します。

チャネルのスケジューリング

チャネルにはスタック、キュー、優先順位付きキューなどが使われますが、よく使われるのはJavaの優先順位付きキューの実装のPriorityBlockingQueueです。

スクリーンショット 2015-02-17 22.28.16.png

PicassoとGlideにはリクエストにPriorityを設定する機能がありますが、Priorityをセットすると上の図のように前に押し出されて先に処理されるようになります。

Picasso.with(this)
    .load(url)
    .priority(HIGH)
    .into(imageView);
Glide.with(this)
    .load(url)
    .priority(HIGH)
    .into(imageView);

実は、GlideにはLifecycle Integrationと呼ばれる機能があり、普通のスケジューリングとは少し異なっています。リスト画面から詳細画面に開いて、見終わったら詳細画面を閉じてまたリストから違うアイテムを選んで詳細画面を開く、というシナリオを想定して、どのようにPriorityをセットするべきかを考えてみます。

Picassoの場合

スクリーンショット 2015-02-15 15.22.50.png

PicassoではPriorityの設定は手動で行う必要があります。リクエストのPauseもそれなりにコストがかかるのと、リスト画面は何度も開くものなのでリスト画面のActivityがPauseされてもリクエストはそのままにします。その代わりに詳細画面のファーストビューの画像のPriorityをHIGHにして真っ先に読み込むようにします。詳細画面は一度閉じたら同じ画面を開くことは少ないので、画面を閉じたらリクエストはキャンセルしてしまって良いでしょう。

Glideの場合

スクリーンショット 2015-02-15 15.23.06.png

Glideはリクエストの管理を自動で行ってくれます。基本的には何もしなくて良く、特別に早く表示したい画像があればHIGHに設定すると良いでしょう。Glideは色々と自動でやってくれるので便利な反面、画面遷移時に強制的に元いた画面の読み込みがPauseになるのと、ライフサイクルのコールバックを受けるための監視用のFragmentがActivityに裏でaddされたりします。

Bitmap Pool

Bitmap PoolはBitmapのためのメモリの確保を効率良く行うための仕組みです。これは今のところはGlideにしか実装されていません。

たとえばリストビューの1つのアイテムのサムネイル (100dp * 140dp) を xxhdpi の端末で Bitmap.Config.ARGB_8888 として表示する場合は 168000 byte のメモリを必要とします。

(100 * 140 * 3) * (8 * 4) / 8 = 168000 byte

この分のメモリをリサイズしたり変換したりするなどして、何回も確保/解放するのはもったいないので、不要になり破棄される予定だったBitmapをメモリに取っておいて、新しいBitmapが要求されたときに再利用が可能だったら取っておいたBitmapを返すようにしたのがBitmap Poolです。Glideでは使う側が特に意識する必要なくBitmap Poolを使うことができます。

スクリーンショット 2015-02-15 15.23.41.png

再利用可能なBitmapには制限があります。OS 4.4より前の端末では画像のフォーマットがjpegかpngであり、かつBitmapのwidthとheightとConfigが同じである必要があります。OS 4.4以降では画像のサイズが同じであれば再利用することが出来るようになっています。なので、Glideを使う場合はなるべく同じサイズの画像を読み込むようにすると、Bitmap Poolが効果的に働きます。
Bitmap Poolの詳細な実装については、Android Developersに載っています。(Managing Bitmap Memory | Android Developers

そんなに良い機能であればPicassoにあっても良いのでは?と思いますがそれなりにリスクもあります。Picassoのissueでは、Glideの開発者も交えて5ヶ月前に議論されています。 Implement a Bitmap pool for Picasso · Issue #672 · square/picasso

まとめ

スクリーンショット 2015-02-15 15.23.57.png

Glideは開発者が何もしなくても自動で色々やってくれたり多機能ですが、その分Picassoと比べてメモリを多く使ったりライブラリのサイズが大きかったりするので、厳しい環境に対応する必要がある場合はPicassoをカスタムして使うのが良いと思います。何事もトレードオフです。