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吧

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 1 場景問題# 1.1 訂閱報紙的過程## 來考慮實際生活中訂閱報紙的過程,這里簡單總結了一下,訂閱報紙的基本流程...
    七寸知架構閱讀 4,688評論 5 57
  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,991評論 19 139
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 173,524評論 25 708
  • 本文的結構如下: 什么是觀察者模式 為什么要用該模式 模式的結構 代碼示例 推模型和拉模型 優點和缺點 適用環境 ...
    w1992wishes閱讀 1,464評論 0 16
  • 今年之前我跟我的客戶溝通方式有下面的三種1,不清楚的電話,這個主要是臨時的溝通,或者重要的事情;2,訂單通過短信S...
    木老馬閱讀 970評論 0 1