blog

Android-RecyclerViewの原理

ListView は最も単純なリスト実装の 1 つで、アダプタを介してデータをビューに変換できます。以下のコードはListViewの典型的な使用例です。 ListViewでリストを実装するために最も重...

Aug 3, 2020 · 11 min. read
シェア

ListViewによるリストの実装

ListView は最も単純なリストの実装のひとつで、 アダプタを通してビューに変換することができます。次のコードは ListView の典型的な使用例です。

data class DemoItem(val text:String, val target: Class<*>)
override fun onCreate(savedInstanceState: Bundle?) {
 super.onCreate(savedInstanceState)
 val list = ListView(this)
 setContentView(list)
 list.adapter = DemoListAdapter(this, getDemo())
  
 
 class DemoListAdapter(val context: Context, val data: List<DemoItem>): BaseAdapter() {
 override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
 val text = TextView(context)
 text.height = 150
 text.gravity = Gravity.CENTER
 text.textSize = 25.0F
 text.setPadding(, 30)
 text.text = data[position].text
 text.setOnClickListener {
 context.startActivity(Intent(context, data[position].target))
 }
 return text
 }
 override fun getItem(position: Int): Any {
 return data[position]
 }
 override fun getItemId(position: Int): Long {
 return position.toLong()
 }
 override fun getCount(): Int {
 return data.size
 }
 }

ListViewのキャッシュ機構

ListViewでリストを実装するには、BaseAdapterのサブクラスを実装し、getItem、getItemId、getCount、getView抽象メソッドを実装することが最も重要です。上記のコードでは、getView が呼び出されるたびに新しいビューが作成され、パフォーマンスに大きな影響を与えます。

override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
 if (convertView == null || convertView !is TextView) {
 val text = TextView(context)
 text.height = 150
 text.gravity = Gravity.CENTER
 text.textSize = 25.0F
 text.setPadding(, 30)
 text.text = data[position].text
 text.setOnClickListener {
 context.startActivity(Intent(context, data[position].target))
 }
 return text
 } else {
 convertView.text = data[position].text
 convertView.setOnClickListener {
 context.startActivity(Intent(context, data[position].target))
 }
 return convertView
 }
 }

ここでは、ListViewの利用メカニズムをフルに利用しています。特定の位置にスライドするとき、ListViewは現在の位置のItemViewをgetViewのパラメータとして渡します。そして、入力されたItemViewがnullでなければ、このItemViewを再利用できると判断できます。新しいViewを作り直すことなく、データを更新するだけで、このItemViewを再利用することができます。

  実際、ListViewもキャッシュメカニズムを持っています。ListViewの親クラスAbsListViewはRecycleBin内部クラスを持っています。このRecycleBinには2つの変数があり、それぞれmCurrentScrapのListとmScrapViewsのList[]です。mCurrentScrapはキャッシュ・プールで、mScrapViewsは全てのキャッシュ・プールのコレクションです。mScrapViewsはListの配列です。これは、各ViewTypeのキャッシュ・プールを作成する必要があるためで、 この配列の長さはgetViewTypeCountの返り値です。

View getScrapView(int position) {
 ArrayList<View> scrapViews;
 if (mViewTypeCount == 1) {
 scrapViews = mCurrentScrap;
 int size = scrapViews.size();
 if (size > 0) {
 return scrapViews.remove(size - 1);
 } else {
 return null;
 }
 } else {
 int whichScrap = mAdapter.getItemViewType(position);
 if (whichScrap >= 0 && whichScrap < mScrapViews.length) {
 scrapViews = mScrapViews[whichScrap];
 int size = scrapViews.size();
 if (size > 0) {
 return scrapViews.remove(size - 1);
 }
 }
 }
 return null;
 }

コードのロジックは非常にシンプルで、viewTypeが1つしかない場合は、キャッシュプールのリストの末尾から直接取り出します。viewTypeが複数ある場合は、まず現在のポジションのviewTypeが何かを判断し、このviewTypeのキャッシュプールを取得し、このキャッシュプールの末尾から取得します。

void addScrapView(View scrap) {
 AbsListView.LayoutParams lp = (AbsListView.LayoutParams) scrap.getLayoutParams();
 if (lp == null) {
 return;
 }
 // Don't put header or footer views or views that should be ignored
 // into the scrap heap
 int viewType = lp.viewType;
 if (!shouldRecycleViewType(viewType)) {
 return;
 }
 if (mViewTypeCount == 1) {
 mCurrentScrap.add(scrap);
 } else {
 mScrapViews[viewType].add(scrap);
 }
 if (mRecyclerListener != null) {
 mRecyclerListener.onMovedToScrapHeap(scrap);
 }
 }

