在Android中RecyclerView的使用隨處可見,它的性能優化程度跟用戶體驗息息相關。
性能優化實戰的例子如下,是獲取手機所有已安裝app列表:
RecyclerView的一些優化方案和使用技巧:
-
recyclerView.setHasFixedSize(true)
當Item的高度如是固定的,設置這個屬性為true可以提高性能,尤其是當RecyclerView有條目插入、刪除時性能提升更明顯。RecyclerView在條目數量改變,會重新測量、布局各個item,如果設置了setHasFixedSize(true),由于item的寬高都是固定的,adapter的內容改變時,RecyclerView不會整個布局都重繪。
void onItemsInsertedOrRemoved() {
if (hasFixedSize) layoutChildren();
else requestLayout();
}
-
使用getExtraLayoutSpace為LayoutManager設置更多的預留空間
在RecyclerView的元素比較高,一屏只能顯示一個元素的時候,第一次滑動到第二個元素會卡頓。
RecyclerView (以及其他基于adapter的view,比如ListView、GridView等)使用了緩存機制重用子 view(即系統只將屏幕可見范圍之內的元素保存在內存中,在滾動的時候不斷的重用這些內存中已經存在的view,而不是新建view)。
這個機制會導致一個問題,啟動應用之后,在屏幕可見范圍內,如果只有一張卡片可見,當滾動的時 候,RecyclerView找不到可以重用的view了,它將創建一個新的,因此在滑動到第二個feed的時候就會有一定的延時,但是第二個feed之 后的滾動是流暢的,因為這個時候RecyclerView已經有能重用的view了。
val linearLayoutManager: LinearLayoutManager = object : LinearLayoutManager(applicationContext, LinearLayoutManager.VERTICAL, false) {
override fun getExtraLayoutSpace(state: RecyclerView.State): Int {
return 300
}
}
recyclerView.layoutManager = linearLayoutManager
-
避免創建過多監聽器
onCreateViewHolder 和 onBindViewHolder 對時間都比較敏感,盡量避免繁瑣的操作和循環創建對象。例如創建 OnClickListener,可以全局創建一個。同時onBindViewHolder調用次數會多于onCreateViewHolder的次數,如從RecyclerViewPool緩存池中取到的View都需要重新bindView,所以我們可以把監聽放到CreateView中進行。
優化前:
注意,反復滑動列表,會一直調用onBindViewHolder方法,所以這里會一直創建OnClickListener對象。
override fun onBindViewHolder(holder: AppViewHolder, position: Int) {
holder.ivIcon?.background = context.packageManager.getActivityIcon(datas[position].intent)
holder.tvName?.text = datas[position].name
holder.tvPkg?.text = "包名:" + datas[position].pkg
holder.itemView.setOnClickListener(object: OnClickListener {
override fun onClick(v: View?) {
context.startActivity(datas[position].intent)
}
})
}
優化后:
class AppViewHolder(itemView: View): ViewHolder(itemView) {
var ivIcon: ImageView?= null
var tvName: TextView?= null
var tvPkg: TextView?= null
init {
ivIcon = itemView.findViewById(R.id.iv_icon)
tvName = itemView.findViewById(R.id.tv_name)
tvPkg = itemView.findViewById(R.id.tv_pkg)
itemView.setOnClickListener(onClickListener)
}
var onClickListener: OnClickListener = object: OnClickListener {
override fun onClick(v: View?) {
}
}
}
數據處理與視圖綁定分離
RecyclerView的 bindViewHolder方法是在UI線程進行的,如果在該方法進行耗時操作,將會影響滑動的流暢性。
比如:
mTextView.setText(Html.fromHtml(data).toString());
這里的 Html.fromHtml(data) 方法可能就是比較耗時的,存在多個
TextView 的話耗時會更為嚴重,這樣便會引發掉幀、卡頓,而如果把這
一步與網絡異步線程放在一起,站在用戶角度,最多就是網絡刷新時間稍
長一點。
局部刷新
可以用一下一些方法,替代notifyDataSetChanged,達到局部刷新的目的。notifyDataSetChanged會觸發所有item的detached回調再觸發onAttached回調。
notifyItemChanged(int position)
notifyItemInserted(int position)
notifyItemRemoved(int position)
notifyItemMoved(int fromPosition, int toPosition)
notifyItemRangeChanged(int positionStart, int itemCount)
notifyItemRangeInserted(int positionStart, int itemCount)
notifyItemRangeRemoved(int positionStart, int itemCount)
復用RecycledViewPool
在TabLayout+ViewPager+RecyclerView的場景中,當多個RecyclerView有相同的item布局結構時,多個RecyclerView共用一個RecycledViewPool可以避免創建ViewHolder的開銷,避免GC。RecycledViewPool對象可通過RecyclerView對象獲取,也可以自己實現。
如果LayoutManager是LinearLayoutManager或其子類,需要手動開啟這個特性: layout.setRecycleChildrenOnDetach(true)
val recycledViewPool = recyclerView.recycledViewPool
recyclerView1.setRecycledViewPool(recycledViewPool)
recyclerView2.setRecycledViewPool(recycledViewPool)
使用DiffUtil局部刷新
DiffUtil是androidx.recyclerview.widget包下的一個工具類,當你的RecyclerView需要更新數據時,將新舊數據集傳給它,它就能快速告知adapter有哪些數據需要更新。就相當于如果改變了就對某個item刷新,沒改變就沒刷新,可以簡稱為局部刷新。
mAdapter.notifyDataSetChanged()有兩個缺點:
1.不會觸發RecyclerView的動畫(刪除、新增、位移、change動畫)
2.性能較低,畢竟是無腦的刷新了一遍整個RecyclerView , 極端情況下:新老數據集一模一樣,效率是最低的。
它會自動計算新老數據集的差異,并根據差異情況,自動調用以下四個方法
adapter.notifyItemRangeInserted(position, count);
adapter.notifyItemRangeRemoved(position, count);
adapter.notifyItemMoved(fromPosition, toPosition);
adapter.notifyItemRangeChanged(position, count, payload);
簡單使用DiffUtil,我們需要且僅需要額外編寫一個類。
class AticalDiff: DiffUtil.ItemCallback<ArticleItem>() {
override fun areItemsTheSame(oldItem: ArticleItem, newItem: ArticleItem): Boolean {
return oldItem.userId == newItem.userId
}
override fun areContentsTheSame(oldItem: ArticleItem, newItem: ArticleItem): Boolean {
return (oldItem == newItem && oldItem.userId == newItem.userId && oldItem.link == newItem.link &&
oldItem.title == newItem.title)
}
}
使用方式:
adapter繼承androidx.recyclerview.widget.ListAdapter包下的ListAdapter,并在構造方法中傳入自定義的DiffUtil.ItemCallback,代碼如下:
class Adapter(val context: Context): ListAdapter<ArticleItem, Adapter.MyViewHolder>(AticalDiff()) {
}
優化滑動操作
如果RecyclerView加載很多大圖,快速滑動卡頓解決方案:
考慮滾動的時候不做復雜布局及圖片的加載,盡量減少滾動過程中的耗時操作,這樣滾動停止的時候再加載可見區域的布局。
onScrollStateChanged的幾種狀態:
SCROLL_STATE_IDLE 屏幕停止滾動
SCROLL_STATE_DRAGGING 屏幕滾動且用戶使用的觸碰或手指還在屏幕上
SCROLL_STATE_SETTLING 由于用戶的操作,屏幕產生慣性滑動
Gilde同時也為我們提供了兩個方法:
resumeRequests() 開始加載圖片
pauseRequests() 停止加載圖片
public class AutoLoadRecyclerView extends RecyclerView {
private void init() {
addOnScrollListener(new ImageAutoLoadScrollListener());
}
//監聽滾動來對圖片加載進行判斷處理
public class ImageAutoLoadScrollListener extends OnScrollListener {
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
}
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
switch (newState) {
case SCROLL_STATE_IDLE: // The RecyclerView is not currently scrolling.
//當屏幕停止滾動,加載圖片
try {
if (getContext() != null) Glide.with(getContext()).resumeRequests();
} catch (Exception e) {
e.printStackTrace();
}
break;
case SCROLL_STATE_DRAGGING: // The RecyclerView is currently being dragged by outside input such as user touch input.
//當屏幕滾動且用戶使用的觸碰或手指還在屏幕上,停止加載圖片
try {
if (getContext() != null) Glide.with(getContext()).pauseRequests();
} catch (Exception e) {
e.printStackTrace();
}
break;
case SCROLL_STATE_SETTLING: // The RecyclerView is currently animating to a final position while not under outside control.
//由于用戶的操作,屏幕產生慣性滑動,停止加載圖片
try {
if (getContext() != null) Glide.with(getContext()).pauseRequests();
} catch (Exception e) {
e.printStackTrace();
}
break;
}
}
}
}
方案二:在正在滾動和停止滾動時給adapter設置是否滾動的屬性值,在adapter判斷值的狀態去過略加載圖片邏輯。
參考:
https://blog.csdn.net/GracefulGuigui/article/details/103646864
https://blog.csdn.net/yaojie5519/article/details/117174114