参考文章
Android ListView工作原理完全解析,带你从源码的角度彻底理解
http://blog.csdn.net/guolin_blog/article/details/44996879
/* 观前提示 */
本文以郭霖大神的文章为参考,总结ListView的工作原理,供自己加深理解和记忆,无法保证内容的正确性和完整性。
思考问题:
- 为什么ListView加载成百上千条数据都不会发生OOM?
- 为什么滑动浏览更多数据时,程序占用内存不会跟着增长?
RecyclerBin机制
AbsListView中的一个内部类,继承AbsListView的ListView和GridView同样可以使用这个机制。
几个重要方法:
- fillActiveViews(),接收两个int类型参数,第一个参数表示要存储的View的数量,第二个参数表示ListView中第一个可见元素的position值。调用后根据指定参数将ListView中指定的元素存储到mActiveViews数组中。
- getActiveViews(),接收一个位置(position)参数,取出mActiveViews中对应的View,不可重复利用,下次获取同样位置将返回null。
- addScrapView(),接收一个View参数,用于将废弃的View进行缓存。使用mScrapViews和mCurrentScrap这两个List来存储废弃View。
- getScrapView(),从mCurrentScrap的末尾获取一个view返回。
第一次Layout
ListView大部分神奇功能都是在onLayout()方法中进行的。
但ListView本身没有onLayout()方法,这个方法在它的父类AbsListView中实现。
以下将粗略描述各个方法的调用和执行过程(仅主要部分)。
1. onLayout()
判断ListView的大小和位置是否发生变化,若发生变化,强制所有子布局重绘。
调用layoutChildren()方法进行子元素布局。
2. layoutChildren()
获取子元素个数,此时ListView中还没有任何子View,得到的值为0。
调用RecyclerBin的fillActiveViews()方法进行缓存,然而此时ListView中还没有子View,此方法不起任何作用。
决定布局模式,默认情况下,并且子元素个数为0时会调用fillFromTop()方法。
3.fillFromTop()
从方法名可以看出,它的功能是自顶至底填充ListView,实际在其调用的fillDown()方法中完成。
4.fillDown()
包涵一个While循环,当ListView中需要渲染的View已占满屏幕(某一个View的顶部超出屏幕底部),或在不超出屏幕的情况下已全部渲染完时退出该循环。循环中通过调用makeAndAddView()方法获取需要渲染的View。
5.makeAndAddView()
调用RecyclerBin的getActiveView()方法尝试快速获取一个View,很可惜在layoutChildren()方法执行过程中,由于当时ListView中并没有任何子View,RecyclerBin的fillActiveViews方法并未缓存到任何View,此处返回null。
获取View失败,继续调用obtainView()方法尝试获取一个View,它能保证一定返回一个View。
6.obtainView()
调用RecyclerBin的getScrapView()方法尝试获取一个View,显然获取不到。
调用Adapter的getView获取。泪目,终于见到熟人了。
总算成功获取到了View,这个View将返回到makeAndAddView()方法。
7.setupChild()
obtainView()执行完毕后,在makeAndAddView()中紧接着调用此方法。
此方法将调用addViewInLayout()方法,将View添加进ListView中。至此,ListView中添加了第一个View。
再回头看fillDown()方法中的while循环,即使适配器中有再多的数据,也仅会加载一屏,以保证内容能够快速显示。
第二次Layout
1. onLayout()
同样的,调用layoutChildren()方法进行子元素布局。
2. layoutChildren()
获取子元素个数,第一次Layout过程中已经向ListView中添加了一屏View,这里将返回具体的数值。
再次调用RecyclerBin的fillActiveViews()方法进行缓存,当前ListView中的所有子View都被缓存到RecyclerBin的mActiveViews数组中。
调用detachAllViewsFromParent()方法,将所有ListView中的子View清除,以保证在第二次Layout过程中不会产生重复的数据。而这些被清除的View已经缓存在mActiveViews中,之后再次加载直接使用即可,并不会明显影响效率。
子元素个数不再为0,调用fillSpecific()方法。
3.fillSpecific()
此方法优先将指定位置的子View加载到屏幕上,然后加载该子View往上或往下的其他子View。由于传入的position值就是第一个子View的位置,此时该方法与fillDown()方法基本无异。
它同样需要调用makeAndAddView()方法获取需要渲染的View。
4.makeAndAddView()
仍尝试从RecyclerBin中快速获取一个View,显然这次可以获取到,无需再调用obtainView()方法了,直接进入setupChild()方法。
RecyclerBin的一个功能所在,由于缓存的存在,不必再像第一次Layout时最终通过Adapter的getView()方法获取View(其中还要infalte加载布局),节省了时间,提高ListView的初始化效率。
5.setupChild()
此方法的最后一个参数表示传入的View是否被回收过,此时为true,表示被回收过(加入缓存)。
那么它将不再调用addViewInLayout()方法,而是调用attachViewToParent()方法。该方法表示将一个之前detach的View重新attach到ViewGroup上,由于在layoutChildren()方法中我们调用过detachAllViewsFromParent()方法,此时所有View都处于deatch状态,毫无疑问应该调用此方法。
至此,ListView中所有的子View又可以显示出来了。
滑动加载更多数据
这里直接照搬了郭神博客中的图解,我觉得非常直观,有这张图就够了。
当View移出屏幕时,调用RecyclerBin的addScrapView()方法将其加入废弃缓存,并detach。
当View移入屏幕时,调用ListView的fillGap()方法,根据滑动方向再次调用fillDown()或者fillUp()方法,makeAndAddView()方法也会再次被调用。
makeAndAddView()
再一次看到这个方法,我对它已经熟悉到不能再熟悉了。
首先尝试从RecyclerBin中获取一个View,很可惜,由于mActiveViews不可复用的机制,此处返回null。
那咋办呢,调用obtainView()呗,又尝试从RecyclerBin中获取一个废弃的View,哦,刚加进去一个啊,那我直接拿来用了,结束。
结论:不管有多少条数据要显示,ListView中的子View永远是那几个,移出屏幕的子View很快会被移入屏幕的数据重新利用起来,因而不管我们加载多少数据都不会出现OOM的情况,甚至内存都不会增加。