RecyclerViewのリサイクル動作で使われるキャッシュは、サイズを大きくすれば多くのViewHolderが保持できます。その分、多くのメモリを消費します。
ViewHolderを多く保持できたとしても、サイクル動作で効率よく使われなければ、メモリの浪費です。
キャッシュのサイズはRecyclerViewの使われ方よって適切なサイズがあります。
そのため、RecyclerViewはキャッシュのサイズを変更できるようになっています。
RecyclerPoolの構成とデフォルトサイズ
RecyclerPoolはRecyclerView.RecyclerViewPoolクラスで管理されています。
図に示したような構成になっていて、mScrapを参照する時のKeyはViewTypeです。
※SparseArray<T>はInt型をKeyにしたHashMapです。T型のオブジェクトを格納します。
public class RecyclerView extends ViewGroup implements ScrollingView, NestedScrollingChild2, NestedScrollingChild3 { ... // 5635行近辺 public static class RecycledViewPool { private static final int DEFAULT_MAX_SCRAP = 5; ... static class ScrapData { final ArrayList<ViewHolder> mScrapHeap = new ArrayList<>(); int mMaxScrap = DEFAULT_MAX_SCRAP; long mCreateRunningAverageNs = 0; long mBindRunningAverageNs = 0; } SparseArray<ScrapData> mScrap = new SparseArray<>(); ... } ... // 5896行近辺 public final class Recycler { ... RecycledViewPool mRecyclerPool; ... // 6868行近辺 RecycledViewPool getRecycledViewPool() { if (mRecyclerPool == null) { mRecyclerPool = new RecycledViewPool(); } return mRecyclerPool; } ... } ... }
RecyclerPoolのサイズはScrapData#mMaxScrapフィールドが持っています。
ScrapDataデータクラスがインスタンス化された時、DEFAULT_MAX_SCRAPが代入されるのでデフォルトサイズは5になります。
RecyclerPoolのサイズ変更
サイズはRecyclerViewPool#setMaxRecycledViews( )メソッドを使用して変更できます。
ViewType毎に指定が可能です。
public class RecyclerView extends ViewGroup implements ScrollingView, NestedScrollingChild2, NestedScrollingChild3 { ... // 367行近辺 final Recycler mRecycler = new Recycler(); ... // 1486行近辺 public RecycledViewPool getRecycledViewPool() { return mRecycler.getRecycledViewPool(); } ... // 5635行近辺 public static class RecycledViewPool { ... // 5678行近辺 public void setMaxRecycledViews(int viewType, int max) { ScrapData scrapData = getScrapDataForType(viewType); scrapData.mMaxScrap = max; final ArrayList<ViewHolder> scrapHeap = scrapData.mScrapHeap; while (scrapHeap.size() > max) { scrapHeap.remove(scrapHeap.size() - 1); } } ... } ... // 5896行近辺 public final class Recycler { ... RecycledViewPool mRecyclerPool; ... // 6868行近辺 RecycledViewPool getRecycledViewPool() { if (mRecyclerPool == null) { mRecyclerPool = new RecycledViewPool(); } return mRecyclerPool; } ... } ... }
例えば、RecyclerPoolのサイズを7に変更したければ次のように記述します。
val _viewType = 0 // デフォルトのViewType rcySample.recycledViewPool.setMaxRecycledViews(_viewType, 7)
RecyclerPoolの適切なサイズ
キャッシュのサイズを変更できることがわかったところで、最適なサイズを考察してみようと思います。
例えば、アイテムが7つ表示されているRecyclerViewで全データを更新してみます。
全データの更新ですから、データの参照先を変更して、変更の通知はnotifyDataSetChangedで行います。
... val _items = mutableListOf( "Item 0", "Item 1", "Item 2", "Item 3", "Item 4", "Item 5", "Item 6", "Item 7", "Item 8", "Item 9", "Item A", "Item B", "Item C", "Item D", "Item E", "Item F" ) change(_items) ... } private fun change(items: MutableList<String>) { val _adapter = rcySample.adapter as ListAdapter _adapter.items = items // データの変更 _adapter.notifyDataSetChanged() // 変更の通知(表示を更新) }
... I/Adapter: View Detached item = ITEM 6 [pos=-1,type=0] ... I/Adapter: View Recycled item = ITEM 6 [pos=-1,type=0] ... I/Adapter: View Detached item = ITEM 5 [pos=-1,type=0] ... I/Adapter: View Recycled item = ITEM 5 [pos=-1,type=0] ※ポジション6~0まで同じ ... I/Adapter: Bind ViewHolder!! position = 0 ... I/Adapter: View Attached item = Item 0 [pos=0,type=0] ... I/Adapter: Bind ViewHolder!! position = 1 ... I/Adapter: View Attached item = Item 1 [pos=1,type=0] ... I/Adapter: Bind ViewHolder!! position = 2 ... I/Adapter: View Attached item = Item 2 [pos=2,type=0] ... I/Adapter: Bind ViewHolder!! position = 3 ... I/Adapter: View Attached item = Item 3 [pos=3,type=0] ... I/Adapter: Bind ViewHolder!! position = 4 ... I/Adapter: View Attached item = Item 4 [pos=4,type=0] ... I/Adapter: Create ViewHolder!! viewType = 0 ... I/Adapter: Bind ViewHolder!! position = 5 ... I/Adapter: View Attached item = Item 5 [pos=5,type=0] ... I/Adapter: Create ViewHolder!! viewType = 0 ... I/Adapter: Bind ViewHolder!! position = 6 ... I/Adapter: View Attached item = Item 6 [pos=6,type=0]
... I/Adapter: View Detached item = ITEM 6 [pos=-1,type=0]
... I/Adapter: View Recycled item = ITEM 6 [pos=-1,type=0]
... I/Adapter: View Detached item = ITEM 5 [pos=-1,type=0]
... I/Adapter: View Recycled item = ITEM 5 [pos=-1,type=0]
※ポジション6~0まで同じ
... I/Adapter: Bind ViewHolder!! position = 0
... I/Adapter: View Attached item = Item 0 [pos=0,type=0]
... I/Adapter: Bind ViewHolder!! position = 1
... I/Adapter: View Attached item = Item 1 [pos=1,type=0]
... I/Adapter: Bind ViewHolder!! position = 2
... I/Adapter: View Attached item = Item 2 [pos=2,type=0]
... I/Adapter: Bind ViewHolder!! position = 3
... I/Adapter: View Attached item = Item 3 [pos=3,type=0]
... I/Adapter: Bind ViewHolder!! position = 4
... I/Adapter: View Attached item = Item 4 [pos=4,type=0]
... I/Adapter: Create ViewHolder!! viewType = 0
... I/Adapter: Bind ViewHolder!! position = 5
... I/Adapter: View Attached item = Item 5 [pos=5,type=0]
... I/Adapter: Create ViewHolder!! viewType = 0
... I/Adapter: Bind ViewHolder!! position = 6
... I/Adapter: View Attached item = Item 6 [pos=6,type=0]
notifyDataSetChengedは更新なので、旧アイテムが新アイテムに置き換わります。新旧のアイテムはエレメントの情報が異なります。よって、DetachedされたViewHolderは同一のポジションへ再生利用できません。
直接RecyclerPoolへキャッシュされますが、デフォルトサイズが5のため、7つ中の5つはキャッシュされ、溢れてしまった2つは破棄されてしまいます。
... I/Adapter: View Detached item = ITEM 6 [pos=-1,type=0]
... I/Adapter: View Recycled item = ITEM 6 [pos=-1,type=0]
... I/Adapter: View Detached item = ITEM 5 [pos=-1,type=0]
... I/Adapter: View Recycled item = ITEM 5 [pos=-1,type=0]
※ポジション6~0まで同じ
... I/Adapter: Bind ViewHolder!! position = 0
... I/Adapter: View Attached item = Item 0 [pos=0,type=0]
... I/Adapter: Bind ViewHolder!! position = 1
... I/Adapter: View Attached item = Item 1 [pos=1,type=0]
... I/Adapter: Bind ViewHolder!! position = 2
... I/Adapter: View Attached item = Item 2 [pos=2,type=0]
... I/Adapter: Bind ViewHolder!! position = 3
... I/Adapter: View Attached item = Item 3 [pos=3,type=0]
... I/Adapter: Bind ViewHolder!! position = 4
... I/Adapter: View Attached item = Item 4 [pos=4,type=0]
... I/Adapter: Create ViewHolder!! viewType = 0
... I/Adapter: Bind ViewHolder!! position = 5
... I/Adapter: View Attached item = Item 5 [pos=5,type=0]
... I/Adapter: Create ViewHolder!! viewType = 0
... I/Adapter: Bind ViewHolder!! position = 6
... I/Adapter: View Attached item = Item 6 [pos=6,type=0]
アイテムの再表示では、RecyclerPoolから5つが再生利用されます。残りの2つはキャッシュ上に無いので新規作成になります。
キャッシュサイズ5に対して表示するアイテムが7つのため、ViewHolderの新規作成が必要になり、findViewById(重い処理)を含んだonCreateVireHolderを実行しなければなりません。
onCreateVireHolderの実行を無くすには、キャッシュサイズ7に変更して、更新前のアイテムを全てキャッシュ可能にします。
ですが …
最近のAndroid端末はとても高速なので、この程度の改善くらいでは体感できないことがほとんどです。
関連記事: