觀察者模式解析以及在Android中的實際應用

stormWen發表於2017-12-25

概述

在生活中和實際專案中,經常涉及多個物件都對一個特殊物件中的資料變化感興趣,而且這多個物件都希望跟蹤那個特殊物件中的資料變化,在這樣的情況下就可以使用觀察者模式,比如天氣預報的,我相信很多使用者都有經常接到氣象局的天氣預報的推送,或者是雜誌的,如果你訂閱了,那麼便會定期收到郵件的推送等等。

觀察者模式是關於多個物件想知道一個物件中資料變化情況的一種成熟的模式。觀察者模式中有一個稱作“主題(被觀察)”的物件和若干個稱作“觀察者”的物件,“主題”和“觀察者”間是一種一對多的依賴關係,當“主題”的狀態發生變化時,所有“觀察者”都得到通知。前面所述的“氣象局的天氣預報中心”相當於觀察者模式的一個具體“主題”;每個收到簡訊的使用者相當於觀察者模式中的一個具體“觀察者”。

觀察者模式的結構

觀察者模式的結構中包含了四種角色:

● 主題(Subject):一般情況下主題是一個介面,該介面規定了具體主題需要實現的方法,一般都有新增,刪除和更新內容等基本方法,當然可以適當擴充,一般主題也叫做被觀察者。

● 觀察者(Observer):觀察者一般也是一個介面,主要方法為更新內容的介面方法。

● 具體主題(ConcreteSubject):具體主題是實現主題介面類的一個例項,該例項包含有可以經常發生變化的資料。具體主題需使用一個集合,比如ArrayList,存放觀察者的引用,以便資料變化時通知具體觀察者。

● 具體觀察者(ConcreteObserver):具體觀察者是實現觀察者介面類的一個例項。具體觀察者包含有可以存放具體主題引用的主題介面變數,以便具體觀察者讓具體主題將自己的引用新增到具體主題的集合中,使自己成為它的觀察者,或讓這個具體主題將自己從具體主題的集合中刪除,使自己不再是它的觀察者,所以一般觀察者的方法裡面都有一個刪除的介面。

下面是觀察者模式的類圖:

觀察者模式解析以及在Android中的實際應用

可以看到類的結構圖跟上面的介紹的角色是完全一致的。

觀察者模式的優點

● 具體主題和具體觀察者是鬆耦合關係。由於主題介面僅僅依賴於觀察者介面,因此具體主題只是知道它的觀察者是實現觀察者介面的某個類的例項,但不需要知道具體是哪個類。同樣,由於觀察者僅僅依賴於主題介面,因此具體觀察者只是知道它依賴的主題是實現主題介面的某個類的例項,但不需要知道具體是哪個類。

● 觀察者模式滿足設計模式中的“開-閉原則”。主題介面僅僅依賴於觀察者介面,這樣,就可以讓建立具體主題的類也僅僅是依賴於觀察者介面,因此,如果增加新的實現觀察者介面的類,不必修改建立具體主題的類的程式碼。同樣,建立具體觀察者的類僅僅依賴於主題介面,如果增加新的實現主題介面的類,也不必修改建立具體觀察者類的程式碼。

使用場景

● 當一個物件的資料更新時需要通知其他物件,但這個物件又不希望和被通知的那些物件形成緊耦合。

● 當一個物件的資料更新時,這個物件需要讓其他物件也各自更新自己的資料,但這個物件不知道具體有多少物件需要更新資料

說了一堆枯燥的理論,下面看一個簡單的例子:

首先定義一個主題和抽象觀察者,程式碼如下:

public interface Subject {
     void registerObserver(Observer o);
     void removeObserver(Observer o);
     void notifyObservers(float temprature);
}

//抽象的觀察者
public interface Observer {
    void update(float temprature);

    void remove(Subject mSubject);//不想再觀察的時候刪除
}
複製程式碼

然後定義實現主題的類和實現抽象觀察者的類:

public class ConcreteSubject implements Subject {
    private static final List<Observer> observers;
    static {
        observers = new ArrayList<>();
        observers.clear();
    }

    @Override
    public void registerObserver(Observer o) {
        observers.add(o);
    }

    @Override
    public void removeObserver(Observer o) {
        if (observers.indexOf(o) >= 0) {
            observers.remove(o);
        }
    }

