Skip to content

控件使用

二者控件的使用差异不大,值得一提的是子项之间分割线的使用 ListView默认自带分割线,可以通过divider属性和dividerHeight属性修改分割线的颜色和高度 RecyclerView默认不带分割线,可以通过_DividerItemDecoration_类指定分割线的方向,以及通过继承_DividerItemDecoration_自定义分割线的样式。 Android 为 RecyclerView 设置分割线 | Liarr’s Studio

xml
<ListView
    android:id="@+id/listview"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />
xml
<androidx.recyclerview.widget.RecyclerView
    android:id="@+id/recycler_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent"/>

image.pngimage.png

布局差异

ListView只支持线性纵向布局 RecyclerView可以实现线性布局,网格布局和瀑布流布局,同时每种布局都可以指定滚动是横向还是纵向。 如此比较RecyclerView实现的滚动列表样式更加灵活多样。

java
// recyclerView.setLayoutManager(new LinearLayoutManager(this,LinearLayoutManager.VERTICAL,false));//线性纵向滚动
// recyclerView.setLayoutManager(new LinearLayoutManager(this,LinearLayoutManager.HORIZONTAL,false));//线性横向滚动
// recyclerView.setLayoutManager(new GridLayoutManager(this,2,GridLayoutManager.HORIZONTAL,false));//网格横向滚动,并指定每列两行
// recyclerView.setLayoutManager(new GridLayoutManager(this,2,GridLayoutManager.VERTICAL,false));//网格纵向滚动,并指定每行两列
recyclerView.setLayoutManager(new StaggeredGridLayoutManager(2,LinearLayoutManager.VERTICAL));//瀑布纵向滚动,并指定每行两列
recyclerView.addItemDecoration(new DividerItemDecoration(this,DividerItemDecoration.VERTICAL));//指定子项分割线

image.pngimage.pngimage.png1.gif2.gif2.gif

刷新策略

ListView:ListView中通常使用notifyDataSetChanged() 刷新数据 ,这种刷新是全局刷新的,每一个item的数据都会重新加载一次,这样很消耗资源,在一些需要频繁更新数据的场景,比如淘宝实时更新的界面,ListView实现会很鸡肋。 RecyclerView:可以通过 notifyItemChanged() 来实现局部刷新

ps:不过如果要在ListView实现局部刷新,依然是可以实现的,当一个item数据刷新时,我们可以在Adapter中,实现一个onItemChanged()方法,在方法里面获取到这个item的position(可以通过getFirstVisiblePosition()),然后调用getView()方法来刷新这个item的数据。

item view的复用实现

ListView:默认每次加载一个新的item创建一个新的item view,引起内存增加,不过可以通过判断 convertView 是否为空来重用item view。

java
public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
    if(convertView==null){
        //加载item view布局
        convertView= LayoutInflater.from(getContext()).inflate(resourceId,null);
        //获取控件绑定数据
        ···
        convertView.setTag(tagId);
    }else{
        //复用缓存的item view(缓存在convertView中)直接获取控件绑定数据
        ···
    }
    return convertView;
}

RecyclerView**:默认实现重用item view, RecyclerView复用item全部搞定,不需要像ListView那样setTag()与getTag();**

ViewHolder的使用

ListView:ViewHolder需要自定义,如果用getview去获取控件,则每次调用getview都要通过 findViewById 去获取控件, 如果控件个数过多,会严重影响性能 ,因为findViewById相对比较耗时,所以我们需要创建自定义ViewHolder,通过getTag()和setTag(viewHolder)直接获取view。

java
public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
    ViewHolder holder;
    Fruit fruit=getItem(position);
    if(convertView==null){
        holder=new ViewHolder();
        convertView= LayoutInflater.from(getContext()).inflate(resourceId,null);
        holder.textName=convertView.findViewById(R.id.fruit_name);
        convertView.setTag(holder);
    }else{
        holder= (ViewHolder) convertView.getTag();
    }
    holder.textName.setText(fruit.getName());
    return convertView;
}
static class ViewHolder{
    public TextView textName;
}

