RecyclerView notifyDataSetChangedにより、画像がちらつきます



Recyclerview Notifydatasetchanged Causes Picture Flicker



現在、プロジェクトにはいくつかのメインページがあります( 図1のホームページに示されているように。 ViewPager + TabLayoutを使用して、データの遅延読み込みが実現されました )データ(ネットワーク)の最初のページがキャッシュされ、DBに保存されます。次にアクセスするときは、最初にDBデータを要求し、次にネットワークデータを要求するため、ユーザーエクスペリエンスが向上します。以前は、メインページが使用されていました RadioPullToRefreshListView( カプセル化されたPullToRefreshListView、以下ListViewと呼びます )。 DB内のデータがネットワーク要求データと同じであるかどうかに関係なく、2回更新され、エクスペリエンスに問題がないようにする方法(refreshメソッドはnotifyDataSetChangedを使用します)。 4.0バージョンがRecyclerViewに実装されているため、画像が読み込まれていることがわかります場所が点滅します

ListViewの論理本体のみがRecyclerViewに置き換えられるため、RecyclerViewで目がすぐにロックされます。オンラインで検索した後、多くの同様の問題があります。 ほとんどの場合、setStableIds(true)を設定せず、getItemId()をオーバーライドすることが原因です。



ひょうたん塗装スクープによると、対応するアダプターをセットしました setStableIds(true)およびオーバーライドgetItemId()、 フラッシュオーバーの問題がまだ存在していることがわかりました。その時点で解決されなかった理由については( 実際には解決できますが、理由は後で説明します )、この方法を試すのをあきらめます。

RecylerViewがListViewに基づいたさまざまな更新メソッドを追加したことは誰もが知っています。 notifyDataSetChanged()が機能しないため、使用するように変更しました。 notifyItemRangeChanged(int positionStart、int itemCount)、 変更方法により、上記のちらつきの問題が解決されることがわかった。



1.2つの更新方法の類似点と相違点を比較します

(1)notifyDataSetChanged()が最終的にonChangedに作用します


疑わしいのは次のとおりです。 RecyclerView.this.setDataSetChangedAfterLayout()、 ViewHolderを更新済みまたは役に立たないものとしてマークし、一時的に記録します。後で説明します。



(2)RecyclerViewでの更新(など notifyItemRangeChanged() )。

最後に、notifyItemRangeChangedおよびその他のメソッドが呼び出されます triggerUpdateProcessor()、 メインページは基本的に複数のViewTypeの場合であるため、通常、HasFixedSizeはtrueに設定されず、最後にelseブランチのみが呼び出されます。

(1)と(2)から、2つの更新の違いは、メソッド(1)がViewHolderのロゴを変更し、共通点がrequestLayout()を呼び出すことです。これにより、RecyclerViewが再測定されて配置されます。アウト。理由。言及されていない別の重要な実験があります。ネットワークの更新のみが実行された場合、画像のちらつきは基本的にありません(ページジャンプによりちらつきが明確になりません)。さまざまな兆候は、ビューがデータ更新プロセス中に再利用されない可能性があること、時間のかかる再膨張、またはその他の理由により、ちらつきを引き起こす可能性があることを示しています。

次に、手がかりを見つけるために、RecyclerViewの測定とレイアウトからのみ分析できます。

次に、RecyclerViewの測定、レイアウト、およびキャッシュ戦略を簡単に分析します

(1)RecyclerView :: onMeasure()

RecyclerViewのonMeasureは、次のように比較的単純です。現在提供されているLayoutManagerのmAutoMeasureがtrueであり、プロジェクト内のRecyclerViewの幅と高さが正確であるため、skipMeasureがtrueであり、直接戻ります。 RecyclerView :: onMeasure()からは、2つの更新の違いを確認できません。 onLayoutを見てみましょう。

(2)RecyclerView :: onLayout()

ここでは詳しく説明しません。 1000語をスキップします。 RecyclerViewが柔軟に配置できる理由は、onLayout()をLayoutManagerに委任するためです。ここでは、例としてLinearLayoutManagerを取り上げます。

LinearLayoutManager :: onLayoutChildren() (呼び出しチェーンRecyclerView :: dispatchLayout()-> dispatchLayoutStep2()-> onLayoutChildren() )onLayoutChildren()では、さらに2つの重要な関数が説明されています。detachAndScrapAttachedViews(recycler)はViewHolder多重化(ビュー多重化)を処理するために使用され、fill()はレイアウトを実行します。

1)detachAndScrapAttachedViews()

