ListView 工作原理解析

ListView 的继承结构

文章内容摘取自- Android ListView工作原理完全解析,带你从源码的角度彻底理解

ListView 的继承结构

由上图可知,ListView 直接继承自 AbsListView,而 AbsListView 有两个子实现类(ListViewGridView),因此可以猜出 ListView 和 GridView 在工作原理和实现上有很多共同点。

AbsListView 又继承自 AdapterView,AdapterView 又继承自 ViewGroup。了解了 ListView 的继承结构有助于我们更加清晰地分析代码。

Adapter 的作用

数据源适配

ListView 是为了交互展示数据用的,但是它只承担交互和展示工作,至于这些数据来自哪儿,它并不关心。

如果真的让 ListView 和数据源直接打交道的话,那 ListView 所要做的工作就更加复杂了。因为数据源这个概念太模糊了,我们只知道它包含了很多数据而已,至于这个数据源到底是什么样类型,并没有严格的定义,有可能是数组,也有可能是集合,甚至有可能是数据库表中查询出来的游标。所以,如果 ListView 真的去为每一种数据源都进行适配操作的话,一是扩展性会比较差,内置了几种适配就只有几种适配,不能动态进行添加。二是超出了本身应该负责的工作范围,不再是仅仅承担交互和展示工作,它会变得比较臃肿。

Adapter (适配器)机制就出现了。它在 ListView 和数据源之间起到了一个桥梁的作用,ListView 并不会直接和数据源打交道,而是会借助 Adapter 这个桥梁来访问真正的数据源。Adapter 的接口是统一的,因此 ListView 不用再去担心任何适配方面的问题。而 Adapter 又是一个接口,它可以去实现各种各样的子类,每个子类都能通过自己的逻辑来完成特定的功能,以及与特定数据源的适配操作,比如 ArrayAdapter 用于数组和 List 类型的数据源适配,SimpleCursorAdapter 用于游标类型的数据源适配,这样就非常巧妙地把数据源适配困难的问题解决掉了,并且还拥有相当不错的扩展性。

Adapter 数据源适配原理

getView() 方法

除了上面的数据源适配,还有一个非常非常重要的方法也需要我们在 Adapter 中去重写,就是 getView() 方法,这个方法会在下面进行详细说明。

RecycleBin 机制

RecycleBin 机制,它是 ListView 能够实现成百上千条数据都不会 OOM 最重要的一个原因。

它是写在 AbsListView 中的一个内部类,所以所有继承自 AbsListView 的子类,也就是 ListView 和 GrildView ,都有使用这个机制。

RecycleBin 中几个主要的方法进行简单解读:

// 方法1
 fillActiveViews(int childCount, int firstActivePosition)

第一个参数 childCount 表示要存储的 view 的数量,第二个参数 firstActivePosition 表示 ListView 中第一个可见元素的 position 值。RecycleBin 当中使用 mActiveViews 这个数组来存储 View,调用这个方法后就会根据传入的参数来将 ListView 中指定元素存储到 mActiveViews 数组当中。

// 方法2
View getActiveView(int position)

和上一个方法是对应的,用于从 mActiveViews 数组中获取数据,该方法接收一个 position 参数,表示元素在 ListView 当中的位置,方法内部会自动将 position 值替换成 mActiveViews 数组对应的下标值。需要注意的是,mActiveViews 当中所存储的 View,一旦被获取后就会从 mActiveViews 当中移除,下次获取同样位置的 View 将会返回 null,也就是说 mActiveViews 不能被重复利用

// 方法3
addScrapView(View scrap, int position)

方法用于将一个废弃的 View 进行缓存,该方法接收一个 View 参数,当有某个 View 确定要废弃掉的时候(比如滚动出了屏幕),就应该调用这个方法来对 View 进行缓存, RecycleBin 当中使用 mScripViewsmCurrentScrap 这两个 List 来存储废弃 View。

// 方法4
 View getScrapView(int position)

方法用于从废弃缓存中取出一个 View,这些废弃缓存中的 View 是没有顺序可言的,因此该方法中的算法非常简单,就是直接从 mCurrentScrap 当中获取尾部的一个 scrap view进行返回。

// 方法5
setViewTypeCount(int viewTypeCount)

我们都知道 Adapter 当中可以重写一个 getViewTypeCount() 来表示 ListView 中有几种类型的数据项,而 setViewTypeCount() 方法的作用就是为每种类型的数据项都单独启用一个 RecycleBin 缓存机制

ListView 的 onLayout() 方法

View 的执行流程无非就分为三步,onMeasure() 用于测量 View 的大小,onLayout() 用于确定 View 的布局,onDraw() 用于将 View 绘制到界面上。而在 ListView 当中, onMeasure() 并没有什么特殊的地方,因为它终归是一个 View,占用的空间最多并且通常也就是整个屏幕。onDraw() 在 ListView 当中也没有什么意义,因为 ListView 本身并不负责绘制,而是由 ListView 当中的子元素来进行绘制的。那么 ListView 大部分的神奇功能其实都是在 onLayout() 方法中进行的,而这个方法是在 ListView 的父类 AbsListView 中实现的。

