RecyclerView のキャッシュ取得の仕組み
RecyclerView は、開発中によく使用されるコントロールです。彼の公式の定義は、「大規模なデータ セットに限られたウィンドウを提供するための柔軟なビュー」です。その定義には非常に目を引く大きなデータがあります。では、RecyclerView は、OM を使用せずに大量のデータをどのように処理するのでしょうか。遅れ?これは RecyclerView のキャッシュ メカニズムです。まず、いくつかの基本的な概念を見てみましょう。
- バインディング: サブビューには、アダプター内の位置に対応するデータが表示されます。
- リサイクル (ビュー): アダプター内の特定の場所にデータを表示するために以前使用されていたアイテムビューはキャッシュされ、後で同じタイプのデータを表示するために再利用されます。これにより、レイアウトの膨張と構築を回避できます。
- Scrap (view): レイアウト中に一時的に切り離された状態になるItemView。スクラップ ビューは、親ビュー RecyclerView を完全に切り離さなくても再利用できますが、再バインドする必要がない場合は変更できません。ビューがダーティであるとみなされる場合は、アダプタ Binding によってバインドできます。
- Dirty (ビュー): Dirty の ItemView は、アダプターによって再バインドされる必要があるビューを指します。
リサイクラークラス
RecyclerView には、再利用のために破棄または分離されたアイテム ビューを管理する内部クラス Recycler があります。まず、次のように Recycler のソース コードを見てみましょう。
public final class Recycler {
final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
ArrayList<ViewHolder> mChangedScrap = null;
final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();
private final List<ViewHolder> mUnmodifiableAttachedScrap = Collections.unmodifiableList(mAttachedScrap);
private int mRequestedCacheMax = DEFAULT_CACHE_SIZE;
int mViewCacheMax = DEFAULT_CACHE_SIZE;
RecycledViewPool mRecyclerPool;
private ViewCacheExtension mViewCacheExtension;
static final int DEFAULT_CACHE_SIZE = 2;
}
ソース コードから、このクラスには 5 つのメンバー変数があることがわかります。
- mAttachedScrap: アタッチされた親ビュー RecycleView のスクラップ ビューを保存します。これは再バインドせずに再利用できます。容量制限はありません。
- mChangedScrap: 変更された Scrap ビューを保存します。再利用する場合は、アダプターによって再度バインドする必要があります。
mChangedScrap 和 mAttachedScrap 只在布局阶段使用。其他时候它们是空的。布局完成之后,这两个缓存中的 viewHolder,会移到 mCacheView 或者 RecyclerViewPool 中。
- mCachedViews: 削除されたビュー、RecyclerView から分離されたビューを格納しますが、位置と Binding のデータ情報は引き続き格納され、デフォルトの容量は 2 です。
- mRecyclerPool: バインディングの痕跡を残さずに、工場出荷時の設定に復元されたビューを保存します。RecycledViewPool のソース コードを見てみましょう。デフォルトの容量が 5 であることがわかります。ViewHolder を格納する ScrapData 内部クラスがあり、データの種類に応じて別々に格納されることを示しており、RecyclerView のマルチシード レイアウトがどのように実装されているかがわかります。
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<>();
}
- mViewCacheExtension: このレベルのキャッシュ開発者はキャッシュを拡張できます。ViewCacheExtension は抽象クラスであり、必要に応じて独自の実装を定義できます。
public abstract static class ViewCacheExtension {
@Nullable
public abstract View getViewForPositionAndType(@NonNull Recycler recycler, int position, int type);
}
読み取りキャッシュ ルール
キャッシュのストレージ タイプと形式について説明した後、キャッシュのストレージ ルールを見てみましょう。RecyclerView パッケージの下には合計 38 個のクラス ファイルがあり、RecyclerView 自体のコードだけでも 13,501 行ありますが、すべてを読み取るのは現実的ではないため、リファレンスを参照して具体的なキャッシュ戦略を検討します。
まず、mAttachedScrap の使用状況を確認しました。下図に示すように、主な追加および削除メソッドは、 void scrapView (View ビュー) と void unscrapView (ViewHolder ホルダー) であることがわかります。私たちは使用方法の参考資料を見つけるという点で引き続き調査を続けます。
最後に、メソッド tryGetViewHolderForPositionByDeadline(int Position, boolean dryRun, long DeadlineNs) を見つけました。このメソッドの優れた呼び出しは getViewForPosition(int Position, boolean dryRun) であることがわかりました。そのソース コードを見てみましょう。
ViewHolder tryGetViewHolderForPositionByDeadline(int position, boolean dryRun, long deadlineNs) {
// 0) If there is a changed scrap, try to find from there
if (mState.isPreLayout()) {
holder = getChangedScrapViewForPosition(position);
fromScrapOrHiddenOrCache = holder != null;
}
// 1) Find by position from scrap/hidden list/cache
if (holder == null) {
holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
}
// 2) Find from scrap/cache via stable ids, if exists
if (mAdapter.hasStableIds()) {
holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
type, dryRun);
if (holder != null) {
// update position
holder.mPosition = offsetPosition;
fromScrapOrHiddenOrCache = true;
}
}
if (holder == null && mViewCacheExtension != null) {
// We are NOT sending the offsetPosition because LayoutManager does not
// know it.
final View view = mViewCacheExtension.getViewForPositionAndType(this, position, type);
}
if (holder == null) { // fallback to pool
holder = getRecycledViewPool().getRecycledView(type);
}
//3 creating it directly
if (holder == null) {
holder = mAdapter.createViewHolder(RecyclerView.this, type);
}
return holder;
}
この方法から、Google エンジニアが書いたメモは非常に優れていることがわかり、シリアル番号 0123 が直接マークされています。 ステップ 1: レイアウト前の状態であれば、変更されたスクラップから取得されます。つまり、mChangedScrap です。具体的には、空の場合はまず、position を通じて取得し、stableid が設定されている場合は、stableid を通じて取得します。
ViewHolder getChangedScrapViewForPosition(int position) {
// If pre-layout, check the changed scrap for an exact match.
final int changedScrapSize;
if (mChangedScrap == null || (changedScrapSize = mChangedScrap.size()) == 0) {
return null;
}
// find by position
for (int i = 0; i < changedScrapSize; i++) {
final ViewHolder holder = mChangedScrap.get(i);
if (!holder.wasReturnedFromScrap() && holder.getLayoutPosition() == position) {
holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
return holder;
}
}
// find by id
if (mAdapter.hasStableIds()) {
final int offsetPosition = mAdapterHelper.findPositionOffset(position);
if (offsetPosition > 0 && offsetPosition < mAdapter.getItemCount()) {
final long id = mAdapter.getItemId(offsetPosition);
for (int i = 0; i < changedScrapSize; i++) {
final ViewHolder holder = mChangedScrap.get(i);
if (!holder.wasReturnedFromScrap() && holder.getItemId() == id) {
holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
return holder;
}
}
}
}
return null;
}
ステップ 2: スクラップ/非表示リスト/キャッシュから、getScrapOrHiddenOrCachedHolderForPosition メソッドを呼び出してビューを取得します。このメソッドは、アタッチされたスクラップ、非表示の子、およびキャッシュから位置を介して順番にビューを取得します。アタッチ スクラップは mAttachedScrap キャッシュから取得され、キャッシュは mCachedViews キャッシュから取得されることがわかっています。コードを詳しく調べると、ChildHelper クラスに変数 Final List mHiddenViews が存在します。非表示のビューとは、RecyclerView の境界から外れているビューを指します。これらのビューは、対応する分離アニメーションを正しく実行するために、RecyclerView のサブビューとして保持されます。
ステップ 3: 前のステップでキャッシュが取得されなかった場合、アダプターが安定 ID を設定していれば、安定 ID を介して mAttachedScrap でキャッシュが取得されます。
ステップ 4、mViewCacheExtension を確認します。このオブジェクトはデフォルトでは null であると述べましたが、これは開発者によってカスタマイズされたキャッシュ戦略のレイヤーであるため、定義していない場合は、ここで View を見つけることはできません。
ステップ 5、RecycledViewPool から取得します。
ステップ 6: 上記の 5 つのステップのいずれも取得できない場合は、アダプターの createViewHolder メソッドを使用して直接作成します。
補足知識 StableID
notifyDataSetChanged が呼び出されたときにアダプターが hasStableId を設定しない場合、RecyclerView は何が起こったのか、何が変更されたのかを認識できないため、すべてが変更され、すべての ViewHolder が無効であると想定されるため、それらはスクラップではなく RecyclerViewPool に配置される必要があります。StableId がある場合、ViewHolder はプールではなくスクラップに入ります。次に、位置ではなく特定の ID (アダプターの getItemId によって取得される ID) を使用して、スクラップ内の ViewHolder を検索します。
最適化の提案
- 部分的な通知更新には、notifyDataSetChanged() の代わりに、notifyItem***() 関連メソッドを使用してみてください。
- データ セットの変更には DiffUtil を使用することをお勧めします。データ セットが比較的大きい場合は、AsyncListDiffer を使用してサブスレッドで差分操作を実行することもできます。
- 特定の viewType の項目が 1 つだけある場合は、RecyclerView.getRecycledViewPool() .setMaxRecycledViews(viewType,1); を使用してキャッシュ領域のサイズを調整し、メモリ使用量を削減できます。
- RecyclerView が RecyclerView をネストしている場合は、RecyclerView.setRecycledViewPool(@Nullable RecycledViewPool pool); を使用して、RecyclerView のキャッシュ プールを再利用できます。