Android
RecyclerView
glide
fresco

[Android] Glideを使ってRecyclerViewを表示するとき、パフォーマンス問題があるならFrescoを使おう

* 本記事は、Glide 4.5.0, Fresco 1.7.1の時に作成しました。(2018/01)

まとめ

Glideを信じていたが裏切られた。
RecyclerViewで大量に画像がある状態で、素早くスクロールするとOOMで落ちる。

GlideがRecyclerViewの動きに合わせて、内部的にBitmap pool処理をうまくクリアしてくれない(多分バグ)。
素早くスクロールした時に、スクロールして見えなくなったViewに対しても、裏で画像処理が走り、OOMが発生したり、OOMが発生しなくても、端末メモリ使用が増えて端末が重くなる。

Glideで色々チューニングして頑張っても上手くいかないので、
Frescoに変更したら、何も特別な処理を書かなくてもサクサク動いた!

はじめに

Androidで、画像ライブラリと言えば、Picasso, Glide, Fresco, Universal Image Loader の4つが多く使われる。

主流は、Picasso, Glideで、どっちを使うべきかは、この記事を読むととても良い。

Frescoは、Facebookが開発したライブラリで、まだ新しく情報も少ない。
Universal Image Loaderは、最新バージョンが2年前に出て、更新されていない状況なので、特にこれを選ぶ理由はない。

そんな中、私が最初Glideを選んだ理由はこうだった。

  • どの画像ライブラリよりも、バージョンアップが早く活発。継続的に改善されている
  • コミュニティが活性化されて、開発者との距離が近い。開発者から回答をもらうことも簡単にできる。(実際に質問してすぐに回答をもらったことがある)
  • ユーザーが多く、情報が多い。
  • 使い方がシンプル。ドキュメントも充実
  • Googleグループ会社(Bump)で、Googleの公式アプリでも使っているそうなので信頼度が高い

Glideが最強だと思っていた。
しかし、ある日、Glideの限界を発見したのである!

問題の発見

RecyclerViewに数千件の大量の画像を乗せて素早くスクロールすると、アプリが落ちる。

画面仕様

RecyclerViewをGrid状にして、大量の画像を表示する。
さらに、1つのItem(ViewHolder)には、1つの画像だけではなく、カバー画像やメイン画像など、何枚も画像が重なっている状態。
さらにそんなRecyclerViewをもつFragment画面が、ViewPagerで複数ある。
(ViewPagerのsetOffScreenPageLimit()は画面数にしてFragmentを破棄させない。)

本来ならRecyclerViewでViewをリサイクルしてくれるので、「見えない部分は、うまくGCされて、Glideもそれに合わせてクリアしてくれるはずで、画像の多さは問題にならない。。」と思っていたが、、

それは間違いだった!

上のリストを、素早くスクロールして、数千件の大量の画像を読み込んだら、OOMが発生してしまった。
Glideのデフォルトでは、RecyclerViewのviewholderにGlideをセットした場合、自動的にBitmap画像処理を中断してくれず、見えない部分までロードが止まらず走っているのである。

