• home
  • 複数のRecyclerViewで巨大なリスト扱ってめちゃくちゃ死にまくったまとめ

複数のRecyclerViewで巨大なリスト扱ってめちゃくちゃ死にまくったまとめ

前提 : RecyclerView

いわゆるVirtualScroll実装。
Viewの初期化時の計算や転送コストを下げつつ、画面に表示されるリストを「見えてる分だけ」に制限してリストのガワを最大限再利用することで、レイアウトのレンダリングコストを下げる実装。
似たような機構はiOSやwebでの無限スクロール系ライブラリにもある。
VirtualDOM的なアレとは非なる。

アンチパターンと解決

画像の再利用をしない

  • 問題
    リストの行を表現するxml上で直接drawableフォルダ内のimageリソースを指定したりすると、レイアウトの初期化時にどんどんメモリが確保されていってやがてOOMを迎える。
    また、新しいリストの生成時にも画像の確保コストでUIスレッドがブロックし、なんかカクカクするようになる。

  • 解決
    面倒でもPicassoやGlideなどのライブラリを使いながら、コード側で随時画像キャッシュを使ってリスト内の該当箇所にimageを当てていく。
    と言うかAndroidにおいて直接imageviewにbitmapをアタッチすることは根本的にアンチパターンだと思っておく。確保時にOOMで死ぬ。

Picassoなどは内部にWeakMap?的な実装を持てるため、割り当てたメモリ上限を超えると勝手にアクセス頻度の少ないものは捨ててくれる。

// インスタンス実装
ActivityManager am = (ActivityManager) MainApplication.getApplication().getSystemService(Context.ACTIVITY_SERVICE);
boolean largeHeap = (MainApplication.getApplication().getApplicationInfo().flags & ApplicationInfo.FLAG_LARGE_HEAP) != 0;
int memoryClass = am.getMemoryClass();
if (largeHeap && Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
    memoryClass = am.getLargeMemoryClass();
}
int cacheSize = 1024 * 1024 * memoryClass / 8;
Picasso picasso = new Picasso.Builder(MainApplication.getApplication()).memoryCache(new LruCache(cacheSize)).build();
Picasso.setSingletonInstance(picasso);

// 画像当てこみ

Picasso.with(view.getContext())
    .load(hashMap.get("url"))
    .resize(size ,size)
    .into((ImageView) imageView);

リストの追加通知にとりあえずnotifyDataSetChanged()を叩く

  • 問題
    VirtualScrollとVirtualDOM的なアレと同じような感じでとりあえず adapter:::notifyDataSetChanged()を雑に叩くと計算コストが半端ない。
    予期せぬリストまでviewが再生成されたりする。
    今までのアプリでは実は表示するものの数が大したことないので割と問題なかったが、複数タブにそれぞれ複数のrecyclerView実装を抱えて、なおかつ巨大なリスト群を扱うとスレッドがブロックする。

  • 解決
    多少実装が複雑になっても myAdapter.notifyItemRangeInserted(lastCount , newCount); など適切に追加/変更を伝える必要がある。
    ただし最近はAdapterに食わせるviewModelにidを与えておくことで、雑に notifyDataSetChanged() を呼んでも問題ないような作りになってきているらしい(試してない)

スクロールリスナー動きすぎ問題

  • 問題
    無限スクロール実装のために、スクロール時に全体のリスト数と表示されているアイテムのindexを監視している。
    数十ms間隔でイベントが来る時とかもあって、そんなに毎回計算が要らない。

  • 解決
    現在時間を使う。 new Date().getTime() あたりを使って、適当に前の時間との差分を取りながらスロットルする。
    リスナー内であまり複雑なことをしないなら必要ないかも。

setHasFixedSize(true)を設定しない

  • 解決
    特に必要がなければデフォルトで設定しておく。

行内のインタラクションのためにnotify~系を叩く

  • 問題
    別の場所で起こったインタラクションなどをリスト内の行viewに反映させる時、前述の notifyDataSetChanged()だともちろん重い。
    アダプターの機構でrangeをとって更新でもいいけど正直管理が複雑なのと融通が利かないのがある。

  • 解決
    リストで表示された各行Viewの onAttachedToWindow() , onDetachedToWindow() ライフサイクルイベントで、適当なObserverパターン実装を購読し、各行viewが自分で差分を計算したほうが個人的には融通がきくと思われる。
    なんか負けた気がするけど。
    画面上に表示されているものだけになるので、observer側でたくさんイベントが走ろうが走査数は実は大したことないはず。

VideoView , 横スクロールページャーなどをリストの一部に実装する

  • 問題
    スライダーやVideoViewがリストの一部になると、画面から出た際に随時detacheされ、状態が初期化してしまう時。

  • 解決

 RecyclerView:::setItemViewCacheSize(i : int);

のサイズを必要な分大きくしておくか、リスト再生成時に、適切に状態を復元する機構を用意しないといけない。
特にVideoViewはPlayer部分とレンダリングされるViewを分離しなくてはいけないので、ちゃちゃっとビデオ表示したい用途だとだいぶ面倒くさそうだけど。
多分にステートフルなコンポーネントだとまあしょうがないという気はする。

無限スクロールでどんどんメモリに行情報溜まっていくよ問題

  • 問題
    別にAndroidに限ったことではないが、無限スクロール実装でだんだん表示すべきdataのリストが貯まっていく。

  • 解決
    リスト内で表示しないものは、行情報の取得時に別スレッド側であらかじめプロパティをどんどん捨てておく。サーバー側で抜けるならサーバー側で。
    description , リリースノートなど巨大な文字列を有するものには割と効く。
    あとは保持しているリストの上の方の過去のデータを適切に消したり、SharedPreference的なところに一時的に書き出しておく、などの実装が考えられる。結構複雑になりそうのでやってないけど。