从RecyclerView开始,研究观察者模式

Android开发中一个很常见的场景是有一个列表,列表上每条Item有个点赞的按钮,当你给这条Item点赞的时候,这个按钮就会变成已点赞状态,相信大家可以脑补到这个场景。这里就用到了观察者模式。

那么什么是观察者模式,我们稍微瞧一下这个模式的定义,不用太在意,看不懂也没关系,我觉得设计模式是需要用代码感受的,加这些定义只是为了让懂的人感受更深,更系统。
观察者模式:

观察者模式

两个抽象类或者接口,
一个是被观察者的抽象类或者接口,需要持有所有观察者的引用,
被观察者要能做三件事儿:
1.可以注册观察者。
2.有注册就有注销。
3.可以通知观察者的方法。

一个是观察者的抽象类或者接口,用来更新自己。
观察者要能做一件事儿,那就是更新自己。

具体举个例子:
虽然我们主要是写android的,但是服务器的东西还是要懂点,就拿简书这个类似于博客的网站来说吧,如果你订阅了或者说关注了一个领域,就能收到这个领域文章的推送,如果没有关注,则不能。是相当于有一个总控制台(被观察者,持有数据源,这里的数据源是我们每个订阅了的人)通知下面的观察者。

在java中可以这么写。(因为java的观察者模式是内置的,所以你只需要实现Observer接口和继承Observable)

观察者的代码:

/**
 * 程序员,也就是你,订阅这个专题的人是。观察者
 */

public class Coder implements Observer {
    private String yourName;
    public Coder(String yourName){
        this.yourName=yourName;
    }

    @Override
    public void update(Observable o, Object arg) {
        System.out.println("你订阅的"+arg.toString()+"更新了。");
    }

    @Override
    public String toString() {
        return "your name "+yourName;
    }
}

以下是被观察者和服务器的代码。

/**
 * 你订阅的简书android领域
 */
public class JianShuAndroid extends Observable{
    public void postNewContentToCoder(String content){
        setChanged();
        notifyObservers(content);
    }
}

/**
 * 服务器的代码
 */
public class Server{   
    public static void main(String[] args){
        JianShuAndroid jianShuAndroid=new JianShuAndroid();
        Coder coder1=new Coder("name1");
        Coder coder2=new Coder("name2");
        Coder coder3=new Coder("name3");
        jianShuAndroid.addObserver(coder1);
        jianShuAndroid.addObserver(coder2);
        jianShuAndroid.addObserver(coder3);
        jianShuAndroid.postNewContentToCoder("contentChanged");
    }
}

(这里顺便提一下,为什么观察者是实现一个observer接口,而被观察者是继承一个抽象类呢?我觉得,被观察者写成抽象类的原因是复用,观察者写成接口的原因是降低代码的耦合度,面向接口编程,在原则里就是依赖倒置原则,我们倒着思考,如果这里不是接口,而是一个具体的类,那么,耦合度就相当高了,如果不是Coder注册就无法添加到observable里,就要修改observable的代码。)

接下来验证我们看看android的套路是怎么玩的。
文章开头提到的RecyclerView,整个列表是一个被观察者也就是这个RecyclerView的Adapter,为什么要叫被观察者呢?因为Adapter持有了所有数据源,而每条Item要做的是观察这些数据源有没有变化,所以就叫被观察者,每个Item就叫观察者。当数据变化的时候,adapter就通知这条Item,数据源发生了变化了(这个通知的过程是notifyDataSetChanged来进行的),我们可以猜想,RecyclerView的内部的模式是观察者模式。

玩观察者模式,假如你用的是java,那么observable,observer是内置的,你的被观察者继承observable,观察者实现observer接口,就行了。其他的语言,如果你搞清了原理,自己写一个也很简单。

上面我们说到了RecyclerView,开始分析(看源码,一万多行的RecyclerView不要急,找我们想要的。我感觉看源码就是像给了你一团毛线让你理顺,当然你要去找那个线头,千万不要陷入泥潭,每一行都看。)

