場景描述
在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吧