Android开发——ListView的复用机制源码解析

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/SEU_Calvin/article/details/78332790

0. 前言  

前段时间找工作,看了很多人的面经,不得不说找个工作很麻烦。尤其是Android,岗位的数量比不上前端后Java后台也就算了,问的东西又多又杂,这里就不多列举了,其中有一个印象比较深的问题是关于ListView复用机制的。复用机制谁都会用,但是却不一定能真正讲清楚。因此才有了此文。

 

1.   ListView的继承关系和Adapter的由来

ListView直接继承自的AbsListViewAbsListView还有另一个子实现类,就是GridView),然后AbsListView又继承自AdapterViewAdapterView继承自ViewGroup继而继承ViewObject

ListView为了避免臃肿,本职工作就是和用户交互和展示数据,而不负责对数据源的适配工作,因为数据源类型烦杂,一旦在ListView中写死就没办法拓展,于是就有了Adapter的出现。Adapter的作用就是作为中间人去访问真正的数据源。比如说继承Adapter接口的子类ArrayAdapter,用于数组和List类型的数据源适配,还有子类SimpleCursorAdapter用于游标类型的数据源适配,等等。当然我们用的最多的还是自己重写其中的getView()方法。

 

2.  RecycleBin机制

RecycleBin机制是在理解ListView工作原理之前不得不提的。RecycleBin类是在AbsListView中的一个内部类。

RecycleBin类是实现复用的关键类,这个类内部维护了一个存放ActiveViews的数组mActiveViews ActiveView是在屏幕上可见的视图,也是与用户进行交互的View,这些View被第一次加载后会通过RecycleBin直接存储到mActivityView数组当中以便直接复用

当我们滑动ListView的时候,被滑动到屏幕之外的View就成为了ScrapView,即废弃的View将会被RecycleBin存储到mScrapView数组当中,以便间接复用

 

3.  ListView复用机制

下面是对这个过程的源码分析过程:

 

3.1  ListView第一屏数据的显示过程

ListView的绘制过程中,onMeasure()过程与普通View区别不大,onDraw()ListView当中也没有什么意义,因为绘制工作由ListView当中的子元素来完成。那么就着重看看ListViewonLayout()方法了。

//父类AbsListView中的onLayout方法
@Override
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;
}

可以看到onLayout()方法中,如果ListView的大小或者位置发生了变化,那么会要求所有的子布局都强制进行重绘。后面则调用layoutChildren()方法,这个方法父类中空实现,由ListView完成。layoutChildren()方法代码太长了就不进行粘贴了,我们只需要知道这是ListView中的子View进行布局的一个方法就可以了。

值得一提的是,在layoutChildren()方法中,间接调用了一个makeAndAddView()方法

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

这里在第5尝试从RecycleBin当中快速获取一个ActiveView,不过目前RecycleBin当中还没有缓存任何View,因此会返回null,接着到第14行会调用obtainView()方法来再次尝试获取一个View,接着将获取到的View传入到了setupChild()方法当中。setupChild()方法的核心功能就是obtainView()方法获取到的子元素添加到了ListView当中,直到将ListView所能显示的第一屏数据填满。而不去管在屏幕以外的控件的布局,这样保证了ListView中的内容能够迅速展示到屏幕上。

 

显然需要看一下obtainView()中的实现,这个方法很重要:

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

在第4行代码中调用了RecycleBingetScrapView()方法来尝试获取一个废弃缓存中的View,目前也没有缓存废弃的View因此返回null。代码会执行到else语句块,调用了mAdaptergetView()方法来去获取一个View。也就是我们重写Adapter中的getView()方法了。第二个参数传入null则说明convertViewnull,那么就需要在getView()中去调用inflate()方法加载一个布局了,这就比较好理解了。

此时ListView已经加载好第一屏的数据了。


3.2  ListView向下滑动

向下滑动的过程中会调用onTouchEvent()方法,并且在其中调用了trackMotionScroll()方法,该方法在手指在屏幕上稍微移动就会被触发,接收两个参数,第一个deltaY表示从手指按下时的位置到当前手指位置的距离,另一个incrementalDeltaY则表示Y方向上位置的改变量,那么就可以通过incrementalDeltaY的正负值情况来判断用户是向上还是向下滑动的了。不管怎么滑动,将偏移量传入offsetChildrenTopAndBottom()方法,这个方法的作用是让ListView中所有的子View都按照传入的参数值进行相应的偏移,实现内容随着手指拖动的效果

ListView向下滑动的时候,会从上往下依次遍历子View,如果该Viewbottom值已经小于ListViewtop了,如果是ListView向上滑动的话,就是从下往上依次遍历子View,然后判断该子Viewtop值是不是大于ListViewbottom值了。此时说明这个子View已经移出屏幕了,此时会做两件事情:

1调用RecycleBinaddScrapView()方法将这个View加入到废弃缓存中;

2并把所有移出屏幕的子View全部detach

 这时通过判断最后一个View的底部已经移入了屏幕,或者第一个View的顶部移入了屏幕,就会调用fillGap()方法去加载屏幕外的数据。这时会调用makeAndAddView()方法来实现数据的填充。之前makeAndAddView()方法已经分析过了,这里首先仍然是会尝试调用RecycleBingetActiveView()方法来获取子布局,这里会返回null。

//返回null原因解释:
//具体原因是因为ListView会至少调用两次Layout过程,在第二次Layout过程中
//在ListView中已经有子View情况下,子View都会被缓存到RecycleBin的mActiveViews数组中
//而在第二次填充ListView数据时,为了防止数据的重复填
//会先detach掉了所有的view,再将mActiveViews数组中的缓存拿来使用
//而又因为RecycleBin自身的机制,mActiveViews是不能够重复利用的,因此这里返回的值肯定是null

既然getActiveView()方法返回的值是null,那么就还是会走到obtainView()方法当中:

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

这个方法前面分析过了,不过这时候,getScrapView()是有数据的,获取到了一scrapView作为参数传入到了AdaptergetView()方法当中,即我们熟悉的convertView。接下来就不用多说了。因此ListView折腾来折腾去就那么几个子View,因此不会出现OOM的情况。


3.3  复用机制总结

1)填充第一屏数据的时候,第一次onLayout()尝试获取一个ActiveView,无缓存返回null,再去调用obtain()方法ScrapView也返回null,继而到getView中去inflate view

2)第二次onLayout()时会获取ListView元素不为0,此时会将ListView中的子View放入ActiveView数组中,detach所有View又从数组里取出缓存(缓存取出后会被删除),此时第一屏数据显示完毕。

3)接下来向下滑,onTouchEvent()中获取Y轴偏移量后一方面使子View都跟着滑动,另一方面会判断滑动方向并且detach掉移除屏幕的View,同时将其放入ScrapView数组。发现最后一个子Viewbottom要进入屏幕时,尝试获取一个ActiveView,显然返回null,从而继续走obtain()方法,幸运的是ScrapView返回了view,继而将其传入到getView中的convertView中。


4  ViewHolder

在实现Adapter的时候,我们一般会加上ViewHolder这个东西,ViewHolder和复用机制和原理是无关的,其主要作用是持有Item中控件的引用,从而减少findViewById()的次数,因为findViewById()方法也是会影响效率的。因此ViewHolder起到了提高效率的作用。但是显然和ListView的复用机制不是一码事。


猜你喜欢

转载自blog.csdn.net/SEU_Calvin/article/details/78332790