大家找一下这个突破口,也就是线头,我们明显现在已经知道了recyclerview里肯定用到了观察者模式,还有一个重要线索是,我们知道它使怎么通知观察者改变的,就是notifyDataSetChanged。那就从这个开始看起。
找了半天,知道了notifyDataSetChanged是在Adapter里面,而这个adapter开始的第一句就是

public static abstract class Adapter<VH extends ViewHolder> {
        private final AdapterDataObservable mObservable = new AdapterDataObservable();
        private boolean mHasStableIds = false;
  ....
  ....
  ....
}

很熟悉的字眼,observable(被观察者)来了。看一下这个AdapterDataObservable,不要怕,名字是长了点,但是一般名字长的都是纸老虎。

static class AdapterDataObservable extends Observable<AdapterDataObserver> {
        public boolean hasObservers() {
            return !mObservers.isEmpty();
        }

        public void notifyChanged() {
            // since onChanged() is implemented by the app, it could do anything, including
            // removing itself from {@link mObservers} - and that could cause problems if
            // an iterator is used on the ArrayList {@link mObservers}.
            // to avoid such problems, just march thru the list in the reverse order.
            for (int i = mObservers.size() - 1; i >= 0; i--) {
                mObservers.get(i).onChanged();
            }
        }

        public void notifyItemRangeChanged(int positionStart, int itemCount) {
            notifyItemRangeChanged(positionStart, itemCount, null);
        }
  ...
  ...
  ...

一看,没错这家伙肯定是被观察者,因为它继承了Observable。我们那个简书网站就是这样啊,JianShuAndroid继承了observable。
还记得我们前面说被观察者一般要可以做三件事儿。注册,注销和通知。那么这里也不会例外。我们看到的是通知这件事儿,但是注册和注销是java内部的Observable帮我们实现了。

public abstract class Observable<T> {
    /**
     * The list of observers.  An observer can be in the list at most
     * once and will never be null.
     */
    protected final ArrayList<T> mObservers = new ArrayList<T>();

    /**
     * Adds an observer to the list. The observer cannot be null and it must not already
     * be registered.
     * @param observer the observer to register
     * @throws IllegalArgumentException the observer is null
     * @throws IllegalStateException the observer is already registered
     */
    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);
        }
    }

    /**
     * Removes a previously registered observer. The observer must not be null and it
     * must already have been registered.
     * @param observer the observer to unregister
     * @throws IllegalArgumentException the observer is null
     * @throws IllegalStateException the observer is not yet registered
     */
    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);
        }
    }
    ....
    ....
    ....

这时候observable出现了,那observer呢?
其实上面就已经出现了。回上一段代码里找,AdapterDataObserver的泛型里是AdapterDataObserver,然后我们全局搜一下就可以发现

private class RecyclerViewDataObserver extends AdapterDataObserver {
        RecyclerViewDataObserver() {
        }

        @Override
        public void onChanged() {
            assertNotInLayoutOrScroll(null);
            if (mAdapter.hasStableIds()) {
                // TODO Determine what actually changed.
                // This is more important to implement now since this callback will disable all
                // animations because we cannot rely on positions.
                mState.mStructureChanged = true;
                setDataSetChangedAfterLayout();
            } else {
                mState.mStructureChanged = true;
                setDataSetChangedAfterLayout();
            }
            if (!mAdapterHelper.hasPendingUpdates()) {
                requestLayout();
            }
        }
      ...
      ...
      ...

这就是observer了,这个是干嘛的,根据我们上面说的,有一件事儿,那就是更新数据的,onChanged里面涉及到requestLayout()的,在requestLayout里会重新调用onDraw来重绘。

最后整理一遍这个过程,
在Adapter里面有一个AdapterDataObservable,是被观察者,被观察者必须有三个方法,注册,销毁,通知,这里的注册就是registerAdapterDataObserver,通知就是notify相关的。
在setAdapter的时候,将观察者,也就是RecyclerViewDataObserver注册到AdapterDataObservable里面来维护,观察者里面自然是更新布局。
我们调用notifyDataSetChanged其实就是调用被观察者的notify相关方法

关于观察者模式,有兴趣的话可以看看EventBus和AndroidEventBus的源码,这两个开源库的实现也是运用了观察者模式。

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

推荐阅读更多精彩内容