RecyclerView:继承RecyclerView.ViewHolder,默认需要重写ViewHodler,使用已经封装好的

java
static class MyViewHolder extends RecyclerView.ViewHolder{
    private TextView textName;
    public MyViewHolder(@NonNull View itemView) {
        super(itemView);
        textName=itemView.findViewById(R.id.fruit_name);
    }
}

缓存机制

ListView和RecyclerView的缓存机制其实有诸多类似之处,滚动列表只作为展示数据的容器,列表中由一条条的item view组成,而缓存机制就是在用户滑动滚动列表的过程中,顶部的item view离开屏幕被回收至缓存中,底部进入屏幕的item view优先从缓存中获取,而不是重新加载新的item view。

ListView的两级缓存

ListView继承自AbsListView,在AbsListView中有一个内部类RecycleBin。其中有两个数组(集合)

  • _private View[] mActiveViews = new View[0];_
  • _private ArrayList<View>[] mScrapViews;_
  • _private ArrayList<View> mCurrentScrap;_
java
class RecycleBin {
    @UnsupportedAppUsage
    private RecyclerListener mRecyclerListener;
    /**
     * 存储在mActiveView中的第一个View的位置(position)。
     */
    private int mFirstActivePosition;
    /**
     *布局开始时屏幕上的视图。该数组在布局开始时填充,
	 *在布局结束时,mActiveViews中的所有视图都移动到mScrapViews。
	 *mActiveViews中的视图表示一个连续的视图范围,
	 *第一个视图存储在mFirstActivePosition中。
     */
    private View[] mActiveViews = new View[0];
    /**
     * Unsorted views that can be used by the adapter as a convert view.
     */
    private ArrayList<View>[] mScrapViews;
    
    private int mViewTypeCount;
    private ArrayList<View> mCurrentScrap;
    private ArrayList<View> mSkippedScrap;
    private SparseArray<View> mTransientStateViews;
    private LongSparseArray<View> mTransientStateViewsById;
    ···
}

mActiveViews:一级缓存,活跃的view,存储的是layout开始的时候屏幕上那些view,layout结束后,所有ActiveViews中的view被移动到ScrapViews中。ActiveViews代表了一个连续范围的views,其第一个view的位置存储在FirstActivePosition变量中。 mScrapViews:二级缓存,废弃的view,它是无序的被adapter的convertView使用的view的集。 ScrapViews是多个list组成的数组,数组的长度为viewTypeCount(item view视图的类型数量),每个item是个list,所以每个list缓存不同类型item布局的view,所以ScrapViews应该是下图的样子。

mCurrentScrap:跟mScrapViews的区别是,mScrapViews是个队列数组,ArrayListView[]类型,数组长度为mViewTypeCount,而默认ViewTypeCount = 1的情况下mCurrentScrap=mScrapViews[0]。所以mCurrentScrap是个简化的mScrapViews。 ListView缓存机制源码分析

INFO

The RecycleBin facilitates reuse of views across layouts. The RecycleBin has two levels of storage: ActiveViews and ScrapViews. ActiveViews are those views which were onscreen at the start of a layout. By construction, they are displaying current information. At the end of layout, all views in ActiveViews are demoted to ScrapViews. ScrapViews are old views that could potentially be used by the adapter to avoid allocating views unnecessarily. 英文大意:RecycleBin有助于跨布局重用视图。RecycleBin有两个存储级别:ActiveView和ScrapView。ActiveView是在布局开始时显示在屏幕上的视图。通过构造,它们显示当前信息。在布局结束时,Active View中的所有视图都会降级为ScrapView。ScrapView是旧的视图,适配器可能会使用这些视图来避免不必要地分配视图。

image.png

ListView的两级缓存
是否需要回调createView是否需要回调bindView生命周期备注
mActiveViewsonLayout函数周期内用于屏幕内itemView快速重用
mScrapViews与mAdapter一致,当mAdapter被更换时,mScrapViews即被清空缓存离开屏幕(屏幕外)的ItemView,目的是让即将进入屏幕的ItemView重用.

