最熟悉的陌生人:ListView 中的观察者模式

RecyclerView 得宠之前,ListView 可以说是我们用的最多的组件。之前一直没有好好看看它的源码,知其然不知其所以然。 今天我们来窥一窥 ListView 中的观察者模式。 不熟悉观察者模式的可以看看这篇 观察者模式 : 一支穿云箭,千军万马来相见 巩固一下。

在我们使用 ListView 的过程中,经常需要修改 Item 的状态,比如添加、删除、选中等等,通常的操作是在对数据源进行操作后,调用 notifyDataSetChanged() ,比如:

    public void addData(String data) {
        if (mData != null) {
            mData.add(data);
            notifyDataSetChanged();
        }
    }

随后 ListView 中的数据就会更新,我们可以猜到这个过程是把全部 Item View 重新绘制、数据绑定了一遍,这个场景跟观察者模式很一致,具体怎么实现的呢

前方高能预警,代码太多看不下去的可以先翻到篇尾看看流程图,有点印象再回来继续啃的,不然容易晕。

1.首先我们跟进去看下 notifyDataSetChanged() 源码,进入了系统的 BaseAdapter

    /**
     * Notifies the attached observers that the underlying data has been changed
     * and any View reflecting the data set should refresh itself.
     */
    public void notifyDataSetChanged() {
        mDataSetObservable.notifyChanged();
    }

看注释,“通知观察者数据已经改变,任何和数据集绑定的 View 都应该刷新”,的确是观察者模式。

那发布者、观察者是谁?在什么时候注册的?观察者的 notifyChanged() 方法又做了什么呢?

2.在 BaseAdapter 中我们可以看到这几个方法:

public abstract class BaseAdapter implements ListAdapter, SpinnerAdapter {
    private final DataSetObservable mDataSetObservable = new DataSetObservable();

    public boolean hasStableIds() {
        return false;
    }

    /**
    * BaseAdapter 提供了 注册订阅方法
    */
    public void registerDataSetObserver(DataSetObserver observer) {
        mDataSetObservable.registerObserver(observer);
    }

    /**
    * 还提供了 解除订阅方法
    */
    public void unregisterDataSetObserver(DataSetObserver observer) {
        mDataSetObservable.unregisterObserver(observer);
    }

    /**
     * 数据更新时通知观察者
     */
    public void notifyDataSetChanged() {
        mDataSetObservable.notifyChanged();
    }

    /**
     * 提醒观察者散了,别看了,数据不可用了
     * /
    public void notifyDataSetInvalidated() {
        mDataSetObservable.notifyInvalidated();
    }
    //省略无关代码
}

BaseAdapter 提供了 注册订阅、解除订阅、提醒观察者数据更新、告诉观察者数据不可用 等关键方法。

其中 DataSetObservable 是发布者:

/**
 * A specialization of {@link Observable} for {@link DataSetObserver}
 * that provides methods for sending notifications to a list of
 * {@link DataSetObserver} objects.
 */
public class DataSetObservable extends Observable<DataSetObserver> {
    /**
     * 发出更新提醒
     */
    public void notifyChanged() {
        synchronized(mObservers) {
            for (int i = mObservers.size() - 1; i >= 0; i--) {
                mObservers.get(i).onChanged();
            }
        }
    }

    /**
     * 发出数据集无法使用通知
     */
    public void notifyInvalidated() {
        synchronized (mObservers) {
            for (int i = mObservers.size() - 1; i >= 0; i--) {
                mObservers.get(i).onInvalidated();
            }
        }
    }
}

可以看到 notifyChanged 方法的注释中,是倒序遍历观察者集合并进行通知,这是为了避免观察者列表的 iterator 被使用时,进行删除操作导致出问题。

DataSetObservable 继承自 Observable < DataSetObserver > ,看下 Observable 源码:

public abstract class Observable<T> {
    /**
     * 观察者列表,不能重复,不能为空
     */
    protected final ArrayList<T> mObservers = new ArrayList<T>();

    /**
     * 注册一个观察者,不能重复,不能为空
     */
    public void registerObserver(T observer) {
        if (observer == null) {
            throw new IllegalArgumentException("The observer is null.");
        }
        synchronized(mObservers) {
            if (mObservers.contains(observer)) {
                throw new IllegalStateException("Observer " + observer + " is already registered.");
            }
            mObservers.add(observer);
        }
    }

    /**
     * 解除注册一个观察者
     */
    public void unregisterObserver(T observer) {
        if (observer == null) {
            throw new IllegalArgumentException("The observer is null.");
        }
        synchronized(mObservers) {
            int index = mObservers.indexOf(observer);
            if (index == -1) {
                throw new IllegalStateException("Observer " + observer + " was not registered.");
            }
            mObservers.remove(index);
        }
    }

