原文地址:http://blog.csdn.net/guolin_blog/article/details/44996879

Android所有常用的原生控件当中,用法最复杂的应该就是ListView了,它专门用于处理那种内容元素很多,手机屏幕无法展示出所有内容的情况。ListView可以使用列表的形式来展示内容,超出屏幕部分的内容只需要通过手指滑动就可以移动到屏幕内了。

另外ListView还有一个非常神奇的功能,我相信大家应该都体验过,即使在ListView中加载非常非常多的数据,比如达到成百上千条甚至更多,ListView都不会发生OOM或者崩溃,而且随着我们手指滑动来浏览更多数据时,程序所占用的内存竟然都不会跟着增长。那么ListView是怎么实现这么神奇的功能的呢?当初我就抱着学习的心态花了很长时间把ListView的源码通读了一遍,基本了解了它的工作原理,在感叹Google大神能够写出如此精妙代码的同时我也有所敬畏,因为ListView的代码量比较大,复杂度也很高,很难用文字表达清楚,于是我就放弃了把它写成一篇博客的想法。那么现在回想起来这件事我已经肠子都悔青了,因为没过几个月时间我就把当初梳理清晰的源码又忘的一干二净。于是现在我又重新定下心来再次把ListView的源码重读了一遍,那么这次我一定要把它写成一篇博客,分享给大家的同时也当成我自己的笔记吧。

首先我们先来看一下ListView的继承结构,如下图所示:

可以看到,ListView的继承结构还是相当复杂的,它是直接继承自的AbsListView,而AbsListView有两个子实现类,一个是ListView,另一个就是GridView,因此我们从这一点就可以猜出来,ListView和GridView在工作原理和实现上都是有很多共同点的。然后AbsListView又继承自AdapterView,AdapterView继承自ViewGroup,后面就是我们所熟知的了。先把ListView的继承结构了解一下,待会儿有助于我们更加清晰地分析代码。

Adapter的作用

 

Adapter相信大家都不会陌生,我们平时使用ListView的时候一定都会用到它。那么话说回来大家有没有仔细想过,为什么需要Adapter这个东西呢?总感觉正因为有了Adapter,ListView的使用变得要比其它控件复杂得多。那么这里我们就先来学习一下Adapter到底起到了什么样的一个作用。

其实说到底,控件就是为了交互和展示数据用的,只不过ListView更加特殊,它是为了展示很多很多数据用的,但是ListView只承担交互和展示工作而已,至于这些数据来自哪里,ListView是不关心的。因此,我们能设想到的最基本的ListView工作模式就是要有一个ListView控件和一个数据源。

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

那么显然android开发团队是不会允许这种事情发生的,于是就有了Adapter这样一个机制的出现。顾名思义,Adapter是适配器的意思,它在ListView和数据源之间起到了一个桥梁的作用,ListView并不会直接和数据源打交道,而是会借助Adapter这个桥梁来去访问真正的数据源,与之前不同的是,Adapter的接口都是统一的,因此ListView不用再去担心任何适配方面的问题。而Adapter又是一个接口(interface),它可以去实现各种各样的子类,每个子类都能通过自己的逻辑来去完成特定的功能,以及与特定数据源的适配操作,比如说ArrayAdapter可以用于数组和List类型的数据源适配,SimpleCursorAdapter可以用于游标类型的数据源适配,这样就非常巧妙地把数据源适配困难的问题解决掉了,并且还拥有相当不错的扩展性。简单的原理示意图如下所示:

当然Adapter的作用不仅仅只有数据源适配这一点,还有一个非常非常重要的方法也需要我们在Adapter当中去重写,就是getView()方法,这个在下面的文章中还会详细讲到。

RecycleBin机制

 

