一、缓存机制对比
1.1 ListView(两级缓存)
ListView | ||||
---|---|---|---|---|
是否需要回调createView | 是否需要回调bindView | 生命周期 | 备注 | |
mActiveViews | 否 | 否 | onLayout函数周期中 | 用于屏幕内ItemView快速重用 |
mScrapViews | 否 | 是 | 与mAdapter一致,当adapter被更换时,mScrapViews会被清空 |
1.2 RecyclerView(四级缓存)
RecyclerView | ||||
---|---|---|---|---|
是否需要回调createView | 是否需要回调bindView | 生命周期 | 备注 | |
mAttachedScrap | 否 | 否 | onLayout函数周期内 | 用于屏幕内ItemView快速重用 |
mCacheViews | 否 | 否 | 与Adapter一致,当mAdapter被更换时,mCacheViews即被缓存至mRecyclerPool | 默认上限为2,即缓存屏幕外2个ItemView |
mViewCacheExtension | 不直接使用,需要用户在定制,默认不实现 | |||
mRecyclerPool | 否 | 是 | 与自身生命周期一致,不再被引用时即被释放 | 默认上限为5,技术上可以实现所有RecyclerViewPool共用同一个 |
1.3 ListView和RecyclerView获取缓存流程
ListView获取缓存原理
RecyclerView获取缓存原理
二、RecyclerView局部刷新
ListView离屏缓存实现机制:ListView从mScrapViews根据pos获取相应的缓存,但是没有直接用,而是重新getView,bindView。RecyclerView中通过pos获取的是viewholder,即pos->(view, viewHolder, flag),标志位flag的作用是判断View是否需要重新bindView。
三、ListView开发注意事项
3.1 ListView加载图片数据混乱
ListView是根据RecycleBin来实现ItemView的复用的,出现图片数据错乱加载的原因是每次从mScrapViews中获取ItemView,都会重新调用getView()方法,但是图片是异步加载的,但是ItemView是复用的。所以可以通过tag来区分当前ImageView和要显示的图片是否一致,如果一致则不去网络加载。
// 给 ImageView 设置一个 tag
holder.img.setTag(imgUrl);
// 预设一个图片
holder.img.setImageResource(R.drawable.ic_launcher);
// 通过 tag 来防止图片错位
if (imageView.getTag() != null && imageView.getTag().equals(imageUrl)) {
imageView.setImageBitmap(result);
}
3.2 ListView的优化
- 使用ViewHolder复用机制
由于ListView的实现机制,每次从缓存里面获取ItemView都需要getView(),重新inflate布局比较耗时,可以给ListView添加ViewHolder并设置tag,代码如下:
ViewHolder viewHolder = null;
View view = null;//getView方法要返回的View
if(convertView == null){//如果当前没有可以复用的View
viewHolder = new ViewHolder();
view = LayoutInflater.from(context).inflate(resourceId,null);//那么就从XML文件生成一个View
viewHolder.resourceViewName = view.findViewById(resouceViewId);//从XML中找到对应的View
view.setTag(viewHolder);//将ViewHolder设置在当前ItemView的tag里面
}else{//否则
view = convertView;//就使用可以复用的View
viewHolder = (ViewHolder)convertView.getTag();//从复用的View中取出viewHoder
}
class ViewHolder {
TextView name;
}
-
对图片的优化
给图片设置内存缓存(LruCache,SoftReference),当使用图片时可用直接加载好的图片,这样体验会更流畅;另外当用户快速滑动ListView时候,可以默认为不加载。
listView.setOnScrollListener(new AbsListView.OnScrollListener() {
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
if (scrollState == OnScrollListener.SCROLL_STATE_IDLE) {//list停止滚动时加载图片
loadImage(startPos, endPos);// 异步加载图片 ,只加载可以看到的图片
}
}
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
//设置当前屏幕显示的起始pos和结束pos
startPos = firstVisibleItem;
endPos = firstVisibleItem + visibleItemCount;
if (endPos >= totalItemCount) {
endPos = totalItemCount - 1;
}
}
});
- onClickListener的处理
当contentView中有很多View需要对点击事件的处理,可以集中在viewHolder中处理:
class ViewHolder implements OnClickListener{
int position;
TextView name;
public void setPosition(int position){
this.position = position;
}
@Override
public void onClick(View v) {
switch (v.getId()){
//XXXX
}
}
}
ViewHolder viewHolder = null;
View view = null;//getView方法要返回的View
if(convertView == null){//如果当前没有可以复用的View
viewHolder = new ViewHolder();
view = LayoutInflater.from(context).inflate(resourceId,null);//那么就从XML文件生成一个View
viewHolder.resourceViewName = view.findViewById(resouceViewId);//从XML中找到对应的View
viewHolder.setPosition(position);//设置位置
viewHolder.name.setOnClickListener(viewHolder);//设置ClickListener
view.setTag(viewHolder);//将ViewHolder设置在当前ItemView的tag里面
}else{//否则
view = convertView;//就使用可以复用的View
viewHolder = (ViewHolder)convertView.getTag();//从复用的View中取出viewHoder
}
四、RecyclerView开发注意事项
4.1 RecyclerView的性能优化
-
数据方面的处理
类似于这种对数据进行html栅格化的操作 ( Html.fromHtml(data) ), 有必要放在子线程和网络加载中一起处理,可以让用户觉得数据加载的时间会稍微长点;分页加载的数据要进行缓存起来,加快加载速度。
-
布局优化
- 减少过度绘制,可考虑自定义View来减少层级,或更合理的设置布局来减少层级,不推荐在RecyclerView中使用ConstranintLayout。
- 减少xml文件inflate时间,如果当item的复用几率很低的情况下,随着type增多,可以尝试用代码去生成布局,这样可以加快布局的加载速度。
- 减少View对象的创建,尽量的简化ItemView,设计ItemType时,多对ViewType能够共用的地方设计成自定义View。
-
其他
- 25.1.0版本及以上使用Prefetch功能,RecyclerView视图数据预取。
在RecyclerView显示一帧的过程包括UI线程创建视图和绑定视图的操作,然后交给RenderThread发指令通知GPU进行渲染的操作,但是在显示一帧完成和下帧开始创建视图的过程中,UI线程是无所事事的,可以在这个时候对后面的视图进行创建和绑定操作。
如果Item高度是固定的话,可以使用RecyclerView.setHasFixedSize(true);来避免requestLayout浪费资源。
设置RecyclerView.addOnScrollListener(listener),来对滑动过程中停止加载的操作。
如果不要求动画,可以调用 getItemAnimator().setSupportsChangeAnimations(false),把默认动画关闭。
对TextView使用String.toUpperCase来替代 android:textAllCaps="true"。
通过重写RecyclerView.onViewRecycled(holder)来回收资源,当这个方法被回调的时候,就表示表示这个Holder已经被扔进mRecyclerPool.mScrap里了,也就是再次取出的时候会经过onBindViewHolder方法重新绑定数据。
通过设置RecyclerView.setItemViewCacheSize(size);来加大RecyclerView的缓存,用空间来换取时间上的流畅性。
如果多个RecyclerView的Adapter是一样的,比如嵌套的RecyclerView中存在一样的Adapter,可以设置RecyclerView.setRecycledViewPool(pool);来共用一个RecycledViewPool。
对ItemView设置监听器,不要对每个Item都调用addXXListener,应该共用一个Listener,根据ID来进行不同的操作,优化了对象的频繁创建带来的资源消耗。
通过getExtraLayoutSpace来增加RecyclerView预留的额外空间。
//显示范围之外提供更大的缓存空间。 new LinearLayoutManager(this) { @Override protected int getExtraLayoutSpace(RecyclerView.State state) { return size; } };
4.2 stableId 的使用
setHasStableIds用来标识每一个itemView是否需要一个唯一标识,当stableId设置为true的时候,每一个itemView数据就有一个唯一标识。getItemId()返回代表这个ViewHolder的唯一标识,如果没有设置stableId唯一性,返回NO_ID=-1。通过setHasStableIds可以使itemView的焦点固定,从而解决RecyclerView的notify方法使得图片加载时闪烁问题。注意:setHasStableIds()必须在 setAdapter() 方法之前调用,否则会抛异常。因为RecyclerView.setAdapter后就设置了观察者,设置了观察者stateIds就不能变了。
4.3 7.0新工具类DiffUtil
DiffUtil是support-v7:24.2.0中的新工具类,它用来比较两个数据集,寻找出旧数据集—>新数据集的最小变化量,它和mAdapter.notifyDataSetChanged()最大不同在于`它会自动计算新老数据集的差异,并根据差异情况,自动调用以下四个方法:
adapter.notifyItemRangeInserted(position, count);
adapter.notifyItemRangeRemoved(position, count);
adapter.notifyItemMoved(fromPosition, toPosition);
adapter.notifyItemRangeChanged(position, count, payload);
且调用notifyDataSetChanged()
不会触发RecyclerView的动画(删除、新增、位移、change动画),其次性能较低,它不管数据是否一样都整个刷新了一遍整个RecyclerView ,使用如下:
public abstract static class Callback {
public abstract int getOldListSize();//老数据集size
public abstract int getNewListSize();//新数据集size
//新老数据集在同一个position的Item是否是一个对象,如果给itemView设置了stableIds,则仅比较它们单独的 //id(可能内容不同,如果这里返回true,会调用下面的方法)
public abstract boolean areItemsTheSame(int oldItemPosition, int newItemPosition);
//这个方法仅仅是上面方法返回true才会调用,判断item的内容是否有变化,类似于Object.equals(Object)
public abstract boolean areContentsTheSame(int oldItemPosition, int newItemPosition);
//当areItemsTheSame()返回true且areContentsTheSame()返回false,
//同之处用下面的方法找出两个itemView的data不
@Nullable
public Object getChangePayload(int oldItemPosition, int newItemPosition) {
return null;
}
}
//使用的时候实现Callback接口
DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new ProductListDiffCallback(mProducts, newProducts));
diffResult.dispatchUpdatesTo(mProductAdapter);
4.4 NestedScrollView嵌套RecyclerView
-
滑动IRecyclerView列表会出现强烈的卡顿感
mRecyclerView.setNestedScrollingEnabled(false);
RecyclerView默认是setNestedScrollingEnabled(true),是支持嵌套滚动的,也就是说当它嵌套在NestedScrollView中时,默认会随着NestedScrollView滚动而滚动,放弃了自己的滚动。将该值置false可以让RecyclerView不支持嵌套滑动,这样RecyclerView可以自己响应滑动事件。
-
每次打开界面都是定位在RecyclerView在屏幕顶端,列表上面的布局都被顶上去了
RecyclerView抢占了焦点,自动滚动导致的.
RecyclerView会在构造方法中调用setFocusableInTouchMode(true), 抢占焦点后一定会定位到第一行的位置,可以在NestedScrollView中添加属性:android:focusableInTouchMode="true",同时在RecyclerView中添加属性:android:descendantFocusability="blocksDescendants"或直接设置mRecyclerVIew.setFocusableInTouchMode(false)
4.5 图片混乱和闪烁的问题
- 当Item还在加载图片的过程中,被移除屏幕可视范围,不需要继续加载这张图片了,可以在onViewRecycled中取消图片的加载,这样就不会造成图片加载完设置到其他Item的ImageView中了。
- 每一个经过屏幕可视区域的item,加载的图片都要放到缓存中,即使item离开了可视区域,也要加载完毕并放入缓存中,方便下次浏览时能够快速加载,每次onBind时对ImageView设置tag标记,如果tag标记已经被更改了,旧线程加载好的图片不再设置到ImageView中。