    /**
     * 移除所有观察者
     */
    public void unregisterAll() {
        synchronized(mObservers) {
            mObservers.clear();
        }
    }
}

DataSetObserver 就是观察者抽象类,将来需要被具体观察者者继承:

/**
 * DataSetObserver must be implemented by objects which are added to a DataSetObservable.
 */
public abstract class DataSetObserver {
    /**
     * 数据改变时调用
     */
    public void onChanged() {
        // Do nothing
    }

    /**
     * 数据不可用时调用
     */
    public void onInvalidated() {
        // Do nothing
    }
}

了解发布者、观察者基类后,接下来去看下在什么时候进行注册、通知。

3.ListView.setAdapter 源码:

public void setAdapter(ListAdapter adapter) {
        //移除旧的观察者
        if (mAdapter != null && mDataSetObserver != null) {
            mAdapter.unregisterDataSetObserver(mDataSetObserver);
        }

        //省略不相关内容...

        if (mAdapter != null) {
            //...

            //初始化新观察者并注册
            mDataSetObserver = new AdapterDataSetObserver();
            mAdapter.registerDataSetObserver(mDataSetObserver);

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

        requestLayout();
    }

可以看到在 ListView.setAdapter 方法中,先解除旧的观察者,然后初始化了新的观察者 AdapterDataSetObserver 并注册。

而 AdapterDataSetObserver 定义在 ListView 的父类 AbsListView 中:

class AdapterDataSetObserver extends AdapterView<ListAdapter>.AdapterDataSetObserver {
        @Override
        public void onChanged() {
            super.onChanged();
            if (mFastScroll != null) {
                mFastScroll.onSectionsChanged();
            }
        }

        @Override
        public void onInvalidated() {
            super.onInvalidated();
            if (mFastScroll != null) {
                mFastScroll.onSectionsChanged();
            }
        }
    }

AdapterDataSetObserver 继承自 AdapterView.AdapterDataSetObserver,在 onChanged 和 onInvalidated 方法中先调用 AdapterView.AdapterDataSetObserver 对应的方法,然后调用了 mFastScroll.onSectionsChanged();

先看 AdapterView.AdapterDataSetObserver :

class AdapterDataSetObserver extends DataSetObserver {

        private Parcelable mInstanceState = null;

        @Override
        public void onChanged() {
            //更新 数据修改状态
            mDataChanged = true;
            //更新 数据数量
            mOldItemCount = mItemCount;
            //更新 ItemView 数量
            mItemCount = getAdapter().getCount();

            // 监测是否有数据之前不可用、现在可用
            // 由于 BaseAdapter.hasStableIds() 默认返回 false ,所以我们直接看 else
            if (AdapterView.this.getAdapter().hasStableIds() && mInstanceState != null
                    && mOldItemCount == 0 && mItemCount > 0) {
                AdapterView.this.onRestoreInstanceState(mInstanceState);
                mInstanceState = null;
            } else {
               //记录当前状态,接下来刷新时要用到这些状态
                rememberSyncState();
            }
            checkFocus();
            requestLayout();
        }

        @Override
        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();
        }

        public void clearSavedState() {
            mInstanceState = null;
        }
    }

看 onChanged() 方法,这个方法中先后更新了 数据更新状态(mDataChanged ),数据数量,而由于 BaseAdapter.hasStableIds() 默认返回 false , 所以我们直接看 else 情况下 rememberSyncState 方法:

 /**
     * 保存屏幕状态
     *
     */
    void rememberSyncState() {
        if (getChildCount() > 0) {
            mNeedSync = true;
            mSyncHeight = mLayoutHeight;
            if (mSelectedPosition >= 0) {
                //如果选择了内容,保存选择的位置和距离顶部的偏移量
                View v = getChildAt(mSelectedPosition - mFirstPosition);
                mSyncRowId = mNextSelectedRowId;
                mSyncPosition = mNextSelectedPosition;
                if (v != null) {
                    mSpecificTop = v.getTop();
                }
                mSyncMode = SYNC_SELECTED_POSITION;
            } else {
                // 如果没有选择内容就保存第一个 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;
            }
        }
    }

rememberSyncState 方法中针对是否选择了 item,保存了当前状态,重新绘制时会恢复状态。当我们滑动 ListView 后进行刷新数据操作,ListView 并没有滚动到顶部,就是因为这个方法的缘故。

回到 AdapterDataSetObserver.onChanged() 方法:

class AdapterDataSetObserver extends DataSetObserver {
        @Override
        public void onChanged() {
            //更新 数据修改状态
            mDataChanged = true;
            //更新 数据数量
            mOldItemCount = mItemCount;
            //更新 ItemView 数量
            mItemCount = getAdapter().getCount();

            // 监测是否有数据之前不可用、现在可用
            // 由于 BaseAdapter.hasStableIds() 默认返回 false ,所以我们直接看 else
            if (AdapterView.this.getAdapter().hasStableIds() && mInstanceState != null
                    && mOldItemCount == 0 && mItemCount > 0) {
                AdapterView.this.onRestoreInstanceState(mInstanceState);
                mInstanceState = null;
            } else {
               //记录当前状态,接下来刷新时要用到这些状态
                rememberSyncState();
            }
            checkFocus();
            requestLayout();
        }
        //...
}

保存数据状态后,进入 chekFocus 方法:

    void checkFocus() {
        final T adapter = getAdapter();
        final boolean empty = adapter == null || adapter.getCount() == 0;
        final boolean focusable = !empty || isInFilterMode();
        // The order in which we set focusable in touch mode/focusable may matter
        // for the client, see View.setFocusableInTouchMode() comments for more
        // details
        super.setFocusableInTouchMode(focusable && mDesiredFocusableInTouchModeState);
        super.setFocusable(focusable && mDesiredFocusableState);
        if (mEmptyView != null) {
            updateEmptyStatus((adapter == null) || adapter.isEmpty());
        }
    }

在这里设置 FocusFocusableInTouchMode 状态,关于 FocusableInTouchMode 不熟悉的可以 查看这篇文章

最后终于到了 View 的重新绘制 requestLayout, 这里将遍历 View 树重新绘制:

public void requestLayout() {
        if (mMeasureCache != null) mMeasureCache.clear();

        if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == null) {
            // Only trigger request-during-layout logic if this is the view requesting it,
            // not the views in its parent hierarchy
            ViewRootImpl viewRoot = getViewRootImpl();
            if (viewRoot != null && viewRoot.isInLayout()) {
                if (!viewRoot.requestLayoutDuringLayout(this)) {
                    return;
                }
            }
            mAttachInfo.mViewRequestingLayout = this;
        }

        mPrivateFlags |= PFLAG_FORCE_LAYOUT;
        mPrivateFlags |= PFLAG_INVALIDATED;

        if (mParent != null && !mParent.isLayoutRequested()) {
            mParent.requestLayout();
        }
        if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == this) {
            mAttachInfo.mViewRequestingLayout = null;
        }
    }

至此,我们了解了 ListView 中的观察者模式的大概流程,看得人快吐血了,一层调一层啊,还是画个 UML 图和流程图来回顾一下:

ListView 中的观察者模式

ListView 注册观察者 流程图 :

ListView 通知观察者更新 流程图 :

备注:

设计模式代码在这里

ListView 另外牛的一点就是可以加载各种各样的 Item View,这得益于当初设计的 Adapter,下篇文章我们来分析下 ListView 中的适配器模式。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏吴裕超

点击穿透原理及解决

一、事件触发顺序 PC网页上的大部分操作都是用鼠标的,即响应的是鼠标事件,包括mousedown、mouseup、mousemove和click事件。一次点击行...

3256
来自专栏跟着阿笨一起玩NET

winform程序中将控件置于最顶层或最底层的方法

一种方法是在WinForm窗体中使用Controls控件集的SetChildIndex方法,该方法将子控件设定为指定的索引值,其方法原型如下:

412
来自专栏葡萄城控件技术团队

用css3制作旋转加载动画的几种方法

以WebKit为核心的浏览器,例如Safari和Chrome,对html5有着很好的支持,在移动平台中这两个浏览器对应的就是IOS和Android。最近在开发一...

2086
来自专栏增长技术

Slide Bar相关

帮助快速查阅对应分组的侧边栏,可以配合任意列表,demo中给出配合RecyclerView(浮动分组使用stickyheadersrecyclerview)。

743
来自专栏web前端教室

JavaScript-事件委托(事件代理)

今天给自己的知识结构填个坑,再复习下JS的事件代理。 事件代理可以给JS批量生成的DOM元素添加事件,并且还可以提高效率,因为你确实不用给每个DOM节点添加事件...

17110
来自专栏Android开发经验

无意间遇到的TextView的一个坑

1184
来自专栏向前进

【整合】input标签JS改变Value事件处理方法

  某人需要在时间控件给文本框赋值时,触发事件函数。实现的效果:   1、文本框支持手工输入,通过用户输入修改值,手工输入结束后触发事件。阻塞在于失去焦点后才触...

3315
来自专栏前端之路

CSS布局-绝对尾部(Css Sticty Footer)

1194
来自专栏DannyHoo的专栏

iOS开发中系统的UITableViewCell只有当有数据的时候显示分割线

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u010105969/article/details/...

562
来自专栏前端杂货铺

position:sticky的兼容性尝试

开篇 笔者刚刚结束淘宝的工作,现在加入了一家有青春活力的垂直电商公司,正对着阿里巴巴的西溪园区,最近一直在熟悉新的工作环境和规范,因此博客有好些时间没有更新了,...

4219

扫码关注云+社区