キャッシュに追加することも比較的簡単ですが、基本的にはキャッシュの逆の操作です。これは、キャッシュの第二レベルと呼ばれ、ListViewは、キャッシュの第一レベルと呼ばれるメカニズムがあり、実際には、原理は比較的単純ですが、つまり、スライドで、ビュー内の現在の画面のキャッシュのみ、現在のアダプタのデータが変更されていない場合は、キャッシュの第一レベルから位置によると、フェッチの使用に直接撮影することができます取得するgetViewへの呼び出しで取得することはできません。

  上記の分析から、我々はListViewは、キャッシュの2つのレベルを持っていることがわかります、最初のキャッシュの最初のレベルから取得されます、あなたがそれを取得する場合、それは直接使用されます。キャッシュの2番目のレベルからそれを取得し、キャッシュの2番目のレベルに起因するViewのデータが変更されている可能性がありますので、キャッシュの2番目のレベルは、それに対処するためにgetViewメソッドでViewから取り出され、メカニズムが実際にあるかどうかの判断の再利用の使用の最も上は、セカンダリキャッシュ機構に一致させることです。

RecyclerView

ListViewにはすでに2層のキャッシュメカニズムがあります。なぜListViewを置き換えるためにRecyclerViewを開発する必要があるのでしょうか?

  まず第一に、ListViewはデータ更新中に2つのレイアウトを実行します。特に、2つのperformTraversalsを実行するため、2回以上呼び出されることがあります。これにより、getViewが複数回呼び出され、パフォーマンスに大きな影響を与えます。なぜ複数回呼び出されるのでしょうか?これは、ListViewが配置されているViewTreeのパスに、wrap_contentに高さを設定するレイヤーがあるため、計測時にはまだItemViewがロードされていないため、ListViewの本当の高さをまだ知らないのですが、getViewはlayoutによってトリガーされるため、ItemViewをトリガーするために一度layoutを使用する必要があるためです。でItemViewの読み込みをトリガーし、ItemViewが読み込まれたら、再度テストを行い、最終的にListViewの高さを確認します。

  さらに、ListViewはconvertViewが空であるかどうかを判断する必要がありますし、実際にキャッシュの2番目のレベルを使用するために、キャッシュ機構の2番目のレベルでViewHolderのアプローチと組み合わせることで、実際には、このプロセスは、テンプレートコードの非常に多くの書かれている、あなたはItemVieの作成プロセスとデータのレンダリングプロセスは、実際にはキャッシュの2番目のレベルの使用から分離され、唯一のデータのレンダリングプロセスを使用する必要がありますすることができます。これはまた、RecyclerViewの実装では、ビューを作成するonCreateViewHolderで、データをビューにバインドするonBindViewHolderで、キャッシュを使用してのみデータを更新するonBindViewHolderを呼び出すことができます。

  ListViewはアニメーション効果を実現しませんが、ItemViewのアニメーション効果を実現するのはより複雑で、RecyclerViewはすでにItemViewのアニメーション効果のapiを提供しています。ListViewはItemViewごとに設定する必要があります。

&emsp ListViewはグローバルリフレッシュのみをサポートしており、フィードストリームで毎回グローバルリフレッシュを使用する場合、フィードストリームはアイテムデータが数百から数千になるため、パフォーマンスへの影響は非常に大きくなります。RecyclerViewは、ローカルの追加、削除、および変更操作を提供します。

  ListViewには2レベルのキャッシュがありますが、RecyclerViewには4レベルのキャッシュがあり、ユーザーはキャッシュをよりカスタマイズすることができます。

RecyclerView簡単な使い方

最初のステップは、RecyclerViewをレイアウトに追加することです。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://..///id"
 android:orientation="horizontal" android:layout_width="match_parent"
 android:layout_height="50dp">
 <TextView
 android:id="@+id/title"
 android:layout_width="0dp"
 android:layout_height="50dp"
 android:layout_weight="1"
 android:textSize="20sp"
 />
 <TextView
 android:id="@+id/summary"
 android:layout_width="0dp"
 android:layout_height="50dp"
 android:layout_weight="3"
 android:paddingLeft="5dp"
 android:textSize="14sp"
 />
</LinearLayout>

各アイテムのデータ・クラスとアダプタの定義