    @Override
    public void notifyObservers(float temprature) {
        if (observers.size() <= 0) {
            System.out.println("沒有觀察者");
            return;
        }
        for (final Observer o : observers) {
            o.update(temprature);
        }
    }
}

這個實現了所有的主體介面,方便新增,刪除和通知所有的觀察者
複製程式碼

具體的觀察者如下:

public class ConcreteObserver implements Observer {

    @Override
    public void update(float temprature) {
        System.out.println("溫度為:" + temprature);
    }

    @Override
    public void remove(Subject mSubject) {
        mSubject.removeObserver(this);//在不需要觀察的時候刪掉即可
    }
}
複製程式碼

下面測試一下程式碼;

public class ObserverClient {
    public static void main(String[] args){
        final Subject sb = new ConcreteSubject();
        final Observer observer = new ConcreteObserver();
        sb.registerObserver(observer);
        sb.notifyObservers(21.0f);
        
        observer.remove(sb);
        sb.notifyObservers(23.0f);
    }
}
複製程式碼

測試程式碼很簡單,先註冊一個具體的觀察者,然後主題釋出一個溫度,接著具體的觀察者就收到通知了,而當具體的觀察者刪掉了主題之後,就再也收不到通知了,就好比你取消了雜誌的訂閱,便再也收不到雜誌了,一樣的理解就好,測試結果如下:

觀察者模式解析以及在Android中的實際應用

這只是一個主題和一個抽象觀察者,實際上不管多少個主題或者抽象觀察者,都是一樣的,基本原理都是"訂閱-釋出-更新"系統而已。

觀察者模式在Android原始碼中的應用

實際在Android系統的原始碼中,觀察者模式可以說廣泛的應用的,拿我們熟悉的Adapter中資料的更新機制來簡單分析一下,一般情況下,有資料改變的時候,大部分人喜歡一句話解決問題:

mAdapter.notifyDataSetChanged();
我們來看一下呼叫的過程:
public final void notifyDataSetChanged() {
            mObservable.notifyChanged();
        }
複製程式碼

這個mObservable是一個AdapterDataObservable物件,程式碼如下;

private final AdapterDataObservable mObservable = new AdapterDataObservable();
複製程式碼

可見又呼叫了mObservable的更新方法,程式碼如下:

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();
            }
        }
複製程式碼

這裡是重點了,取出所有的觀察者,然後再呼叫onChanged()方法, 有個問題,是什麼時候新增那些具體的觀察者的呢?是在setAdapter()方法裡面,程式碼如下:

public void setAdapter(Adapter adapter) {
        // bail out if layout is frozen
        setLayoutFrozen(false);
        setAdapterInternal(adapter, false, true);
        requestLayout();
    }
複製程式碼

接著又呼叫了

 private void setAdapterInternal(Adapter adapter, boolean compatibleWithPrevious,
            boolean removeAndRecycleViews) {
        if (mAdapter != null) {
        //先移除掉舊的觀察者
            mAdapter.unregisterAdapterDataObserver(mObserver);
            mAdapter.onDetachedFromRecyclerView(this);
        }
        if (!compatibleWithPrevious || removeAndRecycleViews) {
            removeAndRecycleViews();
        }
        mAdapterHelper.reset();
        final Adapter oldAdapter = mAdapter;
        mAdapter = adapter;
        if (adapter != null) {
        //這裡是重點,註冊
            adapter.registerAdapterDataObserver(mObserver);
            adapter.onAttachedToRecyclerView(this);
        }
        if (mLayout != null) {
            mLayout.onAdapterChanged(oldAdapter, mAdapter);
        }
        mRecycler.onAdapterChanged(oldAdapter, mAdapter, compatibleWithPrevious);
        mState.mStructureChanged = true;
        markKnownViewsInvalid();
    }
複製程式碼

我們可以看到註冊的時候呼叫的是mAdapter的registerAdapterDataObserver方法,程式碼如下:

public void registerAdapterDataObserver(AdapterDataObserver observer) {
            mObservable.registerObserver(observer);
        }
繼續跟蹤
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);
        }
    }
 可以看到這裡才是真正註冊的地方
複製程式碼

下面分析真正重新整理的地方;程式碼如下:

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();
            }
        }
//省略一些程式碼
複製程式碼

