看源码学习Android之AdapterView的convertView
2013-11-03 01:22
267 查看
开源项目,最大的好处就是源码可以看到,这样你就可以知道它是怎么工作的,对于解决问题是十分有帮助的。
最近用到AdapterView:ListView,GridView
发现了一个棘手的问题:Android为了提高AdapterView的性能,为getView()方法添传入一个convertView来重复利用,但是确来了一些奇怪的问题。
于是我就想弄明白它是怎么工作的,怎么重复利用的,于是就有了这篇文章。
源码:Android 4.2.2
1.找到问题源头
首先我得先知道getView是谁调用的convertView是怎么来的,(有没有什么工具可以显示函数的调用栈,如果有哪个小伙伴知道,告诉一声,啊,用调试器更简单),我想到用抛出异常的形式来显示函数调用栈,在getView里抛出一个异常:
显示函数调用栈如下:
11-02 14:07:34.606: E/AndroidRuntime(2347): java.lang.RuntimeException: getView
11-02 14:07:34.606: E/AndroidRuntime(2347): at com.example.testadapterview.MyAdapter.getView(MainActivity.java:64)
11-02 14:07:34.606: E/AndroidRuntime(2347): at android.widget.AbsListView.obtainView(AbsListView.java:2159)
11-02 14:07:34.606: E/AndroidRuntime(2347): at android.widget.ListView.makeAndAddView(ListView.java:1831)
11-02 14:07:34.606: E/AndroidRuntime(2347): at android.widget.ListView.fillDown(ListView.java:674)
11-02 14:07:34.606: E/AndroidRuntime(2347): at android.widget.ListView.fillFromTop(ListView.java:735)
11-02 14:07:34.606: E/AndroidRuntime(2347): at android.widget.ListView.layoutChildren(ListView.java:1652)
11-02 14:07:34.606: E/AndroidRuntime(2347): at android.widget.AbsListView.onLayout(AbsListView.java:1994)
。。。
。。。
11-02 14:07:34.606: E/AndroidRuntime(2347): at dalvik.system.NativeStart.main(Native Method)
发现是在AbsListView.obtainView()中调用了getView()
好,那我们就去看看AbsListView。
2.解决问题
有源码,我们可以直接到源码中找,但是我们要充分利用Eclipse的强大功能,随便找个地方(类外不行),定一个变量名随便:
AbsListView abs;
按住ctrl点击AbsListView,如果你在Eclispe中绑定了源码的话就会跳到AbsListView的源码中(否则,跳到了类文件里,字节码看不懂,你可以点击attach,可以选择对应的源码,如果没有源码要去。。。).
来到AbsListView,查找到obtainView(),发现这个函数代码函数不多,太幸福了
找到调用getView的地方(直接查getView不也行吗?,是啊,呵呵)
发现传入的是一个scrapView
往前找发现
scrapView = mRecycler.getScrapView(position);
mRecycler这名一看就很高兴,回收器,convertView就是重复利用的(我承认,convert意思是改变什么的形式或用途,但是就是感觉和回收有关系,请参考文档说明)。
我们去看看getScrapView() (本来,ctrl左键可以直接点击函数名进入的,为什么这里不行呢,有人知道吗??因为不能进入,就不知道哪个类在哪了,查看import发现没有mRecycler的类RecycleBin,有可能是引入的所在包,有可能是静态导入,有可能它就在这,就在本文件中,于是直接搜getScrapView)
这个函数更小,而且还牵扯到View类型的问题(比如ListView条目可能有不同的布局,这时要设置类型,以便在复用时分类分类复用,否则,你懂得)我们这里就不研究了。这样问题转到了
再来看retrieveFromScrap,搜索:(。
这里有点意思了,遍历所有scrapViews,找到传入的position指定位置的View,如果有就返回它并从数组(请容忍我教他数组)移除,否则取最后一个并移除。
我们好像找到最根源了,复用的convertView是从mCurrentScrap中拿的。
但是还有问题:
怎么放里边的呢?
mCurrentScrap是ArrayList,它有get方法,也有put方法,所以接下来我们搜索mCurrentScrap.add。
此时我发现我们该回去了(sorry,当我们调用了一个函数,研究完了就该返回了)。我们先留着这个问题,继续研究我们的obtainView()
如果scrapView是null,就只是调用
child = mAdapter.getView(position, null, this);
否则
child = mAdapter.getView(position, scrapView, this);
然后看是不是使用了scrapView,如果使用了scrapView,就把它添加为ScrapView(这是addScrapView()函数名告诉我的)
接下来当然是看如何添加ScrapView,这和之前我们留下的那个问题肯定是一个。既然是一个那就又有疑问了,现在是这样的,getScrapView()取得scraptview并将其从数组删掉,然后在这里将如果换了一个新的就把新的添加进去。也就是说,刚开始mCurrentScrap是空的,在obtainView()中是不会向里添加。那么肯定还有其他地方添加了。
搜索addScrapView,找到了它的定义,同是发现也的确有其他地方调用了它:trackMotionScroll(),我们先来看addScrapView()
判断viewType如果应该回收就将其添加到mCurrentScrap否则不添加
最后让我们来研究trackMotionScroll(),是最复杂的了(为什么最后是复杂的,黎明之前最黑暗?)
不过我们不能跑了题(这是看源码的大忌),我们有目标,围绕目标来研究。
trackMotionScroll()中用到了两个addScrapView(),分别在下滚和上滚中,
if(down)
....
else
....
也就是说上滚和下滚的策略是不一样的,研究代码后发现,这是方向不同,策略是一样的这样我们只分析其中一半就行了,以下是下滑是的代码片段:
从上到下遍历Adapterview中的每一个孩子,如果孩子的bootom大于AdapterView的top,即显示出屏幕了,就终止,否则再判断如果不是header或是footer就把它添加到回收队列。
总结
1.convertView是用来重用的。
2.但是,我们需要对其做必要的初始化,尤其是在异步加载时,如果不初始化会显示一些错误信息,严重影响用户体验
3.一个listview,如果条目没有超过一屏或(超过一屏没有滑动),就没有convertView。
4.当向上滑时,从屏幕滚出的view会被添加到回收队列,从而,下部出现的view就可以使用convertview了(trackMotionScroll()分析的不够清楚)
在这写解决问题的文章,感觉解决问题的过程很顺畅,实际上不是这样的,就像拍一部电影,有些镜头要不知拍多少遍一样。哇,真的好花时间呀!!!
最近用到AdapterView:ListView,GridView
发现了一个棘手的问题:Android为了提高AdapterView的性能,为getView()方法添传入一个convertView来重复利用,但是确来了一些奇怪的问题。
于是我就想弄明白它是怎么工作的,怎么重复利用的,于是就有了这篇文章。
源码:Android 4.2.2
1.找到问题源头
首先我得先知道getView是谁调用的convertView是怎么来的,(有没有什么工具可以显示函数的调用栈,如果有哪个小伙伴知道,告诉一声,啊,用调试器更简单),我想到用抛出异常的形式来显示函数调用栈,在getView里抛出一个异常:
public View getView(int position, View convertView, ViewGroup parent) { if (convertView==null) { throw new RuntimeException("getView"); } return convertView; }
显示函数调用栈如下:
11-02 14:07:34.606: E/AndroidRuntime(2347): java.lang.RuntimeException: getView
11-02 14:07:34.606: E/AndroidRuntime(2347): at com.example.testadapterview.MyAdapter.getView(MainActivity.java:64)
11-02 14:07:34.606: E/AndroidRuntime(2347): at android.widget.AbsListView.obtainView(AbsListView.java:2159)
11-02 14:07:34.606: E/AndroidRuntime(2347): at android.widget.ListView.makeAndAddView(ListView.java:1831)
11-02 14:07:34.606: E/AndroidRuntime(2347): at android.widget.ListView.fillDown(ListView.java:674)
11-02 14:07:34.606: E/AndroidRuntime(2347): at android.widget.ListView.fillFromTop(ListView.java:735)
11-02 14:07:34.606: E/AndroidRuntime(2347): at android.widget.ListView.layoutChildren(ListView.java:1652)
11-02 14:07:34.606: E/AndroidRuntime(2347): at android.widget.AbsListView.onLayout(AbsListView.java:1994)
。。。
。。。
11-02 14:07:34.606: E/AndroidRuntime(2347): at dalvik.system.NativeStart.main(Native Method)
发现是在AbsListView.obtainView()中调用了getView()
好,那我们就去看看AbsListView。
2.解决问题
有源码,我们可以直接到源码中找,但是我们要充分利用Eclipse的强大功能,随便找个地方(类外不行),定一个变量名随便:
AbsListView abs;
按住ctrl点击AbsListView,如果你在Eclispe中绑定了源码的话就会跳到AbsListView的源码中(否则,跳到了类文件里,字节码看不懂,你可以点击attach,可以选择对应的源码,如果没有源码要去。。。).
来到AbsListView,查找到obtainView(),发现这个函数代码函数不多,太幸福了
找到调用getView的地方(直接查getView不也行吗?,是啊,呵呵)
发现传入的是一个scrapView
往前找发现
scrapView = mRecycler.getScrapView(position);
mRecycler这名一看就很高兴,回收器,convertView就是重复利用的(我承认,convert意思是改变什么的形式或用途,但是就是感觉和回收有关系,请参考文档说明)。
我们去看看getScrapView() (本来,ctrl左键可以直接点击函数名进入的,为什么这里不行呢,有人知道吗??因为不能进入,就不知道哪个类在哪了,查看import发现没有mRecycler的类RecycleBin,有可能是引入的所在包,有可能是静态导入,有可能它就在这,就在本文件中,于是直接搜getScrapView)
这个函数更小,而且还牵扯到View类型的问题(比如ListView条目可能有不同的布局,这时要设置类型,以便在复用时分类分类复用,否则,你懂得)我们这里就不研究了。这样问题转到了
if (mViewTypeCount == 1) { return retrieveFromScrap(mCurrentScrap, position);
再来看retrieveFromScrap,搜索:(。
int size = scrapViews.size(); if (size > 0) { // See if we still have a view for this position. for (int i=0; i<size; i++) { View view = scrapViews.get(i); if (((AbsListView.LayoutParams)view.getLayoutParams()) .scrappedFromPosition == position) { scrapViews.remove(i); return view; } } return scrapViews.remove(size - 1); } else { return null; }
这里有点意思了,遍历所有scrapViews,找到传入的position指定位置的View,如果有就返回它并从数组(请容忍我教他数组)移除,否则取最后一个并移除。
我们好像找到最根源了,复用的convertView是从mCurrentScrap中拿的。
但是还有问题:
怎么放里边的呢?
mCurrentScrap是ArrayList,它有get方法,也有put方法,所以接下来我们搜索mCurrentScrap.add。
此时我发现我们该回去了(sorry,当我们调用了一个函数,研究完了就该返回了)。我们先留着这个问题,继续研究我们的obtainView()
如果scrapView是null,就只是调用
child = mAdapter.getView(position, null, this);
否则
child = mAdapter.getView(position, scrapView, this);
然后看是不是使用了scrapView,如果使用了scrapView,就把它添加为ScrapView(这是addScrapView()函数名告诉我的)
if (child != scrapView) { mRecycler.addScrapView(scrapView, position); if (mCacheColorHint != 0) { child.setDrawingCacheBackgroundColor(mCacheColorHint); } if (ViewDebug.TRACE_RECYCLER) { ViewDebug.trace(scrapView, ViewDebug.RecyclerTraceType.MOVE_TO_SCRAP_HEAP, position, -1); } } else { isScrap[0] = true; child.dispatchFinishTemporaryDetach(); }
接下来当然是看如何添加ScrapView,这和之前我们留下的那个问题肯定是一个。既然是一个那就又有疑问了,现在是这样的,getScrapView()取得scraptview并将其从数组删掉,然后在这里将如果换了一个新的就把新的添加进去。也就是说,刚开始mCurrentScrap是空的,在obtainView()中是不会向里添加。那么肯定还有其他地方添加了。
搜索addScrapView,找到了它的定义,同是发现也的确有其他地方调用了它:trackMotionScroll(),我们先来看addScrapView()
AbsListView.LayoutParams lp = (AbsListView.LayoutParams) scrap.getLayoutParams();
int viewType = lp.viewType;
if (!shouldRecycleViewType(viewType)) { if (viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) { removeDetachedView(scrap, false); } return; } lp.scrappedFromPosition = position;
if (mViewTypeCount == 1) { scrap.dispatchStartTemporaryDetach(); mCurrentScrap.add(scrap); } else { scrap.dispatchStartTemporaryDetach(); mScrapViews[viewType].add(scrap); }
判断viewType如果应该回收就将其添加到mCurrentScrap否则不添加
最后让我们来研究trackMotionScroll(),是最复杂的了(为什么最后是复杂的,黎明之前最黑暗?)
不过我们不能跑了题(这是看源码的大忌),我们有目标,围绕目标来研究。
trackMotionScroll()中用到了两个addScrapView(),分别在下滚和上滚中,
if(down)
....
else
....
也就是说上滚和下滚的策略是不一样的,研究代码后发现,这是方向不同,策略是一样的这样我们只分析其中一半就行了,以下是下滑是的代码片段:
for (int i = 0; i < childCount; i++) { final View child = getChildAt(i); if (child.getBottom() >= top) { break; } else { count++; int position = firstPosition + i; if (position >= headerViewsCount && position < footerViewsStart) { mRecycler.addScrapView(child, position); if (ViewDebug.TRACE_RECYCLER) { ViewDebug.trace(child, ViewDebug.RecyclerTraceType.MOVE_TO_SCRAP_HEAP, firstPosition + i, -1); } } } }
从上到下遍历Adapterview中的每一个孩子,如果孩子的bootom大于AdapterView的top,即显示出屏幕了,就终止,否则再判断如果不是header或是footer就把它添加到回收队列。
总结
1.convertView是用来重用的。
2.但是,我们需要对其做必要的初始化,尤其是在异步加载时,如果不初始化会显示一些错误信息,严重影响用户体验
3.一个listview,如果条目没有超过一屏或(超过一屏没有滑动),就没有convertView。
4.当向上滑时,从屏幕滚出的view会被添加到回收队列,从而,下部出现的view就可以使用convertview了(trackMotionScroll()分析的不够清楚)
在这写解决问题的文章,感觉解决问题的过程很顺畅,实际上不是这样的,就像拍一部电影,有些镜头要不知拍多少遍一样。哇,真的好花时间呀!!!
相关文章推荐
- Android的Context Manager(服务管理器)源码剖析-android学习之旅(99)
- android例子源码学习
- Android程序开发学习笔记系列——基础篇(附源码)
- Android源码学习之浅析SystemServer脉络
- android源码学习之animation1
- Android源码学习——ClasLoader(3)
- Android2.1消息应用(Messaging)源码学习笔记
- android开发——框架理解及源码学习计划
- Android学习备忘022——FBReader源码解析备忘
- 原 android 涂鸦(清屏,画笔,粗细,保存)以及canvas源码学习
- 关于学习的几点总结——Android源码学习有感
- Android Intent 源码学习
- Android学习进阶路线导航线路(附源码)
- Linphone-Android源码学习(二、LinphoneLauncherActivity)
- Android电话短信拦截项目总结之 项目源码及相关学习资料
- 学习笔记五:Ubuntu下载编译Android源码
- Android源码学习之三-Activity是如何进行自动化测试的
- Android源码学习之观察者模式应用
- amdroid源码学习系列之--建立android系统开发环境
- 开放源码的安卓天气应用-android学习之旅(73)