Java多线程下载框架02:观察者模式通知下载内容状态更新

场景描述

在Java多线程下载框架中,我们需要知道下载状态比如暂停下载,恢复下载,取消下载等状态的通知,而且不仅仅是更新当前页面,在任意页面都能接收到状态变化的更新,所以这里要用到观察者模式。


观察者模式

关于设计模式的详细介绍,我这里有几本电子书籍推荐,公号后台回复"设计模式",即可获取下载链接。

那么什么是观察者模式(Observer)?

观察者模式定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。这个主题对象在状态上发生变化时,会通知所有观察者对象,让他们能够自动更新自己。

举一个例子来说明,牛奶送奶站就是主题,订奶客户为监听者,客户从送奶站订阅牛奶后,会每天收到牛奶。如果客户不想订阅了,可以取消,以后就不会收到牛奶。

为什么要使用观察者模式?为什么不用广播,EventBus,RxBus呢?

广播的劣势

广播是相对消耗时间、空间最多的一种方式,但是大家都知道,广播是四大组件之一,许多系统级的事件都是通过广播来通知的,比如说网络的变化、电量的变化,短信发送和接收的状态,所以,如果与android系统进行相关的通知,还是要选择本地广播;在BroadcastReceiver的 onReceive方法中,可以获得Context 、intent参数,这两个参数可以调用许多的sdk中的方法。

应用发送某个广播时,系统会将广播中的intent与系统中所有注册的BroadcastReceiver进行匹配,如果能匹配成功则调用相关的onReceive函数进行处理。这里存在2个问题:
a、性能问题。每个广播都会与所有BroadcastReceiver进行匹配。
b、安全问题。广播发出去的数据可能被其他应用监听。

因此广播相对于其他的方式而言,广播是重量级的,消耗资源较多的方式。他的优势体现在与sdk连接紧密,如果需要同 android 交互的时候,广播的便捷性会抵消掉它过多的资源消耗,但是如果不同android交互,或者说,只做很少的交互,使用广播是一种浪费。

为什么不使用EventBus,RxBus?

这里不对二者的优缺点进行分析,各有各的好处,看实际需要。因为我们是封装自己的多线程下载框架,所以不能依赖第三方的一些库,因为你不知道用户会使用RxJava还是EventBus。比如你这里用到了RxJava的库,而别人使用你的SDK的之前就集成了EventBus,那不是又要集成RxJava?或者说你这里使用的是Rx1.0,而用户使用的是Rx2.0,所以为了避免不必要的麻烦,我们尽量不被依赖外部资源。

为什么使用观察者模式?
  • 松耦合,观察者增加或删除无需修改主题的代码,只需调用主题对应的增加或者删除的方法即可。
  • 主题只负责通知观察者,但无需了解观察者如何处理通知。举个例子,送奶站只负责送递牛奶,不关心客户是喝掉还是洗脸。
  • 观察者只需等待主题通知,无需观察主题相关的细节。还是那个例子,客户只需关心送奶站送到牛奶,不关心牛奶由哪个快递人员,使用何种交通工具送达。
具体实践

一、创建一个Observable

public class DataChanger extends Observable{

    /**
     * 对外提供一个单列引用,用于注册和取消注册监听
     */
    private static DataChanger mDataChanger;

    public static synchronized DataChanger getInstance(){
        if (null == mDataChanger){
            mDataChanger = new DataChanger();
        }
        return mDataChanger;
    }

    public void notifyDataChange(DownloadEnty mDownloadEnty){
        //Marks this <tt>Observable</tt> object as having been changed
        setChanged();
        //通知观察者 改变的内容 也可不传递具体内容 notifyObservers()
        notifyObservers(mDownloadEnty);
    }
}

主要用于提供注册和删除观察者对象以及通知更新的方法,此处直接继承的是Java提供的Observable,其内部已经实现了

 * addObserver
 * deleteObserver
 * notifyObservers()
 * notifyObservers(Object arg)
 * deleteObservers()
 * setChanged()
 * clearChanged()
 * hasChanged()
 * countObservers()

当内容变化的时候,使用setChanged()和notifyObservers(mDownloadEnty)通知观察者。

二、setChanged和notifyObservers为何物

上述代码中存在这样一处代码setChanged();,如果在通知之前没有调用这个方法,观察者是收不到通知的,这是为什么呢?

这里我们看一下setChanged的源码

 /**
     * Marks this <tt>Observable</tt> object as having been changed; the
     * <tt>hasChanged</tt> method will now return <tt>true</tt>.
     */
    protected synchronized void setChanged() {
        changed = true;
    }