是取出依次更新呼叫了onChanged()方法,AdapterDataObserver是一個抽象類,那麼真正的實現類是RecyclerViewDataObserver,程式碼如下:

private class RecyclerViewDataObserver extends AdapterDataObserver {
        RecyclerViewDataObserver() {
        }

        @Override
        public void onChanged() {
            assertNotInLayoutOrScroll(null);
            mState.mStructureChanged = true;

            setDataSetChangedAfterLayout();
            if (!mAdapterHelper.hasPendingUpdates()) {
                requestLayout();//這裡是重點了,真正觸發重新整理
            }
        }
複製程式碼

可以看到終於來到了真正更新的地方了,就是這裡的requestLayout()方法,根據View的原理,requestLayout預設情況下會一層一層往父親類的requestLayout()呼叫,最終會呼叫頂層類的重新整理,來到最後就是大名鼎鼎的ViewRootImpl的performTraversals()的方法,從而重新整理介面,至此重新整理流程結束。

觀察者模式在Android實際中的應用

我們前面說了,觀察者模式在一對多,並且在一個物件的資料改變引起其他物件的資料同時改變的時候特別有用,在Android專案中,這種需求是很多的,比如廣播,廣播傳送出去以後,誰註冊了廣播,那麼便會接收到廣播的訊息,又比如EventBus事件匯流排,只要傳送出去訊息事件,不管在哪裡,誰註冊了訊息事件,也會同時收到事件,從而進行邏輯的處理,在實際的專案中,有時候也會碰到在不同的介面,需要同時改變UI的情況,比如在B介面處理了一個邏輯,然後需要在前面的A介面中,一些UI要求改變等等,假如有下面2個介面,分別是A介面和B介面,

觀察者模式解析以及在Android中的實際應用

觀察者模式解析以及在Android中的實際應用

好了,現在需求來了,要求點選TestActivity2中的改變全部文字按鈕,然後TestActivity2和TestActivity1中的TextView都要求改為預先設定好的文字,這個例子來源於實際例子中的,當然實際例子涉及到紅點或者其他UI的改變,不好抽離出來分析,所以就以這個簡單的例子為說一下,但原理不變,我們首先來分析一下怎麼實現比較好,當然EventBus傳送訊息可以解決問題,但傳送訊息之後需要一個個去手動設定新的文字,在這裡我們不用EventBus來傳送事件,我們利用觀察者模式來解決,我們知道了點選"更改全部文字"之後會觸發了一大堆的UI改變,首先我們定義了一個更新文字的方法,程式碼如下:

public interface Observer {
    void updateText(String text);//更新TextView的文字
    void remove(Observable observable);//刪除自己作為觀察者
}
複製程式碼

然後這些TextView為自定義的,並且實現了觀察者介面,程式碼如下

public class AutoUpdatetextView extends android.support.v7.widget.AppCompatTextView implements Observer {
    public AutoUpdatetextView(Context context) {
        super(context);
    }

    public AutoUpdatetextView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    public AutoUpdatetextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    //在更新文字的介面方法裡面實現了自動更新文字
    @Override
    public void updateText(String text) {
        if (!TextUtils.isEmpty(text)) {
            setText(text);
        }
    }

    @Override
    public void remove(Observable observable) {
        observable.remove(this);
    }
}
複製程式碼

好了,抽象觀察者和具體觀察者就寫好了,我們來寫抽象主題和具體的主題,程式碼如下:

抽象主題實現增加,刪除和更新功能
public interface Observable {
    void addObserver(Observer observer);
    void remove(Observer observer);
    void update(String text);
}

 //具體的主題
public class ObservableManager implements Observable {
    private static final List<Observer> observers;
    private static ObservableManager sInstance;

    private ObservableManager() {

    }

    public static ObservableManager getInstance() {
        if (sInstance == null) {
            synchronized (ObservableManager.class) {
                if (sInstance == null) {
                    sInstance = new ObservableManager();
                }
            }
        }
        return sInstance;
    }

    static {
        observers = new ArrayList<>();
        observers.clear();
    }

    @Override
    public void addObserver(Observer observer) {
        observers.add(observer);
    }

    @Override
    public void remove(Observer observer) {
        if (observers.indexOf(observer) > 0) {
            observers.remove(observer);
        }
    }

    @Override
    public void update(String text) {
        if (observers.size() <= 0) {
            Log.d("[app]", "沒有具體的觀察者");
            return;
        }
        for (Observer observer : observers) {
            observer.updateText(text);
        }

    }

