Android ListView 重难点解析
阅读原文时间:2021年04月20日阅读:1

一、基本使用


 我们先来看看 ListView 的 Adapter 一般是怎么写的:

public class FruitAdapter extends ArrayAdapter<Fruit> {

    private int resourceId;

    public FruitAdapter(Context context, int textViewResourceId, List<Fruit> objects) {
        super(context, textViewResourceId, objects);
        resourceId = textViewResourceId;
    }

    /*  由系统调用,获取一个View对象,作为ListView的条目,屏幕上能显示多少个条目,getView方法就会被调用多少次
     *  position:代表该条目在整个ListView中所处的位置,从0开始
     */
    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        //重写适配器的getItem()方法
        Fruit fruit = getItem(position);
        View view;
        ViewHolder viewHolder;
        if (convertView == null) { //若没有缓存布局,则加载
            //首先获取布局填充器,然后使用布局填充器填充布局文件
            view = LayoutInflater.from(getContext()).inflate(resourceId, null);
            viewHolder = new ViewHolder();
            //存储子项布局中子控件对象
            viewHolder.fruitImage = (ImageView) view.findViewById(R.id.fruit_image);
            viewHolder.fruitName = (TextView) view.findViewById(R.id.fruit_name);
            // 将内部类对象存储到View对象中
            view.setTag(viewHolder);
        } else { //若有缓存布局,则直接用缓存(利用的是缓存的布局,利用的不是缓存布局中的数据)
            view = convertView;
            viewHolder = (ViewHolder) view.getTag();
        }
        viewHolder.fruitImage.setImageResource(fruit.getImageId());
        viewHolder.fruitName.setText(fruit.getName());
        return view;
    }

    //内部类,用于存储ListView子项布局中的控件对象
    class ViewHolder {
        ImageView fruitImage;
        TextView fruitName;        
    }
}

如上所示,在实现 Adapter 的时候,我们一般会加上 ViewHolder 这个东西。ViewHolder 和复用机制的原理是无关的,他的主要目的是持有 Item 中控件的引用,从而减少 findViewById() 的次数,因为 findViewById() 方法也是会影响效率的,因此在复用的时候他起的作用是这个,减少方法执行次数增加效率。

点击事件代码:

listView.setOnItemClickListener(new OnItemClickListener() {  
    @Override                                                
    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {                         
        Fruit fruit = fruitList.get(position);               
        // 加入逻辑代码 
    }                                                        
});  

ListView 对象使用 setAdapter(ListAdapter la) 方法给 ListView 控件设置适配器。

二、ListView 的缓存机制


 ListView 的缓存主要是在 ListView 的内部类 RecycleBin 中,它包含了两级缓存(Active View、Scrap View):

Active View、Scrap View 分别是什么呢,我们继续看下面这张图:

可以看到,Active View 其实就是在屏幕上可见的视图,也是与用户进行交互的 View,那么这些 View 会通过 RecycleBin 直接存储到 mActiveView 数组当中,以便为了直接复用

那么当我们滑动 ListView 的时候,有些 View 被滑动到屏幕之外 ,那么这些 View 就成为了 Scrap View,也就是废弃的 View,已经无法与用户进行交互了,这样在 UI 视图改变的时候就没有绘制这些无用视图的必要了。它将会被 RecycleBin 存储到 mScrapView 数组当中,目的是为了二次复用,也就是间接复用

当新的 View 需要显示的时候,先判断 mActiveView 中是否存在,如果存在那么我们就可以从 mActiveView 数组当中直接取出复用,也就是直接复用,否则的话从 mScrapView 数组当中进行判断,如果存在则间接复用当前的视图然后调用 getView 方法,如果不存在,那么就需要创建新的 View 了。

它们都是在 RecycleBin 类中的:

class RecycleBin {
    private RecyclerListener mRecyclerListener;
    //第一个可见的View存储的位置
    private int mFirstActivePosition;
    //可见的View数组
    private View[] mActiveViews = new View[0];
    //不可见的的View数组,是一个集合数组,每一种type的item都有一个集合来缓存
    private ArrayList<View>[] mScrapViews;
    //View的Type的数量
    private int mViewTypeCount;
    //viewType为1的集合或者说mScrapViews的第一个元素
    private ArrayList<View> mCurrentScrap;
}