RecyclerView的四级缓存

RecyclerView 本身是一个 ViewGroup ,那么滑动时避免不了添加或移除子View(子View通过**RecyclerView#Adapter中的onCreateViewHolder创建),如果每次使用子View都要去重新创建,肯定会影响滑动的流 畅性,所以 RecyclerView 通过 Recycler 来缓存的是 ViewHolder (内部包含子View),这样在滑动时可以复用子View,某些条件下还可以复用子View绑定的数据。所以本质上缓存是为了减少重复绘制View绑定数据**的时间,从而提高了滑动时的性能。 mAttachedScrap 和 mChangedScrap:一级缓存。用于屏幕内item view快速重用,不需要重新createView和bindView

mAttachedScrap 和 mChangedScrap的区别:

  1. **mAttachedScrap:**表示未与RecyclerView分离的ViewHolder列表
  2. **mChangedScrap:**表示数据已经改变的viewHolder列表

mCachedViews:二级缓存。当列表滑动出了屏幕时,ViewHolder会被缓存在 CachedViews ,其大小由ViewCacheMax决定,默认**_DEFAULT_CACHE_SIZE_****_ _**为2,可通过**_Recyclerview.setItemViewCacheSize()_**动态设置。保存最近移出屏幕的ViewHolder,包含数据和position信息,复用时必须是相同位置的ViewHolder才能复用,应用场景在那些需要来回滑动的列表中,当往回滑动时,能直接复用ViewHolder数据,不需要重新bindView。 **ViewCacheExtension:三级缓存。**不直接使用,需要用户自定义实现,默认不实现。 RecycledViewPool:四级缓存。当cacheView满了后或者adapter被更换,将cacheView中移出的ViewHolder放到Pool中,放之前会把ViewHolder数据清除掉,所以复用时需要重新bindView。

java
 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;
 }

image.png

RecyclerView的四级缓存
是否需要回调createView是否需要回调bindView生命周期备注
mAttachedScrap mChangedScraponLayout函数周期内用于屏幕内itemView快速重用
mCachedViews与mAdapter一致,当mAdapter被更换时,mCachedViews即被缓存至RecycledViewPool默认上限为2个,即最多缓存屏幕外两个item view
ViewCacheExtension自定义缓存,提供给用户定制缓存机制
RecycledViewPool与自身生命周期一致,不再被引用时即被释放RecyclerPool缓存池会按照itemview的itemType进行保存,每个itemType缓存个数为5个,超过就会被回收。

总结

  1. mActiveViewsmAttachedScrap功能相似,意义在于快速重用屏幕上可见的列表项ItemView,而不需要重新createView和bindView;
  2. mScrapView和mCachedViews + mReyclerViewPool功能相似,意义在于缓存离开屏幕的ItemView,目的是让即将进入屏幕的ItemView重用.
  3. RecyclerView的优势在于mCacheViews的使用,可以做到屏幕外的列表项ItemView进入屏幕内时也无须bindView快速重用;
  4. 二者缓存对象不同:
    1. RecyclerView缓存RecyclerView.ViewHolder,抽象可理解为:View + ViewHolder(避免每次createView时调用findViewById) + flag(标识状态);RecyclerView中mCacheViews(屏幕外)获取缓存时,是通过匹配pos获取目标位置的缓存,这样做的好处是,当数据源数据不变的情况下,无须重新bindView:
    2. ListView缓存View。而同样是离屏缓存,ListView从mScrapViews根据pos获取相应的缓存,但是并没有直接使用,而是重新getView(即必定会重新bindView)。

关于更深入的RecyclerView的缓存流程与面试题详看一下两篇文章: 再也不用担心面试问RecyclerView了RecyclerView和ListView的区别ListView 和 RecyclerView 的对比分析 - 腾讯云开发者社区-腾讯云RecyclerView面试必问