天天看点

【进阶android】ListView源码分析——适配器及观察者模式

     在 日常的编码习惯中,在初始化ListView实例,或者从布局文件之中引用了一个ListView实例之后,我们通常接着干的事儿,便是调用ListView的setAdapter方法,给当前ListView设置一个适配器。

      而在我们的印象中(未看源代码之前),ListView的setAdapter无非是如下实现:

public void setAdapter(ListAdapter adapter) {
   mAdapter = adapter;
}           

      很显然,此种想法是 不完善的!

     在真正的ListView源码之中,setAdapter方法不仅仅只是设置ListView的适配器,而是在此基础上,引入了观察者模式;不说废话,直接贴上源代码:

public void setAdapter(ListAdapter adapter) {
        if (mAdapter != null && mDataSetObserver != null) {
            mAdapter.unregisterDataSetObserver(mDataSetObserver);
        }

        resetList();
        mRecycler.clear();

        if (mHeaderViewInfos.size() > 0|| mFooterViewInfos.size() > 0) {
            mAdapter = new HeaderViewListAdapter(mHeaderViewInfos, mFooterViewInfos, adapter);
        } else {
            mAdapter = adapter;
        }

        mOldSelectedPosition = INVALID_POSITION;
        mOldSelectedRowId = INVALID_ROW_ID;

        // AbsListView#setAdapter will update choice mode states.
        super.setAdapter(adapter);//多选择模式下,清空所有被选择的item

        if (mAdapter != null) {
            mAreAllItemsSelectable = mAdapter.areAllItemsEnabled();//是否所有项目都可选择
            mOldItemCount = mItemCount;//保存上一次的item数量
            mItemCount = mAdapter.getCount();//保存当前item的数量
            checkFocus();

            mDataSetObserver = new AdapterDataSetObserver();
            mAdapter.registerDataSetObserver(mDataSetObserver);//注册数据观察者

            mRecycler.setViewTypeCount(mAdapter.getViewTypeCount());//item共有多少中视图类型

            int position;
            if (mStackFromBottom) {//从下到上
                position = lookForSelectablePosition(mItemCount - 1, false);
            } else {//从上到下
                position = lookForSelectablePosition(0, true);
            }
            setSelectedPositionInt(position);//当前选择的item的位置
            setNextSelectedPositionInt(position);//下次布局时,选择的item的位置

            if (mItemCount == 0) {
                // Nothing selected
                checkSelectionChanged();
            }
        } else {
           ......
        }

        requestLayout();
    }           

        整个ListView的setAdapter方法流程可用总结如下:

        1、如果存在mAdapter及mDataSetObserver(AbsListView.AdapterDataSetObserver实例),则注销适配器对观察者的注册;

        2、调用resetList()方法,重置列表:1)将所有的header或footer对应的视图的参数(recycledHeaderFooter)设置为false;2)将一些变量初始化为无效值;

        3、调用mRecycler.clear()方法,清空二维数组废弃视图堆、临时视图数组中所有的废弃视图;mRecycler便是ListView的重用视图工具,后文会详细介绍;

        4、如果存在页眉或者页脚视图,则将传进方法里的适配器封装为一个HeaderViewListAdapter实例,再将其赋值给当前ListView的适配器;

        5、将上一次所选择的item的位置及行ID置空;

        6、调用super.setAdapter(adapter)方法;AbsListView中的setAdapter方法只干了一件事,那就是如果当前ListView能够选择多个item(多选择模式),清空已经选择的所有item;

        7、更新mItemCount、mOldItemCount两个变量;前者当前处理的item数量;后者保存前一次处理的item数量;

        8、重新实例化一个AbsListView.AdapterDataSetObserver对象,并注册到方法入参传来的适配器之中;

        9、设置item的视图类型;mRecycler会对每一种视图类型创建一个对应的废弃视图堆,用以为该种视图类型提供重用的视图;

        10、找到当前被选择的item的位置和行ID,并将其赋值于本次布局被选择item的位置和下次布局被选择item的位置设置;

        11、请求布局。

        以上11个步骤中,第一、四、八步骤是本章的重点;同时会结合BaseAdapter类来分析适配器的notifyDataSetChanged()和notifyDataSetInvalidated()两个方法的实现流程;综上所述,笔者将分四方面来阐述:

        1、适配器注册、注销观察者;

        2、HeaderViewListAdapter类;

        3、BaseAdapter中的notifyDataSetChanged()方法;

        4、BaseAdapter中的notifyDataSetInvalidated()方法;

一、适配器注册、注销观察者;

       适配器(BaseAdapter为例子)之中存在一个DataSetObservable的实例,DataSetObservable类相当于是一个元素类型为观察者(DataSetObserver)的ArrayList的封装,而封装的主要目的就是确保这个ArrayList是在一种线程安全的状态下,添加、删除元素。

       观察者DataSetObserver是一个抽象类,该类代码如下:

public abstract class DataSetObserver {
    /**
     * This method is called when the entire data set has changed,
     * most likely through a call to {@link Cursor#requery()} on a {@link Cursor}.
     */
    public void onChanged() {
        // Do nothing
    }

    /**
     * This method is called when the entire data becomes invalid,
     * most likely through a call to {@link Cursor#deactivate()} or {@link Cursor#close()} on a
     * {@link Cursor}.
     */
    public void onInvalidated() {
        // Do nothing
    }
}           

二、HeaderViewListAdapter类;

         HeaderViewListAdapter类,是对ListAdapter(BaseAdapter实现了该接口)的一种封装;而封装的主要目的是使得适配器能够像处理正常的item数据那样去处理页眉或者页脚,也就是说如果调用HeaderViewListAdapter类中的getCount方法,不止会返回所有item的数量,还会返回页眉、页脚数据的数量(也就是三种数据数量之和)。此外,该类还会提供一些专门针对页眉、页脚的方法,例如删除页眉(脚)、添加页眉(脚),获取页眉(脚)一种类型的总数量等。

       与一般item数据相比,页眉(脚)数据是一种ListView.FixedViewInfo类型,FixedViewInfo类型将数据和视图绑定在了一起;而一般的item数据则是将数据与视图分离,只有在调用ListAdapter的getView方法才将数据和视图绑定在一起。如果是一般的item数据,则当调用HeaderViewListAdapter类的getView方法时,实际上调用的是ListAdapter类的getView。

三、BaseAdapter中的notifyDataSetChanged()方法;

      前两点主要讲述有关的都和适配器有关,第三、四点则侧重与观察者模式;在重点讲述ListView的观察者流程之前;先要明确一点,那就是什么是观察者模式;所谓观察者模式,即一个目标物件管理所有相依于它的观察者物件,并且在它本身的状态改变时主动发出通知。将这个概念拿到ListView之中,进一步而言,拿到BaseAdapter之中,BaseAdapter是被观察者,DataSetObserver则是观察者,当BaseAdapter发觉到有数据变化时,则将通知所有注册于适配器之中的观察者。因此,适配器提供了两个方法用以主动通知所有的观察者。当使用适配器时,如果发生数据改变时,应该主动调用这两个方法。

      言归正传,这两个方法分别为notifyDataSetChanged()和notifyDataSetInvalidated();我们先看notifyDataSetChanged()。

      BaseAdapter中的notifyDataSetChanged()代码如下:

public void notifyDataSetChanged() {
    mDataSetObservable.notifyChanged();
}           

       根据第一点所示,mDataSetObservable乃DataSetObservable的一个对象。DataSetObservable中的notifyChanged方法则主要依次调用注册于其中DataSetObserver对象的onChange方法。DataSetObserver是一个抽象类,其中主要定义了两个什么都没有做的方法:onChanged()和onInvalidated();BaseAdapter,或者直接说ListView之中并未直接使用DataSetObserver,而是使用它的一个直接子类AdapterView.AdapterDataSetObserver;

      此时,我们可用进行一个总结,BaseAdapter的notifyDataSetChanged()方法的处理本质,DataSetObservable类中的onChanged()方法;而在ListView中,DataSetObservable对象实际上是AdapterDataSetObserver对象;因此,下面我们重点看看AdapterDataSetObserver的onChanged()方法。

     AdapterDataSetObserver的onChanged()方法源码如下:

public void onChanged() {
            mDataChanged = true;
            mOldItemCount = mItemCount;
            mItemCount = getAdapter().getCount();

            // Detect(检测) the case where a cursor that was previously
            //(之前) invalidated(作废) has
            // been repopulated with new data.
            if (AdapterView.this.getAdapter().hasStableIds() && mInstanceState != null
                    && mOldItemCount == 0 && mItemCount > 0) {
                AdapterView.this.onRestoreInstanceState(mInstanceState);
                mInstanceState = null;
            } else {
            	/*当调用adpter中的notifyDataSetChanged方法后,
            	 *adpterView视图(例如ListView)的屏幕会恢复到
            	 *调用notifyDataSetChanged方法之前的位置;其原
            	 *因就是在重回界面之前调用rememberSyncState方法
            	 *记录被选则项目的位置*/
                rememberSyncState();
            }
            checkFocus();
            requestLayout();
        }           

                这个方法之中可以分为四个步骤:

                1、将mDataChanged设置为true,标识此时的ListView数据已经改变;

                2、更新item的数量,并记录上一次的item数量;

                3、调用rememberSyncState()方法,保存当前同步状态,确保布局成功后能够恢复相应的状态;

                4、检测焦点,请求布局。

               四个步骤中,1、2、4此刻暂不细讲,直接进入remeberSyncState()方法,源码如下:

void rememberSyncState() {
        if (getChildCount() > 0) {
            mNeedSync = true;
            mSyncHeight = mLayoutHeight;
            if (mSelectedPosition >= 0) {//存在被选择的item
                // Sync the selection state
                View v = getChildAt(mSelectedPosition - mFirstPosition);//获取对应的视图
                mSyncRowId = mNextSelectedRowId;
                mSyncPosition = mNextSelectedPosition;
                if (v != null) {
                    mSpecificTop = v.getTop();
                }
                mSyncMode = SYNC_SELECTED_POSITION;
            } else {
                // Sync the based on the offset of the first view
                View v = getChildAt(0);
                T adapter = getAdapter();
                if (mFirstPosition >= 0 && mFirstPosition < adapter.getCount()) {
                    mSyncRowId = adapter.getItemId(mFirstPosition);
                } else {
                    mSyncRowId = NO_ID;
                }
                mSyncPosition = mFirstPosition;
                if (v != null) {
                    mSpecificTop = v.getTop();
                }
                mSyncMode = SYNC_FIRST_POSITION;
            }
        }
    }           

       该方法的主要目的是保存相关用以同步的变量(mSyncHeight、mSyncRowId、mSyncPosition及mSyncMode),从而确保在布局之后,能够通过这些同步变量的值来恢复相应的屏幕状态(例如屏幕位置、视图滚动量等)。

       首先细说一下这四个同步变量的意义;

  •        mSyncHeight:同步时视图的高度;
  •        mSyncPosition:同步时,从哪个位置开始寻找具有mSyncRowId的item;
  •        mSyncRowId:进行同步时,从哪个item开始同步;
  •        mSyncMode:同步模式;一共有两种同步模式:SYNC_SELECTED_POSITION和SYNC_FIRST_POSITION;SYNC_SELECTED_POSITION表示同步的item是当前被选择的item;SYNC_FIRST_POSITION表示同步的item是当前展示屏幕中第一个子视图对应的item。

       该方法之中,首先将mNeedSync设置为true表示布局之后需要同步,而后更新mSyncHeight,接着根据是否存在被选择的item来确定同步模式;若是SYNC_SELECTED_POSITION模式则将mNextSelectedRowId与mNextSelectedPosition分别设置给mSyncRowId及mSyncPosition;若是SYNC_FIRST_POSITION模式则将当前屏幕中的第一个视图对应的item的行ID及位置设置给mSyncRowId及mSyncPosition。

       在ListView中mNextSelectedRowId与mNextSelectedPosition表示布局之后的选择item的行ID及位置。

四、BaseAdapter中的notifyDataSetInvalidated()方法;

     与BaseAdapter类中的notifyDataSetChanged()方法类似,notifyDataSetInvalidated()方法实质是调用的DataSetObservable类中的onInvalidated()方法;而在ListView中,DataSetObservable对象实际上是AdapterDataSetObserver对象;因此我们直接分析AdapterDataSetObserver中的onInvalidated()方法。

       源代码如下:

public void onInvalidated() {
            mDataChanged = true;

            if (AdapterView.this.getAdapter().hasStableIds()) {
                // Remember the current state for the case where our hosting activity is being
                // stopped and later restarted
                mInstanceState = AdapterView.this.onSaveInstanceState();
            }

            // Data is invalid so we should reset our state
            mOldItemCount = mItemCount;
            mItemCount = 0;
            mSelectedPosition = INVALID_POSITION;
            mSelectedRowId = INVALID_ROW_ID;
            mNextSelectedPosition = INVALID_POSITION;
            mNextSelectedRowId = INVALID_ROW_ID;
            mNeedSync = false;

            checkFocus();
            requestLayout();
        }           

    该方法主要的目的是将数据集设置为无效,进一步而言是将当前选择item及布局后的选择item设置为无效,并且不进行相应的同步。

    最后,我们来对比一下notifyDataSetChanged()和notifyDataSetInvalidated()两者的异同点。

    相同点:

  1. 两者都是采用数据观察者模式进行实现的;
  2. 两者都进行了数据的改变,将mDataChanged变量设置为true;
  3. 两者都更新了当前item数量,保存了上一次item的数量;
  4. 两者都检查了焦点,请求了重新布局;从而使得屏幕界面都将进行刷新。

     不同点:

       两者最主要的一个不同便是对同步信息的处理;notifyDataSetChanged()是需要保存同步信息的,及布局后要恢复布局前的同步信息;而notifyDataSetInvalidated()则是不必保存同步信息,布局后也无需恢复同步信息。举一个简单的例子,假设当前屏幕第一个视图对应的item的位置为5,那么调用notifyDataSetChanged()方法后,界面会使得屏幕第一个视图对应的item的位置恢复到5,而调用notifyDataSetInvalidated()方法之后,界面屏幕第一个视图对应的item的位置则将初始化为0。