Android ListView 源码浅析

OverView

Android ListView 是Android中常用的长列表组件,其继承层次如下:


image.png

用法

通常在业务代码中使用ListView的常用姿势是:

  • 创建1个ListView
  • 创建1个BaseAdapter的子类,实现getCount/getItem/getItemId/getView这4个方法,有时候还会实现getItemViewType/getViewTypeCount方法来满足有多种ItemView样式的需求
  • 将BaseAdatper的子类实例通过ListView的setAdapter()方法,设置给ListView实例

常用优化

通常的ListView在View的复用上有2种优化:

  • public View getView(int position, View convertView, ViewGroup parent)这个方法在实现是,首先判断一下传入的convertView是否为null,不为null即可复用,无需调用inflate或者new来新创建1个View
  • 可以通过ViewHoloder的方法,将convertView的子View直接存一个引用在ViewHolder中,然后将ViewHolder通过convertView的setTag方法存储在convertView上;这种做法的好处在于,通过对子View的直接引用访问,避免了findViewById的耗时操作

源码浅析

ListView的源码比较长,暂时先把精力放在理解Adapter的6个方法(getViewTypeCount/getItemViewType/getCount/getItem/getItemId/getView)被调用的时机上,更详细的源码分析文章已经很多了,比较经典的有郭霖前辈的https://blog.csdn.net/sinyu890807/article/details/44996879

int getViewTypeCount()

在源码中搜索getViewTypeCount()被引用的位置,得到的和ListView相关的结果是在setAdapter(ListAdapter adapter)方法中有1行:

mRecycler.setViewTypeCount(mAdapter.getViewTypeCount());

这个mRecycler成员是RecycleBin类型,RecycleBin的定义在AbsListView中,其作用顾名思义,就是起到1个回收的垃圾箱作用,其setViewTypeCount(int viewTypeCount)方法的实现为:

        public void setViewTypeCount(int viewTypeCount) {
            if (viewTypeCount < 1) {
                throw new IllegalArgumentException("Can't have a viewTypeCount < 1");
            }
            //noinspection unchecked
            ArrayList<View>[] scrapViews = new ArrayList[viewTypeCount];
            for (int i = 0; i < viewTypeCount; i++) {
                scrapViews[i] = new ArrayList<View>();
            }
            mViewTypeCount = viewTypeCount;
            mCurrentScrap = scrapViews[0];
            mScrapViews = scrapViews;
        }

RecycleBin回收废弃View的实现是通过其scrapViews数组实现的,而传入的viewTypeCount决定了这个数组的长度,注意scrapViews的每一个成员是一个ArrayList<View>;在这里我的理解是,viewTypeCount决定了有多少种View会被回收,而每1个被回收的View会根据viewType进入到对应的ArrayList<View>中去,方便在复用时从正确的类型中取出对应的View来进行复用。

int getItemViewType(int position)

在源码中搜索该函数,有好几处调用的地方,但多数调用都是得到viewType之后设置到AbsListView.LayoutParams的viewType属性上使用,这种使用并不是非常重要,真正关键的调用在AbsListView的getScrapView(position)函数中:

        View getScrapView(int position) {
            final int whichScrap = mAdapter.getItemViewType(position);
            if (whichScrap < 0) {
                return null;
            }
            if (mViewTypeCount == 1) {
                return retrieveFromScrap(mCurrentScrap, position);
            } else if (whichScrap < mScrapViews.length) {
                return retrieveFromScrap(mScrapViews[whichScrap], position);
            }
            return null;
        }

getScrapView(int position)函数的作用在于,从废弃的View中获取一个View,准备复用,从实现上可以看出,getItemViewType的作用在于,得到正确的ViewType,从而从对应的mScrapViews数组中取出1个ScrapView

int getCount()

getCount()的调用在源码中的搜索结果就实在是太多了,粗略浏览的一下,把觉得比较关键的点记录下来

  • 首先是ListView的setAdatper()函数中有这么一句:
 mItemCount = mAdapter.getCount();

这个mItemCount是ListView的祖先类AdapterView的成员,设置到这上面之后就能更加方便地使用了

  • 然后是ListView的layoutChildren()函数中有这么一段:
            if (mItemCount == 0) {
                resetList();
                invokeOnItemScrollListener();
                return;
            } else if (mItemCount != mAdapter.getCount()) {
                throw new IllegalStateException("The content of the adapter has changed but "
                        + "ListView did not receive a notification. Make sure the content of "
                        + "your adapter is not modified from a background thread, but only from "
                        + "the UI thread. Make sure your adapter calls notifyDataSetChanged() "
                        + "when its content changes. [in ListView(" + getId() + ", " + getClass()
                        + ") with Adapter(" + mAdapter.getClass() + ")]");
            }