Glideのドキュメント(https://bumptech.github.io/glide/doc/getting-started.html) によると、「Glideが必要ないロードは自動的にクリアして上手くやってくれるから、気にしなくても良いよ!」と書いてあるので、心配しなくて良いと思えるが、実際は心配した方が良い。

Although it’s good practice to clear loads you no longer need, you’re not required to do so. In fact, Glide will automatically clear the load and recycle any resources used by the load when the Activity or Fragment you pass in to Glide.with() is destroyed.

(この嘘つき!!)

解決策: GlideでのRecyclerView使用時のベスト・プラクティス

実はこの問題は、Glideの開発者が提案する解決策がある。
recyclerViewのadapterのonViewRecycled()で、Viewに対してGlideを手動でクリアしてあげると大丈夫だと書いてある。(さっきのドキュメントでは、clear()メソッドは書かなくても良いと言ったのに!)
https://github.com/bumptech/glide/issues/1779
https://github.com/bumptech/glide/issues/1536
https://stackoverflow.com/questions/39946746/android-is-it-a-good-practice-to-clear-glide-manually-in-adapter-recyclerview

コードはこんな感じだ。

@Override
public void onViewRecycled(final ViewHolder viewHolder) {
    Glide.clear(viewHolder.getImageView());
}
// 余談だが onViewRecycledではなく、onViewDetachedFromWindow()でクリア処理を書いたらいけない。1回クリアした画像が再表示されなくなる

今の時点では、Glide+RecyclerViewでは、この方法がベストプラクティスであり、Glideの公式サンプルアプリでもこの方法が使われている。(なぜかドキュメントには書いてない)
https://github.com/bumptech/glide/blob/master/samples/flickr/src/main/java/com/bumptech/glide/samples/flickr/FlickrPhotoGrid.java#L97

しかし、この方法を使うと確かに緩和はされるが、完璧にクリアな状態にはしてくれない。
この方法を使っても、裏で死なないローディングがあり、数千件とかロードすると、スマホがカクカク重くなってしまう。

この時、Glideのメモリをクリアするメソッドがあるので、試してみたが、それを使っても効果なし。

// https://bumptech.github.io/glide/doc/caching.html
// To simply clear out Glide’s in memory cache and BitmapPool, use clearMemory
Glide.get(this).clearMemory();

おそらくこのメソッドは、ロード完了後のメモリキャッシュは消してくれるが、ロード中のは消してくれないんだと思う。

なので、Glideを諦めてFrescoに変えてみたら、特別な処理なしで、上手く行った!

Glideと比べて違う点。Frescoの特徴

1. Contextの指定がない。ContextはApplicationContextを使う

Glideでは with()のメソッドに、画像をロードするたびに、FragmentやActivity, ApplicationContextなどを渡して使っていた。
そうするとFragmentやActivityのライフサイクルに合わせて、メモリ管理を効率的、自動的にしてくれた。

しかし、Frescoではそういう概念ではなく、ApplicationContextを使う。MainのApplicationのonCreate()でFrescoを初期化すると、その後はContextを意識する必要がない。

では、Glideでやってくれてたライフサイクル的にはどうなるかというと、FrescoはApplicationContextを使うので、View単位でメモリ管理をしてくれる。
Viewが、画面から見えなくなったら、裏の画像処理を消して、メモリを管理している。
なので、先ほどのようににRecyclerViewに大量の画像がある時にも、問題なく上手く行ったと思う。
この点は、FragmentやActivityのライフサイクルに依存しているGlideより、メモリ管理が効率的だと感じた。

2. ImageViewを使わず、独自のCustomViewを使う

Frescoでは、SimpleDraweeViewという独自のViewを使う。ImageView継承ではなくView継承である。
カスタムビューなので、layout xmlで様々な設定ができて、Glideのようにコードで書かなくて良くなった。Viewの属性はXMLで書くという点は、よりAndroidらしい仕組みが可能になったと言える。

例えば、画像を正方形にしたいなら、xmlで

fresco:viewAspectRatio="1"

を設定するだけ。

丸くしたいなら、

fresco:roundAsCircle="true"

にするだけである

その他、設定できる項目は公式ドキュメントを。
http://frescolib.org/docs/using-simpledraweeview.html

3. Fileの絶対パスがロードできない

FileオブジェクトのgetAbsolutePath()で取得する絶対パスはschemeが含まれていないが、このようなurlはロードができない(必ずschmeが必要)。Glideならschemeなしでもロードできる。

例えば、"/data/user/0/com.example.myapp/files/main.jpg" というパスは、StringクラスにしてもUriクラスにしても、ロードができない。 "file:///data/user/0/com.example.myapp/files/main.jpg"ならFrescoでもロードできる。

なので、下記の方法などで渡す必要がある。

  • Fileオブジェクトで渡す
  • Uri.fromFile(file)で渡す
  • こういうメソッドを作る (あまりいけてない)
// kotlin
private fun convertFilePathCorrectly(data: String): String {
    return if (!data.contains(":")) {
        "file://$data"
    } else {
        data
    }
}

4. 画像読み込み中のProgress Barを簡単に入れることができる

http://frescolib.org/docs/progress-bars.html

5. gifや Progressive JPEGsイメージをロードできる

画像ライブラリの中で、唯一Progressive Jpegをサポートしている。
http://frescolib.org/docs/progressive-jpegs.html

最後に

Glideを使っていて、RecyclerViewで大量の画像を表示する時に、あまりパフォーマンスが出なくて悩んでるなら、Frescoを使ってみよう!

しかし、リストで大量の画像を表示する場合を除いて、一般的な場面ではGlideで十分なので、わざわざGlideからFrescoに乗り換える必要はないと考える。

以上。