那么在开始分析ListView的源码之前,还有一个东西是我们提前需要了解的,就是RecycleBin机制,这个机制也是ListView能够实现成百上千条数据都不会OOM最重要的一个原因。其实RecycleBin的代码并不多,只有300行左右,它是写在AbsListView中的一个内部类,所以所有继承自AbsListView的子类,也就是ListView和GridView,都可以使用这个机制。那我们来看一下RecycleBin中的主要代码,如下所示:

  1. /**
  2. * The RecycleBin facilitates reuse of views across layouts. The RecycleBin
  3. * has two levels of storage: ActiveViews and ScrapViews. ActiveViews are
  4. * those views which were onscreen at the start of a layout. By
  5. * construction, they are displaying current information. At the end of
  6. * layout, all views in ActiveViews are demoted to ScrapViews. ScrapViews
  7. * are old views that could potentially be used by the adapter to avoid
  8. * allocating views unnecessarily.
  9. *
  10. * @see android.widget.AbsListView#setRecyclerListener(android.widget.AbsListView.RecyclerListener)
  11. * @see android.widget.AbsListView.RecyclerListener
  12. */
  13. class RecycleBin {
  14. private RecyclerListener mRecyclerListener;
  15.  
  16. /**
  17. * The position of the first view stored in mActiveViews.
  18. */
  19. private int mFirstActivePosition;
  20.  
  21. /**
  22. * Views that were on screen at the start of layout. This array is
  23. * populated at the start of layout, and at the end of layout all view
  24. * in mActiveViews are moved to mScrapViews. Views in mActiveViews
  25. * represent a contiguous range of Views, with position of the first
  26. * view store in mFirstActivePosition.
  27. */
  28. private View[] mActiveViews = new View[0];
  29.  
  30. /**
  31. * Unsorted views that can be used by the adapter as a convert view.
  32. */
  33. private ArrayList<View>[] mScrapViews;
  34.  
  35. private int mViewTypeCount;
  36.  
  37. private ArrayList<View> mCurrentScrap;
  38.  
  39. /**
  40. * Fill ActiveViews with all of the children of the AbsListView.
  41. *
  42. * @param childCount
  43. * The minimum number of views mActiveViews should hold
  44. * @param firstActivePosition
  45. * The position of the first view that will be stored in
  46. * mActiveViews
  47. */
  48. void fillActiveViews(int childCount, int firstActivePosition) {
  49. if (mActiveViews.length < childCount) {
  50. mActiveViews = new View[childCount];
  51. }
  52. mFirstActivePosition = firstActivePosition;
  53. final View[] activeViews = mActiveViews;
  54. for (int i = 0; i < childCount; i++) {
  55. View child = getChildAt(i);
  56. AbsListView.LayoutParams lp = (AbsListView.LayoutParams) child.getLayoutParams();
  57. // Don't put header or footer views into the scrap heap
  58. if (lp != null && lp.viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
  59. // Note: We do place AdapterView.ITEM_VIEW_TYPE_IGNORE in
  60. // active views.
  61. // However, we will NOT place them into scrap views.
  62. activeViews[i] = child;
  63. }
  64. }
  65. }
  66.  
  67. /**
  68. * Get the view corresponding to the specified position. The view will
  69. * be removed from mActiveViews if it is found.
  70. *
  71. * @param position
  72. * The position to look up in mActiveViews
  73. * @return The view if it is found, null otherwise
  74. */
  75. View getActiveView(int position) {
  76. int index = position - mFirstActivePosition;
  77. final View[] activeViews = mActiveViews;
  78. if (index >= 0 && index < activeViews.length) {
  79. final View match = activeViews[index];
  80. activeViews[index] = null;
  81. return match;
  82. }
  83. return null;
  84. }
  85.  
  86. /**
  87. * Put a view into the ScapViews list. These views are unordered.
  88. *
  89. * @param scrap
  90. * The view to add
  91. */
  92. void addScrapView(View scrap) {
  93. AbsListView.LayoutParams lp = (AbsListView.LayoutParams) scrap.getLayoutParams();
  94. if (lp == null) {
  95. return;
  96. }
  97. // Don't put header or footer views or views that should be ignored
  98. // into the scrap heap
  99. int viewType = lp.viewType;
  100. if (!shouldRecycleViewType(viewType)) {
  101. if (viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
  102. removeDetachedView(scrap, false);
  103. }
  104. return;
  105. }
  106. if (mViewTypeCount == 1) {
  107. dispatchFinishTemporaryDetach(scrap);
  108. mCurrentScrap.add(scrap);
  109. } else {
  110. dispatchFinishTemporaryDetach(scrap);
  111. mScrapViews[viewType].add(scrap);
  112. }
  113.  
  114. if (mRecyclerListener != null) {
  115. mRecyclerListener.onMovedToScrapHeap(scrap);
  116. }
  117. }
  118.  
  119. /**
  120. * @return A view from the ScrapViews collection. These are unordered.
  121. */
  122. View getScrapView(int position) {
  123. ArrayList<View> scrapViews;
  124. if (mViewTypeCount == 1) {
  125. scrapViews = mCurrentScrap;
  126. int size = scrapViews.size();
  127. if (size > 0) {
  128. return scrapViews.remove(size - 1);
  129. } else {
  130. return null;
  131. }
  132. } else {
  133. int whichScrap = mAdapter.getItemViewType(position);
  134. if (whichScrap >= 0 && whichScrap < mScrapViews.length) {
  135. scrapViews = mScrapViews[whichScrap];
  136. int size = scrapViews.size();
  137. if (size > 0) {
  138. return scrapViews.remove(size - 1);
  139. }
  140. }
  141. }
  142. return null;
  143. }
  144.  
  145. public void setViewTypeCount(int viewTypeCount) {
  146. if (viewTypeCount < 1) {
  147. throw new IllegalArgumentException("Can't have a viewTypeCount < 1");
  148. }
  149. // noinspection unchecked
  150. ArrayList<View>[] scrapViews = new ArrayList[viewTypeCount];
  151. for (int i = 0; i < viewTypeCount; i++) {
  152. scrapViews[i] = new ArrayList<View>();
  153. }
  154. mViewTypeCount = viewTypeCount;
  155. mCurrentScrap = scrapViews[0];
  156. mScrapViews = scrapViews;
  157. }
  158.  
  159. }