protected void onLayout(boolean changed, int l, int t, int r, int b) {  
    super.onLayout(changed, l, t, r, b);  
    mInLayout = true;  
    if (changed) {  
        int childCount = getChildCount();  
        for (int i = 0; i < childCount; i++) {  
            getChildAt(i).forceLayout();  
        }  
        mRecycler.markChildrenDirty();  
    }  
    layoutChildren();  
    mInLayout = false;  
}  

如果 ListView 的大小或者位置发生了变化,那么 changed 变量就会变成 true,此时会要求所有的子布局都强制进行重绘。layoutChildren() 方法是用来进行子元素布局的,而这个方法不是由父类 AbsListView 完成,而是由具体的实现类 ListView 来负责完成。

LayoutChildren() 方法中几个重要的地方进行说明

fillActiveViews() 将 ListView 的子 View 进行缓存。

mLayoutMode 值决定布局模式,默认情况下都是普通模式 LAYOUT_NORMAL。默认布局的顺序就是从上往下 fillFormTop(),它主要任务就是从 mFirstPosition 开始,自顶至底去填充 ListView,里面调用了 fillDown() 方法,填充 ListView 的操作在该方法里面完成。

private View fillDown(int pos, int nextTop) {  
    View selectedView = null;  
    int end = (getBottom() - getTop()) - mListPadding.bottom;  
    while (nextTop < end && pos < mItemCount) {  
        // is this the selected item?  
        boolean selected = pos == mSelectedPosition;  
        View child = makeAndAddView(pos, nextTop, true, mListPadding.left, selected);  
        nextTop = child.getBottom() + mDividerHeight;  
        if (selected) {  
            selectedView = child;  
        }  
        pos++;  
    }  
    return selectedView;  
}  

这里使用 while 循环来执行重复逻辑,一开始 nextTop 的值是第一个子元素顶部距离整个 ListView 顶部的像素值,pos 则是刚刚传入的 mFirstPosition 的值,而 end 是 ListView 底部减去顶部所得的像素值, mItemCount 则是 Adapter 中的元素数量。 因此一开始的情况下 nextTop 必定是小于 end 值的,并且 pos 也是小于 mItemCount 值的。那么每执行一次 while 循环,pos 的值都会加1,并且 nextTop 也会增加,当 nextTop 大于等于 end 时,也就是子元素已经超出当前屏幕了,或者 pos 大于等于 mItemCount (所有 Adapter 中的元素都被遍历结束)时,就会跳出 while 循环。

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  
        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  
    child = obtainView(position, mIsScrap);  
    // This needs to be positioned and measured  
    setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);  
    return child;  
} 

因为目前 RecycleBin 当中还没有缓存任何的 view,所以这里得到的值肯定是 null。然后调用 obtainView() 方法来再次获取一个 View,这次的 obtainView() 方法是可以保证一定返回一个 View 的。最后将获得的 View 传入到 setupChild() 方法中。

View obtainView(int position, boolean[] isScrap) {  
    isScrap[0] = false;  
    View scrapView;  
    scrapView = mRecycler.getScrapView(position);  
    View child;  
    if (scrapView != null) {  
        child = mAdapter.getView(position, scrapView, this);  
        if (child != scrapView) {  
            mRecycler.addScrapView(scrapView);  
            if (mCacheColorHint != 0) {  
                child.setDrawingCacheBackgroundColor(mCacheColorHint);  
            }  
        } else {  
            isScrap[0] = true;  
            dispatchFinishTemporaryDetach(child);  
        }  
    } else {  
        child = mAdapter.getView(position, null, this);  
        if (mCacheColorHint != 0) {  
            child.setDrawingCacheBackgroundColor(mCacheColorHint);  
        }  
    }  
    return child;  
}

obtainView() 方法中的代码包含了非常非常重要的逻辑,可以说是整个 ListView 中最重要的内容。

在这个方法里面,按照执行流程来看,首先调用 RecycleBin 的 getScrapView 方法来尝试获取废弃缓存中的 View,获取不到则返回 null。接着调用 mAdapter(当前 ListView 关联的适配器)的 getView() 方法来获取一个 View。此时传入的三个参数分别是 position, null 和 this。

而我们平时写 ListView 的 Adapter 时

@Override  
public View getView(int position, View convertView, ViewGroup parent) {  
    Fruit fruit = getItem(position);  
    View view;  
    if (convertView == null) {  
        view = LayoutInflater.from(getContext()).inflate(resourceId, null);  
    } else {  
        view = convertView;  
    }  
    ImageView fruitImage = (ImageView) view.findViewById(R.id.fruit_image);  
    TextView fruitName = (TextView) view.findViewById(R.id.fruit_name);  
    fruitImage.setImageResource(fruit.getImageId());  
    fruitName.setText(fruit.getName());  
    return view;  
} 

convertView 为 null 时,说明没有 convertView 可以利用,此时会调用 LayoutInflater 的 inflate() 方法来加载一个布局,将 View 返回。这个 View 也会作为 obtainView() 的结果进行返回,并最终传入到 setupChild() 方法中。其实也就是说,第一次 layout 过程中,所有的子 View 都是调用 LayoutInflater 的 inflate() 方法加载出来的,这样就会相对比较耗时。