DetachAndScrapAttachedViews()は、すべてのビューをループし、ViewHolderおよびAdapter.hasStableIds()のステータスに応じてさまざまな操作を実行します。

ブランチ1:

ViewHolder役に立たない&&いいえRemove && hasStableIdsはfalseであり、対応するビューを削除すると同時に、条件に従ってmCachedViewsとaddViewHolderToRecycledViewPoolに保存することを選択します

ブランチ2:

最初にdetachViewを実行し、条件に従ってmChangedScrapとmAttachedScrapに保存します

上記の条件を見て、おなじみですか? notifyDataSetChangedの場合、ViewHolderは最初に無効に設定されます。この中断ポイントで、個別のnotifyDataSetChanged()シチュエーションが実際にifブランチに入ることがわかります( notifyItemRangeChanged()はelseブランチに入ります )、hasStableIds(true)の設定はすべきではありませんか?まだ入っていたのに気づいた、ナニ? ? ? ?

よく見てください。対応するアダプタが間違っています。カプセル化されたRecyclerViewコンポーネント、内部のWrapAdapter(より多くの読み込みの実装を容易にするなど)であり、コンポーネントのWrapAdapterの変更に対応し、hasStableIdsを設定します。 (true)およびgetItemId()をオーバーライドし、notifyDataSetChangedを引き続き使用します。点滅の問題は実際に解消されました。

による detachAndScrapAttachedViews() 2つの更新方法の違いは、ViewHolerのキャッシュ方法が異なることです。まず、レイアウトを再利用する方法を見てみましょう。 fill()関数は実際にはレイアウトです。

上記から、Viewがnext()によって取得されていることがわかります。

next()-> recycler.getViewForPosition()-> tryGetViewHolderForPositionByDeadline()

上記から、ViewHolderの作成または再利用のプロセスを確認できます。内部のプロセスを理解するために、次にRecyclerViewの4レベルキャッシュがどのように実装されているかを簡単に紹介します。

(3)RecyclerViewのレベル4キャッシュ

1)画面内キャッシュ

MChangedScrap:データが変更されたことを示すViewHolder

mAttachedScrap:RecyclerViewから分離されていないViewHolderを表します

2)オフスクリーンキャッシュ

MCachedViews:画面が画面からスワイプされると、ViewHolderはmCachedViewsにキャッシュされます(デフォルトは2)。

3)ViewCacheExtensionクラスを実装して、カスタムキャッシュを実装します

4)キャッシュプール

tryGetViewHolderForPositionByDeadLine()での操作は、次のフローチャートのように詳細に説明できます。

tryGetViewHolderForPositionByDeadLine()の呼び出しフローとRecyclerViewの4レベルのキャッシュ戦略を組み合わせることで、2つの更新メソッドを簡単に分析でき、さまざまな現象が発生します。

第三に、RecyclerView notifyDataSetChangedは、画像の実際のちらつきを引き起こします

上記のコンテンツと組み合わせると、notifyDataSetChangedの真の殺人者を分析して画像をちらつくことが簡単にできます。

(1)notifyDataSetChangedのみを使用します。再レイアウトすると、最初にビューが削除され、次に関連するシーンに従って、mCachedViewsにキャッシュ、ViewCacheExtension(プロジェクトは実装されていません)、tryGetViewHolderForPositionByDeadLine()時にキャッシュプールが復元されます。上記のキャッシュはい、図10によると、プールで使用できない場合は、ViewHolderが直接作成されて再膨張し、ちらつきが発生します。

(2)notifyDataSetChanged()+ hasStableIds()true +上書きgetItemId()を使用するか notifyItemRangeChanged()はにキャッシュされます mAttachedScrap、 したがって、createViewHolderは再作成されません。

その理由は、(1)が言ったように、CreateViewHolderを再作成してちらつきを引き起こすためです。簡単な実験は、次のとおりです。図のCustomxxxxは、itemViewの最上位のビューです。

1)notifyDataSetChanged()更新

要求されたネットワークデータを検出した後、基本的にCreateViewHolderは見つかりませんでした(ログに出力がない場合があります。ここでは主にItemViewの測定とレイアウトを示しています)が、再測定してレイアウトするitemViewを見つけました。

2)notifyDataSetChanged()+ hasStableIds()true + getItemId()をオーバーライドします

要求されたネットワークデータを見つけた後、itemViewの再測定とレイアウトが見つかりませんでした

上記の実験は、画像がちらつく理由はこれではなく、createViewHolderが原因ではないことを示しています。これは、2番目のネットワーク要求が戻ってきたときにプールにキャッシュがあるためです。

