博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Android开发——ListView的复用机制源码解析
阅读量:4044 次
发布时间:2019-05-24

本文共 6490 字,大约阅读时间需要 21 分钟。

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方法@Overrideprotected 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的复用机制不是一码事。

转载地址:http://vuzci.baihongyu.com/

你可能感兴趣的文章
CCF 分蛋糕
查看>>
解决python2.7中UnicodeEncodeError
查看>>
小谈python 输出
查看>>
Django objects.all()、objects.get()与objects.filter()之间的区别介绍
查看>>
python:如何将excel文件转化成CSV格式
查看>>
机器学习实战之决策树(一)
查看>>
机器学习实战之决策树二
查看>>
[LeetCode By Python]7 Reverse Integer
查看>>
[leetCode By Python] 14. Longest Common Prefix
查看>>
[LeetCode By Python]121. Best Time to Buy and Sell Stock
查看>>
[LeetCode By Python]122. Best Time to Buy and Sell Stock II
查看>>
[LeetCode By Python]125. Valid Palindrome
查看>>
[LeetCode By Python]136. Single Number
查看>>
Android/Linux 内存监视
查看>>
用find命令查找最近修改过的文件
查看>>
Android2.1消息应用(Messaging)源码学习笔记
查看>>
android raw读取超过1M文件的方法
查看>>
ubuntu下SVN服务器安装配置
查看>>
MPMoviePlayerViewController和MPMoviePlayerController的使用
查看>>
CocoaPods实践之制作篇
查看>>