所以在 setupChild() 方法中,刚才调用 obtainView() 方法获取到了子元素 View,然后调用 addViewInLayout() 方法将它添加到 ListView 当中。根据 fillDown() 方法中的 while 循环,会让子元素 View 将整个 ListView 控件填满然后跳出,也就是说即使我们的 Adapter 中有一千条数据,ListView 也只会加载一屏幕的数据,剩下的数据反正目前在屏幕上也看不到,所以不会去做多余的加载工作,这样就可以确保 ListView 中的内容能够迅速展示到屏幕上。

第二次 Layout?

实验发现即使是一个再简单的 View,在展示到界面上之前都会至少两次 onMeasure() 和两次 onLayout() 的过程。这个小细节对于我们影响不大,因为不管是 onMeasure() 或者 onLayout() 几次,反正都是执行相同的逻辑,我们并不需要过多关心。但是在 ListView 中情况就不一样了,因为这意味着 layoutChildren() 过程会执行两次,而这个过程当中涉及到向 ListView 中添加子元素,如果相同的逻辑执行两遍的话,那么 ListView 中就会存在一份重复的数据。

因此 ListView 在 layoutChildren() 过程中做了第二次 Layout 的逻辑处理,非常巧妙地解决了这个问题。第二次 Layout 和第一次 Layout 的基本流程差不多。

因为目前 ListView 中已经有子 View 了(一屏显示的子 View),这样所有的子 View 都会被缓存到 RecycleBin 的 mActiveViews 数组当中。就直接进入 setupChild() 方法,这样也省略了很多时间,因为如果在 obtainView() 方法中又要去 inflate 布局的话,效率就会很低。

如果我们需要向 ViewGroup 中添加一个新的子 View,应该调用 addViewInLayout() 方法,而如果是想要将一个之前 detach 的 View 重新 attach 到 ViewGroup 上,就应该调用 attachViewToParent() 方法。那么由于前面在 layoutChildren() 方法当中调用了 detachAllViewsFromParent() 方法,这样 ListView 中所有的子 View 都是处于 detach 状态。经历了这样一个 detach 又 attach 的过程,ListView 中所有的子 View 又都可以正常显示出来了。

滑动加载更多数据的原理

这里写图片描述

边界值检测的过程。当 ListView 向下滑动的时候,就会进入一个 for 循环当中,从上往下依次获取子 View,如果该子 View 的 bottom 值已经小于 top 值了,就说明这个子 View 已经移出屏幕了,所以会调用 RecycleBin 的。AddScriptView() 方法将这个 View 加入到废弃缓存当中,并将 count 计数器加1,计数器用于记录有多少个子 View 被移出了屏幕。向上滑动的话,其过程基本相同,只不过变成了从下往上依次获取子 View,然后判断该 View 的 top 值是不是大于 bottom 值了,如果大于的话说明 View 已经移出了屏幕,同样把它加入到废弃缓存中,并将计数器加1。

根据计数器的值来进行一个 detach 操作,它的作用就是把所有移出屏幕的子 View 全部 detach 掉,在 ListView 中,所有看不到的 View 就没有必要为它进行保存,因为屏幕外还有成百上千条数据等着显示,一个好的回收策略才能保证 ListView 的高性能和高效率。

一旦有任何子 View 被移出屏幕,就会将它加入到废弃缓存中,而从 obtainView() 方法中的逻辑来看,一旦有新的数据需要显示到屏幕上,就会尝试从废弃缓存中获取 View。所以它们之间就形成了一个生产者和消费者的模式。

ListView 中的子 View 其实来来回回就那么几个,移出屏幕的子 View 会很快被移入屏幕的数据重新利用起来,因而不管我们加载多少数据都不会出现 OOM 的情况。

Adapter 的 getView() 方法

public View getView(int position, View convertView, ViewGroup parent) {  
    Fruit fruit = getItem(position);  
    View view;  
    if (convertView == null) {  
        view = LayoutInflater.from(getContext()).inflate(resourceId, null);  
    } else {  
        view = convertView;  
    }  
    ImageView fruitImage = (ImageView) view.findViewById(R.id.fruit_image);  
    TextView fruitName = (TextView) view.findViewById(R.id.fruit_name);  
    fruitImage.setImageResource(fruit.getImageId());  
    fruitName.setText(fruit.getName());  
    return view;  
}  

方法中第二个参数 convertView,我们在写 getView() 方法时要进行判断 convertView 是不是等于 null,如果等于 null 才调用 inflate() 方法来加载布局,不等于 null 就可以直接利用 convertView,因为 convertView 就是我们之前利用过的 View,只不过被移出屏幕后进入了废弃缓存中。现在又重新拿出来使用而已,我们只需要把 convertView 中的数据更新成当前位置上应该显示的数据就行。

文章只是作为个人记录学习使用,如有不妥之处请指正,谢谢。

文章内容摘取自- Android ListView工作原理完全解析,带你从源码的角度彻底理解

猜你喜欢

转载自blog.csdn.net/modurookie/article/details/80130604