companion object {
 data class Item(val id: Int, val title: String, val summary: String)
 class DemoAdapter: RecyclerView.Adapter<DemoViewHolder>() {
 val list = LinkedList<Item>()
 fun add(newData: List<Item>) {
 val start = list.size
 list.addAll(newData)
 notifyItemRangeInserted(start, newData.size)
 }
 fun remove(start: Int, itemCount: Int) {
 val count = if (list.size > start + itemCount) {itemCount} else {list.size - start}
 if (list.size > start) {
 val iterator = list.iterator()
 var index = 0
 while(iterator.hasNext()) {
 if (index++ == start) {
 iterator.next()
 iterator.remove()
 }
 if (index == start + count) {
 break
 }
 }
 }
 notifyItemRangeRemoved(start, count)
 }
 override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DemoViewHolder {
 return DemoViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.item_demo, parent, false))
 }
 override fun getItemCount(): Int {
 return list.size
 }
 override fun onBindViewHolder(holder: DemoViewHolder, position: Int) {
 holder.title.text = list[position].title
 holder.summary.text = list[position].summary
 }
 }
 class DemoViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) {
 val title = itemView.findViewById<TextView>(R.id.title)
 val summary = itemView.findViewById<TextView>(R.id.summary)
 }
 }

アダプタの前に RecyclerView のレイアウト・マネージャを設定します。

override fun onCreate(savedInstanceState: Bundle?) {
 super.onCreate(savedInstanceState)
 setContentView(R.layout.activity_demo_recyclerview)
 recyclerview.setHasFixedSize(true)
 recyclerview.layoutManager = LinearLayoutManager(this)
 val demoAdapter = DemoAdapter()
 recyclerview.adapter = demoAdapter
 add.setOnClickListener {
 val newData = MutableList<Item>(10) { index ->
 Item(index + 1 + (demoAdapter.list.lastOrNull()?.id?:0),"title-${index + 1 + (demoAdapter.list.lastOrNull()?.id?:0)}", "summary-${index + 1 + (demoAdapter.list.lastOrNull()?.id?:0)}")
 }
 demoAdapter.add(newData)
 }
 remove.setOnClickListener {
 demoAdapter.remove(0, 5)
 }
 }

RecyclerViewソースコード解析

RecyclerViewの更新はAdapterによってトリガーされるので、RecyclerView.Adapterメソッドのnotifyシリーズを見てみましょう。ソースコードを表示すると、notify一連のメソッドが直接対応するmObservableメソッドを呼び出していることがわかり、mObservableは、オブジェクトのAdapterDataObservable型は、各アダプタは、オブジェクトの新しいアウトにmObservableオブジェクトの作成では、アダプタはまた、レジスタとレジスタを提供します。Adapter も register メソッドと unregister メソッドを提供しています。 AdapterDataObservable は observable オブジェクトで、特定の時間にリスナーのコールバックをトリガーし、すべてのリスニングオブジェクトが一度実行されるようです。AdapterDataObservable 通知一連のメソッドのソースコードを見て、ほとんど常にすべてのリスナーオブジェクトのループ登録のために、リスナーのデータ変更を提供することと同等である対応する on メソッドを呼び出します。

  メソッドのnotifyシリーズを呼び出すと、RecyclerViewが自動的に更新することができますが、RecyclerViewもデータ変更リスナーを登録する必要があります。AdapterのregisterAdapterDataObserverメソッドの使用を検索するには、使用中のRecyclerView#setAdapterInternalを見つけることができます。

/**
 * Replaces the current adapter with the new one and triggers listeners.
 * @param adapter The new adapter
 * @param compatibleWithPrevious If true, the new adapter is using the same View Holders and
 * item types with the current adapter (helps us avoid cache
 * invalidation).
 * @param removeAndRecycleViews If true, we'll remove and recycle all existing views. If
 * compatibleWithPrevious is false, this parameter is ignored.
 */
 private void setAdapterInternal(@Nullable Adapter adapter, boolean compatibleWithPrevious,
 boolean removeAndRecycleViews) {
 if (mAdapter != null) {
 mAdapter.unregisterAdapterDataObserver(mObserver);
 mAdapter.onDetachedFromRecyclerView(this);
 }
 if (!compatibleWithPrevious || removeAndRecycleViews) {
 removeAndRecycleViews();
 }
 mAdapterHelper.reset();
 final Adapter oldAdapter = mAdapter;
 mAdapter = adapter;
 if (adapter != null) {
 adapter.registerAdapterDataObserver(mObserver);
 adapter.onAttachedToRecyclerView(this);
 }
 if (mLayout != null) {
 mLayout.onAdapterChanged(oldAdapter, mAdapter);
 }
 mRecycler.onAdapterChanged(oldAdapter, mAdapter, compatibleWithPrevious);
 mState.mStructureChanged = true;
 }
