看源码学习Android之AdapterView的convertView

本文通过源码分析探讨了Android中的AdapterView,尤其是ListView和GridView如何利用convertView提高性能。作者详细介绍了convertView的工作原理,从AbsListView的obtainView()方法开始,揭示了convertView的获取和复用过程。在滑动过程中,上滚和下滚的convertView策略有所不同,并强调了在使用convertView时进行必要初始化的重要性。

开源项目,最大的好处就是源码可以看到,这样你就可以知道它是怎么工作的,对于解决问题是十分有帮助的。

最近用到AdapterViewListView,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()分析的不够清楚)

       

在这写解决问题的文章,感觉解决问题的过程很顺畅,实际上不是这样的,就像拍一部电影,有些镜头要不知拍多少遍一样。哇,真的好花时间呀!!!

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值