此处把boolen变量changed改为了true

再看notifyObservers源码

 /**
     * If this object has changed, as indicated by the
     * <code>hasChanged</code> method, then notify all of its observers
     * and then call the <code>clearChanged</code> method to indicate
     * that this object has no longer changed.
     * <p>
     * Each observer has its <code>update</code> method called with two
     * arguments: this observable object and the <code>arg</code> argument.
     *
     * @param   arg   any object.
     * @see     java.util.Observable#clearChanged()
     * @see     java.util.Observable#hasChanged()
     * @see     java.util.Observer#update(java.util.Observable, java.lang.Object)
     */
    public void notifyObservers(Object arg) {
        /*
         * a temporary array buffer, used as a snapshot of the state of
         * current Observers.
         */
        Observer[] arrLocal;

        synchronized (this) {
            /* We don't want the Observer doing callbacks into
             * arbitrary Observables while holding its own Monitor.
             * The code where we extract each Observable from
             * the ArrayList and store the state of the Observer
             * needs synchronization, but notifying observers
             * does not (should not).  The worst result of any
             * potential race-condition here is that:
             *
             * 1) a newly-added Observer will miss a
             *   notification in progress
             * 2) a recently unregistered Observer will be
             *   wrongly notified when it doesn't care
             */
            if (!hasChanged())
                return;

            arrLocal = observers.toArray(new Observer[observers.size()]);
            clearChanged();
        }

        for (int i = arrLocal.length-1; i>=0; i--)
            arrLocal[i].update(this, arg);
    }

可以看到

  if (!hasChanged())
       return;

所以这就是为什么通知更新前一定要调用setChanged的原因

但是为什么要加入这样一个开关呢?可能原因大致有三点

1.筛选有效通知,只有有效通知可以调用setChanged。比如,我的微信朋友圈一条状态,好友A点赞,后续该状态的点赞和评论并不是每条都通知A,只有A的好友触发的操作才会通知A。

2.便于撤销通知操作,在主题中,我们可以设置很多次setChanged,但是在最后由于某种原因需要取消通知,我们可以使用clearChanged轻松解决问题。

3.主动权控制,由于setChanged为protected,而notifyObservers方法为public,这就导致存在外部随意调用notifyObservers的可能,但是外部无法调用setChanged,因此真正的控制权应该在主题这里。

三、创建Observer

/**
 * Created by chenshouyin on 2017/10/25.
 * 我的博客:http://blog.csdn.net/e_inch_photo
 * 我的Github:https://github.com/chenshouyin
 */

public abstract class DataWhatcher implements Observer {
    @Override
    public void update(Observable observable, Object data) {
        if (data instanceof DownloadEnty){
            notifyDataChange(data);
        }
    }

    public abstract void notifyDataChange(Object data);

}

为那些在目标发生改变时需获得通知的类定义个更新的接口,这里对接口再进行了判断,对外提供了notifyDataChange抽象方法,外部可在此抽象方法在获取到更新的回调以及更新的对象。

四、添加和取消观察者

private DataWhatcher dataWhatcher = new DataWhatcher() {

        @Override
        public void notifyDataChange(Object data) {
            downloadEnty = (DownloadEnty) data;
            if (downloadEnty.downloadStatus == DownloadEnty.DownloadStatus.downloading){
                LogUtil.e("download","===notifyDataChange===downloading"+downloadEnty.currentLenth);
            }else if (downloadEnty.downloadStatus == DownloadEnty.DownloadStatus.downloadcomplete){
                LogUtil.e("download","===notifyDataChange===downloadcomplete");
            }else if (downloadEnty.downloadStatus == DownloadEnty.DownloadStatus.downloadcansel){
                downloadEnty = null;
                LogUtil.e("download","===notifyDataChange===downloadcansel");
            }else if (downloadEnty.downloadStatus == DownloadEnty.DownloadStatus.downloadpause){
                LogUtil.e("download","===notifyDataChange===downloadpause");
            }else{
                LogUtil.e("download","===notifyDataChange===下载进度"+downloadEnty.currentLenth);
            }

        }
    };
  @Override
    protected void onResume() {
        super.onResume();
        DownloadManager.getInstance().addObserve(dataWhatcher);
    }

    @Override
    protected void onStop() {
        super.onStop();
        DownloadManager.getInstance().removeObserve(dataWhatcher);
    }

五、运行效果

运行效果

六、观察者模式使用总结