点滅の本当の理由は何ですか?現在、まだ疑問があります。 ViewHolderの再利用のみを分析する前ですが、ViewHolder(またはView)を取得した後は、addView操作の2つの方法にも違いがあります。

次の図に示すように、layoutChunk()では、ビューを取得した後、addViewInt()メソッドが最終的に呼び出されます。

1)ホルダーがスクラップからのものである場合は、最後にRecyclerView.this.attachViewToParent(child、index、layoutParams)を呼び出して、前のdetachViewに対応するビューを表示します。

2)child.getParentは現在のRecyclerViewであり、ビューのみを移動します

3)1)と2)はそうではなく、直接RecyclerView.this.addView(child、index)であるため、itemViewが再測定されてレイアウトされます。

これまでのところ、notifyDataSetChangedにより、画像のちらつきはcreateViewHolderではなくitemView再測定レイアウトが原因で発生していました。

第四に、画像のちらつきを解決するいくつかの方法

いくつかの解決策:

1)notifyDataSetChanged()+ SetHasStableIds(true)+ getItemIdレプリケーション

2)notifyItemRangeChanged()を使用します

notifyItemRangeChanged()は通常、より多くのシーンをロードするために使用されます。 notifyDataSetChangedの代わりにnotifyItemRangeChanged()を直接使用すると、人々に幻想を与えます。よく理解されていないnotifyDataSetChanged()を使用してみませんか。

3)diffUtilツールクラスを使用して更新を実行します

詳細については、他の記事を参照してください。 RecyclerView + DiffUtilは事前調査を使用します

5. getItemId()に関する質問

notifyDataSetChanged + setHasStableIds(true)+上書きgetItemId()メソッドを個別に導入するのはなぜですか。この方法では、ちらつきを解決するのに問題はありませんが、大きな落とし穴があります。

つまり、setHasStableIds(true)を設定するときは、一意の識別子があることを確認する必要があります(次の図を参照)。同じネットワークでの一般的な方法は、アダプタでgetItemId()メソッドを直接再利用し、位置を直接返すことです(ただし、データセットの位置はChangeだけではなく、大きな疑問ですか?)。ただし、繰り返しテストしても問題は見つかりませんでした。これは、さまざまな使用シナリオが原因である可能性があり、検出が容易ではありません。

疑わしいことに、Googleの前身の傑作と公式のサンプルコード(単純すぎて関与していない)は、Androidの記事を見つけました ItemTouchHelper使用時のRecyclerView例外 (ありがとうございます)、この記事では、ItemTouchHelperの使用時にフラッシュデータの例外が発生したと述べ、その理由は、setHasStableIds(true)+レプリケーションgetItemId()を使用して位置を返した結果であると指摘しました。具体的な理由は説明されていません。

ItemTouchHelperは、プロジェクトのUGC録音と効果音の曲の選択に使用されました。簡単な実験で、記事に示されているように例外が発生したことがわかりました。操作プロセスは次のとおりです。スライドプロセスが点滅します。スライドが停止した後、データが異常であることが判明しました(2か所のデータが同時に)。

上記の現象の理由:

(1)移動中は呼び出されます notifyItemMoved(fromPosition、toPosition)、 RecyclerViewに再レイアウトさせます

(2)図9のtryGetViewHolderForPositionByDeadLine()メソッドで、hasStableIdsがtrueの場合、多重化されたviewHolderのIDはgetScrapOrCachedViewForIdを介して取得されます(以下を参照)。スライドする前:getItemIdは fromPosition、 したがって、holder.getItemId()は次のようになります。 fromPosition、 スライド式RecyclerView.this.mAdapter.getItemId(offsetPosition)のoffsetPositionが変更されたため、idはfromPositionと等しくなく、最終的にgetScrapOrCachedViewForId()はnullになります。

このステップの取得に失敗すると、バッファプールプールから取得され、再びaddViewが実行され、測定とレイアウトが行われるため、上記のドラッグプロセスが点滅します。

getItemId()が位置を返すことが原因であるため、一意の値を返すだけでは不十分です。データのhashCodeを最初の位置に返しても大丈夫ですか(DB +ネットワークの場合、データの内容は同じですが、hashCodeが異なります)。戻り値を表すより良い方法がある場合は、お知らせ下さい!

この結論から :NotifyDataSetChanged + setHasStableIds(true)+上書きgetItemId()メソッドは万能薬ではなく、そのソリューションはシーンを区別する必要があります。