在layoutChildren()过程中需要检查mItemCount和mAdapter.getCount()是否一致,如果不一致,证明数据源被改变了却没有调用notifyDataSetChanged()通知观察方

Object getItem(int position)

getItem(int position)方法主要被调用的地方在AdapterView的getItemAtPosition(int position)函数中:

    public Object getItemAtPosition(int position) {
        T adapter = getAdapter();
        return (adapter == null || position < 0) ? null : adapter.getItem(position);
    }

除此之外,在源码中再没找到getItem(int position)的相关调用,这也可以理解,因为getItem(int position)更主要的使用场景是我们在业务代码中调用,通过该方法,能够从Adapter里拿出数据项,而不需要直接跟数据源接触。

long getItemId(int position)

getItemId(int postion)函数在源码中搜索,ListView中的调用已经被标注为@Deprecate,其余主要的调用都在AbsListView中,选择其中一处来看下这个方法的作用

   public void setItemChecked(int position, boolean value) {
        if (mChoiceMode == CHOICE_MODE_NONE) {
            return;
        }

        // Start selection mode if needed. We don't need to if we're unchecking something.
        if (value && mChoiceMode == CHOICE_MODE_MULTIPLE_MODAL && mChoiceActionMode == null) {
            if (mMultiChoiceModeCallback == null ||
                    !mMultiChoiceModeCallback.hasWrappedCallback()) {
                throw new IllegalStateException("AbsListView: attempted to start selection mode " +
                        "for CHOICE_MODE_MULTIPLE_MODAL but no choice mode callback was " +
                        "supplied. Call setMultiChoiceModeListener to set a callback.");
            }
            mChoiceActionMode = startActionMode(mMultiChoiceModeCallback);
        }

        final boolean itemCheckChanged;
        if (mChoiceMode == CHOICE_MODE_MULTIPLE || mChoiceMode == CHOICE_MODE_MULTIPLE_MODAL) {
            boolean oldValue = mCheckStates.get(position);
            mCheckStates.put(position, value);
            if (mCheckedIdStates != null && mAdapter.hasStableIds()) {
                if (value) {
                    mCheckedIdStates.put(mAdapter.getItemId(position), position);
                } else {
                    mCheckedIdStates.delete(mAdapter.getItemId(position));
                }
            }
            itemCheckChanged = oldValue != value;
            if (itemCheckChanged) {
                if (value) {
                    mCheckedItemCount++;
                } else {
                    mCheckedItemCount--;
                }
            }
            if (mChoiceActionMode != null) {
                final long id = mAdapter.getItemId(position);
                mMultiChoiceModeCallback.onItemCheckedStateChanged(mChoiceActionMode,
                        position, id, value);
            }
        } else {
           ……
            }
            // this may end up selecting the value we just cleared but this way
            // we ensure length of mCheckStates is 1, a fact getCheckedItemPosition relies on
            if (value) {
                mCheckStates.put(position, true);
                if (updateIds) {
                    mCheckedIdStates.put(mAdapter.getItemId(position), position);
                }
                mCheckedItemCount = 1;
            } else if (mCheckStates.size() == 0 || !mCheckStates.valueAt(0)) {
                mCheckedItemCount = 0;
            }
        }

        // Do not generate a data change while we are in the layout phase or data has not changed
        if (!mInLayout && !mBlockLayoutRequests && itemCheckChanged) {
            mDataChanged = true;
            rememberSyncState();
            requestLayout();
        }
    }

可以看到,主要是通过该方法,获得对应位置的id后,能够作为一个索引,用于增删改查等快速操作。

View getView(int position, View convertView, ViewGroup parent)

最后Adapter中最重要的方法,getView方法的作用是返回某一项对应的ItemView,在源码中关键的调用是AbsListView中的obtainView()方法中的调用:

        final View scrapView = mRecycler.getScrapView(position);
        final View child = mAdapter.getView(position, scrapView, this);
        if (scrapView != null) {
            if (child != scrapView) {
                // Failed to re-bind the data, return scrap to the heap.
                mRecycler.addScrapView(scrapView, position);
            } else if (child.isTemporarilyDetached()) {
                outMetadata[0] = true;

                // Finish the temporary detach started in addScrapView().
                child.dispatchFinishTemporaryDetach();
            }
        }

AbsListView的obtainView函数的作用就是构建出某一position对应的View,首先会从mRecycler中取出一个对应的废弃View,这个废弃View就是传入Adapter的getView()方法中的convertView,这里也就解释了为什么需要判空——在首次布局时,实际上是还没有废弃的View可用的,而后面布局时就有废弃的View可复用,无需重新构建了。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,236评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,867评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,715评论 0 340
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,899评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,895评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,733评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,085评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,722评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,025评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,696评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,816评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,447评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,057评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,009评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,254评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,204评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,561评论 2 343