这里的RecycleBin代码并不全,我只是把最主要的几个方法提了出来。那么我们先来对这几个方法进行简单解读,这对后面分析ListView的工作原理将会有很大的帮助。

  • fillActiveViews() 这个方法接收两个参数,第一个参数表示要存储的view的数量,第二个参数表示ListView中第一个可见元素的position值。RecycleBin当中使用mActiveViews这个数组来存储View,调用这个方法后就会根据传入的参数来将ListView中的指定元素存储到mActiveViews数组当中。
  • getActiveView() 这个方法和fillActiveViews()是对应的,用于从mActiveViews数组当中获取数据。该方法接收一个position参数,表示元素在ListView当中的位置,方法内部会自动将position值转换成mActiveViews数组对应的下标值。需要注意的是,mActiveViews当中所存储的View,一旦被获取了之后就会从mActiveViews当中移除,下次获取同样位置的View将会返回null,也就是说mActiveViews不能被重复利用。
  • addScrapView() 用于将一个废弃的View进行缓存,该方法接收一个View参数,当有某个View确定要废弃掉的时候(比如滚动出了屏幕),就应该调用这个方法来对View进行缓存,RecycleBin当中使用mScrapViews和mCurrentScrap这两个List来存储废弃View。
  • getScrapView 用于从废弃缓存中取出一个View,这些废弃缓存中的View是没有顺序可言的,因此getScrapView()方法中的算法也非常简单,就是直接从mCurrentScrap当中获取尾部的一个scrap view进行返回。
  • setViewTypeCount() 我们都知道Adapter当中可以重写一个getViewTypeCount()来表示ListView中有几种类型的数据项,而setViewTypeCount()方法的作用就是为每种类型的数据项都单独启用一个RecycleBin缓存机制。实际上,getViewTypeCount()方法通常情况下使用的并不是很多,所以我们只要知道RecycleBin当中有这样一个功能就行了。

了解了RecycleBin中的主要方法以及它们的用处之后,下面就可以开始来分析ListView的工作原理了,这里我将还是按照以前分析源码的方式来进行,即跟着主线执行流程来逐步阅读并点到即止,不然的话要是把ListView所有的代码都贴出来,那么本篇文章将会很长很长了。

第一次Layout

 

不管怎么说,ListView即使再特殊最终还是继承自View的,因此它的执行流程还将会按照View的规则来执行,对于这方面不太熟悉的朋友可以参考我之前写的 Android视图绘制流程完全解析,带你一步步深入了解View(二) 。

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

如果你到ListView源码中去找一找,你会发现ListView中是没有onLayout()这个方法的,这是因为这个方法是在ListView的父类AbsListView中实现的,代码如下所示:

Android ListView工作原理完全解析(转自 郭霖老师博客)的更多相关文章

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

    版权声明:本文出自郭霖的博客,转载必须注明出处.   目录(?)[+] Adapter的作用 RecycleBin机制 第一次Layout 第二次Layout 滑动加载更多数据   转载请注明出处:h ...

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

    转载请注明出处:http://blog.csdn.net/guolin_blog/article/details/44996879 在Android全部经常使用的原生控件其中.使用方法最复杂的应该就是 ...

  3. android多线程-AsyncTask之工作原理深入解析(下)

    关联文章: Android 多线程之HandlerThread 完全详解 Android 多线程之IntentService 完全详解 android多线程-AsyncTask之工作原理深入解析(上) ...

  4. android多线程-AsyncTask之工作原理深入解析(上)

    关联文章: Android 多线程之HandlerThread 完全详解 Android 多线程之IntentService 完全详解 android多线程-AsyncTask之工作原理深入解析(上) ...

  5. android handler工作原理

    android handler工作原理 作用 便于在子线程中更新主UI线程中的控件 这里涉及到了UI主线程和子线程 UI主线程 它很特别.通常我们会认为UI主线程将页面绘制完成,就结束了.但是它没有. ...

  6. 路由及路由器工作原理深入解析3:路由与port

        日志"路由及路由器工作原理深入解析1"http://user.qzone.qq.com/2756567163/blog/1438322342介绍了"为什么要使用路 ...

  7. NS域名工作原理及解析

    DNS域名工作原理及解析   0x00 定义 DNS( Domain Name System)是“域名系统”的英文缩写,它作为将域名和IP地址相互映射的一个分布式数据库,能够使人更方便地访问互联网.D ...

  8. 基于libevent, libuv和android Looper不断演进socket编程 - 走向架构师之路 - 博客频道 - CSDN.NET

    基于libevent, libuv和android Looper不断演进socket编程 - 走向架构师之路 - 博客频道 - CSDN.NET 基于libevent, libuv和android L ...

  9. Android Widget工作原理详解(一) 最全介绍

    转载请标明出处:http://blog.csdn.net/sk719887916/article/details/46853033 ; Widget是安卓的一应用程序组件,学名窗口小部件,它是微型应用 ...

随机推荐

  1. UML类图<转>

    UML类图     解释UML类图: 1.       首先看“动物”矩形框,它代表一个类.该类图分为三层,第一层显示类的名称,如果是抽象类就要用斜体显示.第二层是类的特性,通常就是字段和属性.第三层 ...

  2. SQL on Hadoop 的真相(2)

    转自:http://blog.jobbole.com/87159/ 这是一组系列博客,目的是详尽介绍 SQL-on-Hadoop .该系列的第一篇会介绍一些存储引擎和在线事务处理(简称 OLTP )相 ...

  3. Hadoop源码分析之数据节点的握手,注册,上报数据块和心跳

    转自:http://www.it165.net/admin/html/201402/2382.html 在上一篇文章Hadoop源码分析之DataNode的启动与停止中分析了DataNode节点的启动 ...

  4. 转载:CSS3图标图形生成技术个人攻略

    原始地址:http://segmentfault.com/a/1190000000481320 出处:http://www.zhangxinxu.com/wordpress/?p=4113

  5. hive经常使用命令

    hive经常使用命令 show tables; 列出hive里面全部数据表名 desc userProfile; 显示数据表userProfile的基本表字段及字段type desc extended ...

  6. MySQL--执行mysql脚本及其脚本编写

    http://www.cnblogs.com/kex1n/archive/2010/03/26/2286504.html

  7. 学习《深入理解C#》—— 可空类型、可选参数和默认值 (第一章1.3)

    目录 C#可空类型 C# 可选参数和默认值 C# 可空类型 在日常生活中,相信大家都离不开手机,低头族啊!哈哈... 假如手机厂商生产了一款新手机,暂时还未定价,在C#1中我们该怎么做呢? 常见的解决 ...

  8. Struts2_day02--Action获取表单提交数据

    Action获取表单提交数据 1 之前web阶段,提交表单到servlet里面,在servlet里面使用request对象里面的方法获取,getParameter,getParameterMap 2 ...

  9. Hibernate_day02--课程安排_主键生成策略_对实体类crud操作_实体类对象状态

    Hibernate_day02 上节内容 今天内容 实体类编写规则 Hibernate主键生成策略 实体类操作 对实体类crud操作 添加操作 根据id查询 修改操作 删除操作 实体类对象状态(概念) ...

  10. iOS开发之-- 从当前隐藏导航界面push到下一个显示导航界面出现闪一下的问题

    在修改项目代码的过程中,遇到一个问题,就是比如主页面的导航栏是隐藏的,但是需要push到别的页面,这个时候,会出现导航栏闪一下的情况, 下面是我写的一种方案,也就是在loadView这个生命周期函数中 ...