はじめに
最近、新しい市場を求めてサービスを海外展開しようとしている会社が増えてきています。サービスを海外展開するにあたって気にしなければならないことがいくつかありますが、そのうちの一つに通信環境があります。私は先週までインドネシアにいましたが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にはどのような機能があるのか、私の主観でインタフェースをグルーピングしました。
Picassoは、開発者が「すべてのユースケースに対して機能を追加していたらAPIはひどいものになる」と言っている通り、画像を読み込むということに対してシンプルでfluentなAPIを提供することにフォーカスしています。
Glideは、デコードやサンプリング周りの機能が豊富で、レスポンスをバイナリと見なしてどのようにハンドリングするかというところにフォーカスしているようです。
次に、パフォーマンスに影響を与えそうなThread PoolとBitmap Poolの実装を見ていきます。
Thread Pool
非同期で処理を行うライブラリはProducer Consumerパターンを採用しているものが多いです。チャネルのスケジューリング、ワーカースレッドの設定、HTTP通信を行うものであればHTTPクライアントの設定、キャッシュ戦略などでパフォーマンスに差が出ます。
Thread Poolはワーカースレッドを使い回すための仕組みです。JavaにはThreadPoolExecutorというクラスがあり、そのコンストラクタでThread Poolの設定をします。
new ThreadPoolExecutor(
corePoolSize, // アイドル状態でもキープしておく最低限のスレッド数
maximumPoolSize, // スレッド数の最大値
keepAliveTime, // アイドル状態になってからスレッドを終了するまでの時間
timeUnit,
workQueue, // スレッドが処理するためのタスクのキュー
threadFactory // 新しいスレッドを作るときのファクトリー
);
Picassoの場合
Picassoは corePoolSize == maximumPoolSize
で keepAliveTime == 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を外部から渡すことができ、エミュレータにはメモリとネットワークスピードとレイテンシを変える設定があるので、アプリ毎にチューニングしていくしかないと思います。(たとえばこのように: モバイルアプリのスレッドプールサイズの最適化)
たとえば3G環境で試したいならUMTS(Universal Mobile Telecommunications System)に設定します。
チャネルのスケジューリング
チャネルにはスタック、キュー、優先順位付きキューなどが使われますが、よく使われるのはJavaの優先順位付きキューの実装のPriorityBlockingQueueです。
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の場合
PicassoではPriorityの設定は手動で行う必要があります。リクエストのPauseもそれなりにコストがかかるのと、リスト画面は何度も開くものなのでリスト画面のActivityがPauseされてもリクエストはそのままにします。その代わりに詳細画面のファーストビューの画像のPriorityをHIGHにして真っ先に読み込むようにします。詳細画面は一度閉じたら同じ画面を開くことは少ないので、画面を閉じたらリクエストはキャンセルしてしまって良いでしょう。
Glideの場合
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を使うことができます。
再利用可能な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
まとめ
Glideは開発者が何もしなくても自動で色々やってくれたり多機能ですが、その分Picassoと比べてメモリを多く使ったりライブラリのサイズが大きかったりするので、厳しい環境に対応する必要がある場合はPicassoをカスタムして使うのが良いと思います。何事もトレードオフです。