从上面可以看出,实际上观察者和被观察者是通过接口回调来通知更新的,首先创建一个观察者(数据监听)实例并实现数据变化接口,通过注册监听将实例传入被观察者(数据变化),当被观察者数据变化的时候使用该实例的接口回传状态。了解原理之后,我们可以利用观察者模式自定义实现。

拿微信公众号来举例,假设微信用户就是观察者,微信公众号是被观察者,有多个的微信用户关注了 陈守印同学 这个公众号,当这个公众号更新时就会通知这些订阅的微信用户。我们来看看用代码如何实现:

1.抽象观察者(Observer)

public interface Observer {
    public void update(String message);
}

2.具体观察者(ConcrereObserver)

微信用户是观察者,里面实现了更新的方法:

里面定义了一个更新的方法:

public class WeixinUser implements Observer {
    // 微信用户名字
    private String name;
    public WeixinUser(String name) {
        this.name = name;
    }
    @Override
    public void update(String message) {
        System.out.println(name + ":" + message);
    }
}

3.抽象被观察者(Subject)

抽象主题,提供了attach、detach、notify三个方法:

public interface Subject {
    /**
     * 增加订阅者
     * @param observer
     */
    public void attach(Observer observer);
    /**
     * 删除订阅者
     * @param observer
     */
    public void detach(Observer observer);
    /**
     * 通知订阅者更新消息
     */
    public void notify(String message);
}

4.具体被观察者(ConcreteSubject)

微信公众号是具体主题(具体被观察者),里面存储了订阅该公众号的微信用户,并实现了抽象主题中的方法:

public class SubscriptionSubject implements Subject {
    //储存订阅公众号的微信用户
    private List<Observer> weixinUserlist = new ArrayList<Observer>();

    @Override
    public void attach(Observer observer) {
        weixinUserlist.add(observer);
    }

    @Override
    public void detach(Observer observer) {
        weixinUserlist.remove(observer);
    }

    @Override
    public void notify(String message) {
        for (Observer observer : weixinUserlist) {
            observer.update(message);
        }
    }
}

5.客户端调用

public class Client {
   public static void main(String[] args) {
       SubscriptionSubject mSubscriptionSubject=new SubscriptionSubject();
       //创建微信用户
       WeixinUser user1=new WeixinUser("陈守印同学公众号粉丝A");
       WeixinUser user2=new WeixinUser("陈守印同学公众号粉丝B");
       WeixinUser user3=new WeixinUser("陈守印同学公众号粉丝C");
       WeixinUser user4=new WeixinUser("陈守印同学公众号粉丝D");
       //订阅公众号
       mSubscriptionSubject.attach(user1);
       mSubscriptionSubject.attach(user2);
       mSubscriptionSubject.attach(user3);
       mSubscriptionSubject.attach(user4);
       //公众号更新发出消息给订阅的微信用户
       mSubscriptionSubject.notify("陈守印同学公众号的文章更新啦");
   }
}

6.运行结果

陈守印同学公众号粉丝A:陈守印同学公众号的文章更新啦
陈守印同学公众号粉丝B:陈守印同学公众号的文章更新啦
陈守印同学公众号粉丝C:陈守印同学公众号的文章更新啦
陈守印同学公众号粉丝D:陈守印同学公众号的文章更新啦

6.观察者模式优缺点

  • 解除耦合,让耦合的双方都依赖于抽象,从而使得各自的变换都不会影响另一边的变换。
  • 如果一个被观察者对象有很多的直接和间接的观察者的话,将所有的观察者都通知到会花费很多时间。

公号后台回复"设计模式",获取设计模式书籍

设计模式

后续文章持续更新中,微信扫码下方二维码免费关注!上一篇:Java多线程下载01:多线程的好处以及断点续传原理
点此查看全部最新文章


我的博客
我的简书
我的GitHub,喜欢的话给个star吧

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

推荐阅读更多精彩内容

  • 1 场景问题# 1.1 订阅报纸的过程## 来考虑实际生活中订阅报纸的过程,这里简单总结了一下,订阅报纸的基本流程...
    七寸知架构阅读 4,566评论 5 57
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,580评论 18 139
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,378评论 25 707
  • 本文的结构如下: 什么是观察者模式 为什么要用该模式 模式的结构 代码示例 推模型和拉模型 优点和缺点 适用环境 ...
    w1992wishes阅读 1,412评论 0 16
  • 今年之前我跟我的客户沟通方式有下面的三种1,不清楚的电话,这个主要是临时的沟通,或者重要的事情;2,订单通过短信S...
    木老马阅读 937评论 0 1