Epoxyのスクロールがかくつくので調べたらリサイクルされていなかった


この記事はand factory Advent Calendar 2019の24日目の記事です

きっかけ

とあるページの初回表示を高速化するためにをEpoxyのRecyclerView(以下、Epoxyと書きます)を使うようリファクタしました。

対象となるページの構成はこちらです。rootがLinearlayoutでその下にいくつかのViewをぶら下がっています。

LinearLayout // rootのViewGroup
  ∟ RecyclerView
  ∟ RelativeLayout
  ∟ RecyclerView
  ∟ RecyclerView
  ∟ LinearLayout
  ∟ ...
  • 要素毎にaddViewしています。ただ、要素数がそれなりに多く、ページ初回表示するときに画面外の要素の処理も動いていました。

  • Epoxyでは画面外要素の処理はページ表示した時に動かないので、ページ表示の高速化が期待できそうです

LinearLayoutをEpoxyに切り替える

  • 先ほどのrootに配置していたLinearLayoutをEpoxyに切り替えて、子ビューのコードをEpoxyModelWithHolderを拡張したクラスに移動しました

動作テスト

  • 起動時のページ表示が早くなった(体感)気がします。少なくとも画面外の要素の処理は走っていないです。

  • 古い端末だとスクロールでちょいちょいかくつきが気になります。

  • 上下のスクロールを何度繰り返してもカクツクので、キャッシュが効いていなさそうです。EpoxyのViewPool周りの実装をみていきました。

EppxyがViewPoolを管理している箇所

ActivityRecyclerPool.kt
internal class PoolReference(
    context: Context,
    val viewPool: RecyclerView.RecycledViewPool,
    private val parent: ActivityRecyclerPool
) : LifecycleObserver {
    private val contextReference: WeakReference<Context> = WeakReference(context)

    val context: Context? get() = contextReference.get()
  • Epoxyは独自でViewPoolを管理しているようです。RecyclerViewでは1つのActivityで保持するインスタンスがViewTypeごとに5つまでと制限されていますが、Epoxyはこの制限が外されています。この辺りの仕様からもEpoxyとRecyclerViewが別々の実装を持っていることがなんとなくわかります。

  • ActivityRecyclerPool.kt

RecyclerView内でViewPoolからキャッシュを取得するコード

RecyclerView.java
// この辺りでキャッシュを探しているようです(おそらく)

ViewHolder getScrapOrHiddenOrCachedHolderForPosition(int position, boolean dryRun) {
            final int scrapCount = mAttachedScrap.size();
// 省略..

            final int cacheSize = mCachedViews.size();
            for (int i = 0; i < cacheSize; i++) {
                final ViewHolder holder = mCachedViews.get(i);


  • RecyclerViewでも保持しているViewPoolからキャッシュを探しているようです
  • メソッド名からにじみ出ていますが、ViewPool以外でもキャッシュを保持しているクラスがいるようですね。adapterとかmChildHelperとか、どんな役割なのか気になります。
  • RecyclerView.java

原因

  • 今回のリファクタでrootのLinearLayoutをEpoxyに変更しましたが。子ビューで使っていたRecyclerViewはそのままだったのが原因でした。

  • Epoxyは1つのActivity内のEpoxy同士で描画したViewをViewPoolにキャッシュして次回からリサイクルできますが、RecyclerviewのViewPoolはEpoxyのものとは別管理のため毎回inflateが発生していたため、古い端末でかくつきが起こっていました。

対応

  • EpoxyとRecyclerViewの混在するとViewPoolの共有が上手くいかないことがわかったので、RecyclerViewをEpoxyに変更して古い端末でもカクツキが軽減されたことを確認しました。

まとめ

  • EpoxyはネストされたEpoxyでもViewPoolをデフォルトで共有してくれるので便利です。
  • Epoxy使っているのにカクツクことがある場合は、子ビューにRecyclerViewが紛れていないか確認すると良いかもしれません。
  • 公式でViePoolのことがちゃんと書いてあるので、しっかり読むの大事だなと思いました

参考