    public void removeAll(List<Observer> mList) {
        if (mList == null || mList.size() == 0) {
            return;
        }
        Log.d("[app]", "移除的集合大小為:" + mList.size());
        for (Observer observer : mList) {
            remove(observer);
        }
        Log.d("[app]", "剩下的集合大小為:" + observers.size());
    }
}
複製程式碼

好了,抽象主題和具體的實現也寫好了,我們再來分析一下,要把那些TextView新增到具體的主題裡面,那麼應該怎麼新增,在 view這棵樹已經新增到window上之後進行新增,在新增的判斷是否是AutoUpdatetextView,如果是的話,就新增,如果不是,就不要新增了,為此,我們需要寫一個BaseActivity,在這裡,佈局就不貼了,程式碼如下:

public abstract class BaseActivity extends AppCompatActivity {

    protected List<Observer> mList = new ArrayList<>();

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mList.clear();
        setContentView(getResID());
        ViewGroup viewGroup = (ViewGroup) getWindow().getDecorView();
        addUpdateTextView(viewGroup);
    }

    protected abstract int getResID();
    //把Observer的子類新增進去
    private void addUpdateTextView(ViewGroup viewGroup) {
        for (int i = 0; i < viewGroup.getChildCount(); i++) {
            View childView = viewGroup.getChildAt(i);
            if (childView instanceof ViewGroup) {
                ViewGroup childGroup = (ViewGroup) childView;
                addUpdateTextView(childGroup);
            } else {
                if (childView instanceof Observer) {
                    //新增到觀察者集合裡面進去
                    Observer observer = (Observer) childView;
                    ObservableManager.getInstance().addObserver(observer);
                    mList.add(observer);
                }
            }
        }
    }
    //在onDestroy之後需要把這棵樹上的AutoUpdatetextView移除,避免持有Activity導致記憶體洩露
    @Override
    protected void onDestroy() {
        ObservableManager.getInstance().removeAll(mList);
        super.onDestroy();
    }
}
複製程式碼

下面我們看一下TestActivity1的程式碼,

public class TestActivity1 extends BaseActivity {
    private TextView text;

    @Override
    protected int getResID() {
        return R.layout.activity_test1;
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        text = (TextView) findViewById(R.id.text);
        text.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent = new Intent(TestActivity1.this, TestActivity2.class);
                startActivity(intent);
            }
        });
    }
}
就跳轉到TestActivity2而已
複製程式碼

TestActivity2的程式碼:

public class TestActivity2 extends BaseActivity {
    private TextView update;
    String[] texts = {"開開心心", "呵呵,新的文字", "你好啊", "Android開發呢", "ios開發呢", "WP開發", "PHP開發", "Python開發"};

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        update = (TextView) findViewById(R.id.update);
        update.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                int len = texts.length;
                int result = new Random().nextInt(len);
                Log.d("[app]", "result=" + texts[result]);
                //這裡是重點,通知其他註冊了的地方改變文字,這裡是文字,實際上可以做其他事情
                ObservableManager.getInstance().update(texts[result]);
            }
        });
    }

    @Override
    protected int getResID() {
        return R.layout.activity_test2;
    }

}
複製程式碼

我們可以看到:

 ObservableManager.getInstance().update(texts[result]);
複製程式碼

這句話是重點,這裡更新了文字,那麼意味著之前註冊的全部文字控制元件都將收到訊息,從而走AutoUpdatetextView的更新文字的介面方法

//在更新文字的介面方法裡面實現了自動更新文字
    @Override
    public void updateText(String text) {
        if (!TextUtils.isEmpty(text)) {
            setText(text);
        }
    }
複製程式碼

執行如下:

觀察者模式解析以及在Android中的實際應用

觀察者模式解析以及在Android中的實際應用

觀察者模式解析以及在Android中的實際應用

觀察者模式解析以及在Android中的實際應用

觀察者模式解析以及在Android中的實際應用

觀察者模式解析以及在Android中的實際應用

觀察者模式解析以及在Android中的實際應用

結果就是這樣,動態顯示文字,當然實際情況可以做更多的邏輯操作,限於篇幅,不再一一展示,理解原理更為重要,今天的文章就寫到這裡,感謝大家閱讀。

相關文章