可以看到 mScrapViews 是一个 ArrayList 的数组,不知道大家有没有这样的一个疑惑,负责缓存的 mScrapViews 数组的容量是谁来确定的?当我们需要 ListView 支持多类型复用时,往往要覆盖这两个方法:

  • getViewTypeCount() 就决定了 mScrapViews 数组的长度
  • getItemViewType() 就决定了相同类型的 View 投放到哪个坐标下。这句话的意思就是相同类型的 View 需要返回相同的值,并且它的值必须是从 0 开始依次递增的。

当我们使用同类型加载数据的ListView时,这两个方法我们不必去理会。

ListView 将 Item 显示出来的核心部分也就是 makeAndAddView() 方法,这个部分涉及到了 ListView 的复用:

private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
        boolean selected) {
    View child;
    //判断数据源是否发生了变化.
    if (!mDataChanged) {
        // Try to use an exsiting view for this position
        //如果mActivityView[]数组中存在可以直接复用的View,那么直接获取,然后重新布局.
        child = mRecycler.getActiveView(position);
        if (child != null) {
            // Found it -- we're using an existing child
            // This just needs to be positioned
            setupChild(child, position, y, flow, childrenLeft, selected, true);
            return child;
        }
    }
    // Make a new view for this position, or convert an unused view if possible
    /**
      *如果mActivityView[]数组中没有可用的View,那么尝试从mScrapView数组中读取.然后重新布局.
      *如果可以从mScrapView数组中可以获取到,那么直接返回调用mAdapter.getView(position,scrapView,this);
      *如果获取不到那么执行mAdapter.getView(position,null,this)方法.
      */
    child = obtainView(position, mIsScrap);
    // This needs to be positioned and measured
    setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);
    return child;
}

这里可以看到如果数据源没有变化的时候,会从 mActiveView 数组中判断是否存在可以直接复用的 View,可能很多读者都不太明白直接复用到底是怎么个过程,举个例子,比如说我们 ListView 一页可以显示 10 条数据,那么我们在这个时候滑动一个 Item 的距离,也就是说把 position = 0 的 Item 移除屏幕,将 position = 10 的 Item 移入屏幕,那么 position = 1 的 Item 是不是就直接能够从 mActiveView 数组中拿到呢?这是可以的,我们在第一次加载 Item 数据的时候,已经将 position = 0~9 的 Item 加入到了 mActiveView 数组当中,那么在第二次加载的时候,由于 position = 1 的 Item 还是 ActiveView,那么这里就可以直接从数组中获取。这里也就表示的是 Item 的直接复用。

接下来跟进去看看 obtainView 方法:

View obtainView(int position, boolean[] isScrap) {  
    isScrap[0] = false;  
    View scrapView; 
    // 根据position调用getItemViewType(position)方法可以获得View在缓存池的位置
    scrapView = mRecycler.getScrapView(position);  
    View child;  
    if (scrapView != null) {  
        // 如果不为null,我们就可以利用convertView进行复用操作
        child = mAdapter.getView(position, scrapView, this);  
        if (child != scrapView) {
            // 如果返回的View和我们从缓存池中拿出的View不同,则把它重新存进去
            mRecycler.addScrapView(scrapView);  
            if (mCacheColorHint != 0) {  
                child.setDrawingCacheBackgroundColor(mCacheColorHint);  
            }  
        } else {  
            isScrap[0] = true;  
            dispatchFinishTemporaryDetach(child);  
        }  
    } else {  
        // 当缓存池中没有时,传递convertView为null
        child = mAdapter.getView(position, null, this);  
        if (mCacheColorHint != 0) {  
            child.setDrawingCacheBackgroundColor(mCacheColorHint);  
        }  
    }  
    return child;  
} 

这也是 ListView 中最核心的方法,用来获取 scrap view 缓存,我将难点都用注释标注了应该很好理解。

三、总结


与 RecyclerView 缓存 RecyclerView.ViewHolder 不同,ListView 缓存的是 View。

 

是否需要回调createView

是否需要回调bindView

生命周期

备注

mActiveViews

onLayout函数周期内

用于屏幕内ItemView快速重用。容量为一屏能展示的ItemView的个数

mScrapViews

与mAdapter一致,当mAdapter被更换时它被清空

容量不限

Android ListView工作原理完全解析,带你从源码的角度彻底理解

ListView缓存原理剖析