void dispatchLayout() {
 if (mAdapter == null) {
 Log.e(TAG, "No adapter attached; skipping layout");
 // leave the state in START
 return;
 }
 if (mLayout == null) {
 Log.e(TAG, "No layout manager attached; skipping layout");
 // leave the state in START
 return;
 }
 mState.mIsMeasuring = false;
 if (mState.mLayoutStep == State.STEP_START) {
 dispatchLayoutStep1();
 mLayout.setExactMeasureSpecsFrom(this);
 dispatchLayoutStep2();
 } else if (mAdapterHelper.hasUpdates() || mLayout.getWidth() != getWidth()
 || mLayout.getHeight() != getHeight()) {
 // First 2 steps are done in onMeasure but looks like we have to run again due to
 // changed size.
 mLayout.setExactMeasureSpecsFrom(this);
 dispatchLayoutStep2();
 } else {
 // always make sure we sync them (to ensure mode is exact)
 mLayout.setExactMeasureSpecsFrom(this);
 }
 dispatchLayoutStep3();
 }

RecyclerViewのスライドリフレッシュ

オンラインでは、RecycleViewスライドの原理を分析する記事は無数にありますが、RecycleViewスライドのトリガー更新処理を見つける方法を分析する記事は非常に少なく、実際には、すべてのトリガー処理を同じ方法で使用することができます。Androidは、UIの処理では、スレッドを切り替えるには、Handlerを使用するために何度もある可能性があるため、なぜそれを見つけることは困難である、時にはUIスレッドでもHandlerを使用し、より絹のような滑らかな効果を達成するために、Runnableを投稿することがありますUIの更新に一度だけ、しかし、更新するには、この時間はより時間がかかるかもしれない場合、それは他の場所でUIの更新のCPU / GPUを占有します。/GPU。

  ブレークポイントのコールスタックを見ると、onBindViewHolder の呼び出しがどこで終わっているかがわかります。一般的には、スレッドまたはスレッドプールの実行が見つかりますが、実際には GapWorker の run メソッドが Adapter#onBindViewHolder を呼び出しています。postFromTraversalで、recyclerView.post(this)を呼び出し、GapWorkerのRunnableをMainスレッドにポストして実行させます。コールポイントスタックの一番下を探し続け、RecyclerViewのonTouchEvent MotionEvent.ACTION.MOVEブランチが0でない限り、両方向の最後の判定スライドでmGapWorker.postFromTraversalを呼び出します。RecyclerViewのスライドリフレッシュの原則。つまり、RecyclerViewのonTouchEventイベントリスナーは、スライドをリッスンするとき、GapWorkerを使用して、ViewHolderを表示するために次に必要なものをロードします。

次のViewHolderをロードする方法として、GapWorkerのprefetchメソッドと呼ばれるrunメソッドがあり、prefetchメソッドには2つの呼び出しがあります。collectPrefetchPositionsFromViewは、名前を参照してください、実際には、RecyclerViewの現在の状態に応じて、次のViewHolderをロードするように、現在の位置の位置を取得します。の情報をGapWorker$LayoutPrefetchRegistryImplのint配列mPrefetchArrayに格納します。このデータは実際には 2 つのデータとして使用され、それぞれの位置と距離が格納されます。buildTaskList は次の ViewHolder の位置を見つけるために使用され、2 番目のメソッド flushTaskWithDeadline がプリフェッチメソッド内で呼び出されます。次のコードでオーバーロードされたメソッドを実行します。

private void flushTaskWithDeadline(Task task, long deadlineNs) {
 long taskDeadlineNs = task.immediate ? RecyclerView.FOREVER_NS : deadlineNs;
 RecyclerView.ViewHolder holder = prefetchPositionWithDeadline(task.view,
 task.position, taskDeadlineNs);
 if (holder != null
 && holder.mNestedRecyclerView != null
 && holder.isBound()
 && !holder.isInvalid()) {
 prefetchInnerRecyclerViewWithDeadline(holder.mNestedRecyclerView.get(), deadlineNs);
 }
 }
Read next

Java IDEでWebプロジェクトを構築する方法

META-INF:情報パッケージに相当し、ディレクトリ内のファイルとディレクトリはJava 2プラットフォームによって認識、解釈され、アプリケーション、拡張機能、クラスローダー、サービスファイルを構成するために使用されます。 javaコードで生成されたバイトコードは、WEB-INFのClassesファイルの下にあります。

Aug 3, 2020 · 4 min read