Memo 備忘錄模式 | 實現與解說 | Android Framwrok Activity 保存

Memo 備忘錄模式 | 實現與解說 | Android Framwrok Activity 保存

Overview of Content

備忘錄(Memo)設計是一種行為模式,被儲存的物件不會被外部讀取,在不破壞封裝的前提下,獲取、儲存一個物件的內部狀態

本文探討 Memo 備忘錄模式的應用和實現,以及在 Android Framework 中的相關設計。首先,我們會深入了解 Memo 模式的使用場景,並解釋其定義和 UML 示意圖,進一步討論其優缺點。接著,我們會探討 Memo 模式的具體實現方法,以及相關的標準和最佳實踐。此外,我們還會介紹 Memo 模式的一個變形,即使用 Clone 取代 Memo

最後,我們將重點關注於 Android Framework 中的相關部分,包括 Activity 中的 onSaveInstanceStateonRestoreInstanceState 方法,以及它們在資料暫存和恢復方面的應用。


Memo 使用場景

● 需要儲存一個物件在某一個時刻的狀態 (並非執行步驟),並提供使用者 rollback 操作

● 當一個物件不希望直接被讀取到其狀態 (private),就可 透過「中間物件」存取 (這個中間物件就是 Memo)

Memo 定義 & Memo UML

● Memo 定義:在不破獲封裝的前提下,保存一個物件的狀態,好讓之後可以恢復該對象的狀態

● Memo UML 角色關係

Memo 類角色說明
Originator(發起人)讓使用者操控 (可讓使用者直接接觸),同時包含 Memo 功能
Memo(備忘錄)跟 Originator 有相對應的 member,讓 使用者無法直接接觸 Memo 類
Caretaker(管理備忘錄)專門用來記錄、管理 Memo 類

Memo 有很清楚得分出類之間的職責,符合 單一職責迪米特(最少知識)原則

單一職責Originator 負責操作備忘錄、Memo 負責紀錄數據、Caretaker 管理紀錄

迪米特:User 不會直接創建 Memo 類,使用者必須透過 Originator 類來取得 Memo 類

Memo 優缺點

● Memo 設計優點 :

● 提供使用者快速方便的恢復機制,方便找回歷史狀態或是 rollback 資料,而這些操作都是安全性的操作

● 從上面可以看出 使用者不必關心細節 (gettersetter)

● Memo 設計缺點 :

● 消耗資源,類的增加 (這個問題基本上是設計模式的通病),每次儲存都會消耗資源空間

Memo 實現

Memo 標準

A. OriginatorGameProvider 讓使用者直接操控的類 (對外暴露),同時包括 Memo 類的創建、操作,提供給使用者使用

// Originator

public class GameProvider {
    private int hp = 100;
    private int mp = 100;

    public Memo createMemo() {
        Memo memo = new Memo();
        memo.hp = hp;
        memo.mp = mp;

        return memo;
    }

    public void restore(Memo memo) {
        if(memo == null) {
            return;
        }

        hp = memo.hp;
        mp = memo.mp;
    }

    public GameProvider attack() {
        mp -= 20;
        return this;
    }

    public GameProvider defense() {
        hp -= 5;
        return this;
    }

    public void finishGame() {
        System.out.println(this);
    }

    @Override
    public String toString() {
        return "Hp: " + hp + ", mp: " + mp;
    }
}

B. Memo:專注於儲存 Originator 需要的 Member,該類不會讓使用者直接使用

public class Memo {
    int hp = 100;
    int mp = 100;
}

Originator 是跟 Memo 類有相同的 member,所以它必須擁有該類的所有屬性 (可用 data class),在這部分可以用另外一個抽象優化

可以選擇使用 Memo 變形、反射、抽象… 等等

C. Caretaker:身為 Memo 管理員,這裏使用 Map 儲存 Memo,專職在 處理存取,該類會讓使用者使用


public class Caretaker {

    private final Map<String, Memo> memoList = new HashMap<>();


    public void setMemo(String name, Memo memo) {
        if(memo == null) {
            return;
        }

        memoList.put(name, memo);
    }

    public Memo getMemo(String name) {
        if(name == null || name.isEmpty()) {
            return null;
        }

        return memoList.get(name);
    }
}

這裡使用 Map 來儲存 Memo 物件,在實際工作上,最好是添加一個上限或是 LRU 樹來替換舊的備份,否則可能導致 OOM !

● User 使用:User 必須使用 Originator 發起備份、Caretaker 管理(儲存、取得)備份

public class MemoMain {

    public static void main(String[] args) {

        // 備忘錄管理員
        Caretaker caretaker = new Caretaker();

        // 發起者
        GameProvider game = new GameProvider();
        game.attack();
        game.attack();
        game.defense();

        final String player = "Alien";
        // 儲存備忘錄
        caretaker.setMemo(player, game.createMemo());    // 創建備忘錄
        game.finishGame();

        GameProvider game2 = new GameProvider();
        // 使用 memo 紀錄者,讀取紀錄訊息
        game2.restore(caretaker.getMemo(player));
        game2.finishGame();
    }

}

Memo 變形:Clone 取代 Memo

● 這裡我們可以使用 java 的特性,讓需要紀錄的對象實做 Cloneable 界面,讓其轉為可 Clone 的對象;優點是可以更簡單的使用、加快性能d

不符合單一職責? 原本的對象需要覆蓋拷貝的責任了!

的確如此,但這裡我們可以想做,把拷貝責任封裝到 Cloneable 界面,其實這就算是一個簡易的封裝

這種變形僅限於 簡單場合

雖然 clone 可以加快性能,但是我們還是要注意 深、淺拷貝問題,避免增加了邏輯複雜度,再次簡化後 UML 如下

A. Originator:概括了保存、數據複製的工作

class GameProvider : Cloneable {
    private var hp : Int = 100
    private var mp : Int = 100

    // 取代 Caretaker 功能
    private var backup : GameProvider? = null

    fun createMemo() {
        // 取代 Memo 功能
        backup = this.clone() as GameProvider
    }

    fun restore() {
        backup?.let {
            hp = it.hp
            mp = it.mp
        }
    }

    fun attack() {
        mp -=20
    }

    fun defense() {
        hp -=5
    }

    fun finishGame() {
        println(this)
    }

    override fun toString(): String {
        return "Hp: $hp, mp: $mp"
    }

}

User 使用變形 Memo 設計:用起來也相當簡化,基本上就算是 clone 模式的變化板而已

fun main() {
    // 發起者
    val game = GameProvider().apply {
        attack()

        createMemo() // 創建備忘錄

        attack()

        defense()

        finishGame()
    }

    game.apply {
        // 恢復
        restore()

        finishGame()
    }
}

Android Framework Activity 保存

Activity 被系統回收前會透過,特殊方法來儲存、恢復資料

功能Activity 處理的方法
暫存onSaveInstanceState
恢復onRestoreInstanceState

Memo 設計的概念分類如下

Memo 設計腳色Android 實做說明
MemoBundle設定要緩存、攜帶的資料
OriginatorView、ViewGroup一般的 View 操作,不過 多了 Bundle 存取控制
CaretakerActivity、Fragment儲存、恢復 Bundle 數據

Activity onSaveInstanceState 暫存資料

Caretaker 角色:從 Activity#onSaveInstanceState 開始看,可以看到這裡 取得 Bundle 對象 (也就是 Memo),呼叫 PhoneWindow 並傳入 Bundle 物件

// Activity.java

    private Window mWindow;    // 實作類是 PhoneWindow

    private static final String WINDOW_HIERARCHY_TAG = "android:viewHierarchyState";

    protected void onSaveInstanceState(@NonNull Bundle outState) {
        
        // @ 追蹤 saveHierarchyState 方法
        outState.putBundle(WINDOW_HIERARCHY_TAG, mWindow.saveHierarchyState());

        outState.putInt(LAST_AUTOFILL_ID, mLastAutofillId);
        
        // Fragment 狀態保存
        Parcelable p = mFragments.saveAllState();
        if (p != null) {
            outState.putParcelable(FRAGMENTS_TAG, p);
        }
        
        if (mAutoFillResetNeeded) {
            outState.putBoolean(AUTOFILL_RESET_NEEDED, true);
            getAutofillManager().onSaveInstanceState(outState);
        }
        // 分發給所有 Activity 生命週期監聽的 onActivitySaveInstanceState
        dispatchActivitySaveInstanceState(outState);
    }

    private void dispatchActivitySaveInstanceState(@NonNull Bundle outState) {
        // Activity 生命週期所有的 callback
        Object[] callbacks = collectActivityLifecycleCallbacks();
        
        if (callbacks != null) {
            for (int i = callbacks.length - 1; i >= 0; i--) {
                ((Application.ActivityLifecycleCallbacks) callbacks[i])
                        .onActivitySaveInstanceState(this, outState);
            }
        }
        getApplication().dispatchActivitySaveInstanceState(this, outState);
    }

Originator 角色:PhoneWindow#saveHierarchyState方法:目的是 恢復 View 的狀態,創建 SparseArray<Parcelable> 並以 id 作為 key、Parcelable 作為 value (儲存 View 的訊息)

SparseArray<T> 簡單來說:是一個以 Integer 為 Key 的 Map 資料格式,也就是 Map<Integer, T>,不過它的效率較高

// PhoneWindow.java

    ViewGroup mContentParent;

    @Override
    public Bundle saveHierarchyState() {
        // 創建 Bundle 準備傳遞數據
        Bundle outState = new Bundle();
        if (mContentParent == null) {
            // 佈局尚未加載
            return outState;
        }

        SparseArray<Parcelable> states = new SparseArray<Parcelable>();
        // @ 追蹤 saveHierarchyState 方法
        mContentParent.saveHierarchyState(states);
        
        ... 省略部分

        return outState;
    }

saveHierarchyState 方法:View 遞迴呼叫,讓每個 View、ViewGroup 自己處理 (ViewGroup 會在 Override 這個函數)

// View.java

    public void saveHierarchyState(SparseArray<Parcelable> container) {
        // @ 追蹤 dispatchSaveInstanceState 方法
        dispatchSaveInstanceState(container);
    }

A. View#dispatchSaveInstanceState 方法:

從這邊可以看出 xml 上 沒有設定 android:id 屬性的 View 是不會回復狀態 !

由於儲存 View 的空間是使用 SparseArray 結構,所以 同一個 View Tree 上不能有相同的 ID 的 View,否則就只會有一個更新

// View.java

protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) {
    if (mID != NO_ID && (mViewFlags & SAVE_DISABLED_MASK) == 0) {
        // 清除 PFLAG_SAVE_STATE_CALLED flag
        mPrivateFlags &= ~PFLAG_SAVE_STATE_CALLED;

        // 呼叫 onSaveInstanceState
        Parcelable state = onSaveInstanceState();

        // 判斷 PFLAG_SAVE_STATE_CALLED flag
        if ((mPrivateFlags & PFLAG_SAVE_STATE_CALLED) == 0) {
            throw new IllegalStateException(
                    "Derived class did not call super.onSaveInstanceState()");
        }
        if (state != null) {
            // Log.i("View", "Freezing #" + Integer.toHexString(mID)
            // + ": " + state);
            container.put(mID, state);
        }
    }
}

@CallSuper
@Nullable 
protected Parcelable onSaveInstanceState() {
    mPrivateFlags |= PFLAG_SAVE_STATE_CALLED;

    ... 省略部分
}

B. ViewGroup#dispatchRestoreInstanceState 方法:迭代該 ViewGroup 中所有的 View,並遞迴呼叫所有 View#dispatchRestoreInstanceState 方法來回復 View 的狀態

// ViewGroup.java

@Override
protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) {
    // ViewGroup 如果有 id 也會存取
    super.dispatchSaveInstanceState(container);

    // 該 ViewGroup  ChildView 的數量
    final int count = mChildrenCount;
    final View[] children = mChildren;
    for (int i = 0; i < count; i++) {
        View c = children[i];
        if ((c.mViewFlags & PARENT_SAVE_DISABLED_MASK) != PARENT_SAVE_DISABLED) {
        // 只要沒有禁止儲存,就會用遞迴不斷呼叫 dispatchRestoreInstanceState 方法
            c.dispatchSaveInstanceState(container);
        }
    }
}

Activity onRestoreInstanceState 恢復資料

其實流程跟 onSaveInstanceState 差不多,只是做了反向操作

Caretaker 角色:從 Activity#onRestoreInstanceState 開始看,可以看到這裡 取得 Bundle 對象 (也就是 Memo),呼叫 PhoneWindow 並傳入 Bundle 物件

// Activity.java

private Window mWindow;    // 實作類是 PhoneWindow

private static final String WINDOW_HIERARCHY_TAG = "android:viewHierarchyState";

protected void onRestoreInstanceState(@NonNull Bundle savedInstanceState) {
    if (mWindow != null) {
        // 取得復用的 Bundle 對象
        Bundle windowState = savedInstanceState.getBundle(WINDOW_HIERARCHY_TAG);
        if (windowState != null) {

            // @ 追蹤 restoreHierarchyState 方法
            mWindow.restoreHierarchyState(windowState);
        }
    }
}

Originator 角色:PhoneWindow#restoreHierarchyState 方法:目的是 恢復 View 的狀態取得 SparseArray<Parcelable> 並以 id 作為 key、Parcelable 作為 value (儲存 View 的訊息)

SparseArray<T> 簡單來說:是一個以 Integer 為 Key 的 Map 資料格式,也就是 Map<Integer, T>,不過它的效率較高

// PhoneWindow.java

ViewGroup mContentParent;

@Override
public void restoreHierarchyState(Bundle savedInstanceState) {
    if (mContentParent == null) {
        // 尚未加載 xml 布局
        return;
    }

    // 取得 SparseArray<Parcelable>
    SparseArray<Parcelable> savedStates
            = savedInstanceState.getSparseParcelableArray(VIEWS_TAG);

    if (savedStates != null) {
        // @ 追蹤 restoreHierarchyState 方法
        mContentParent.restoreHierarchyState(savedStates);
    }

    // 找尋當前 focused 的 View
    int focusedViewId = savedInstanceState.getInt(FOCUSED_ID_TAG, View.NO_ID);

    ... 省略部分

}

restoreHierarchyState 方法:View 遞迴呼叫,讓每個 View、ViewGroup 自己處理 (ViewGroup 會在 Override 這個函數)

// View.java

public void restoreHierarchyState(SparseArray<Parcelable> container) {
    // @ 追蹤 dispatchRestoreInstanceState 方法
    dispatchRestoreInstanceState(container);
}

A. View#dispatchRestoreInstanceState 方法:

從這邊可以看出 xml 上 沒有設定 android:id 屬性的 View 是不會回復狀態 !

由於儲存 View 的空間是使用 SparseArray 結構,所以 同一個 View Tree 上不能有相同的 ID 的 View,否則就只會有一個更新

// View.java

protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) {

    // 如果該 View 沒有 ID 則不儲存
    if (mID != NO_ID) {
        Parcelable state = container.get(mID);
        if (state != null) {

            // 清除 PFLAG_SAVE_STATE_CALLED
            mPrivateFlags &= ~PFLAG_SAVE_STATE_CALLED;

            // 呼叫 onRestoreInstanceState 方法
            onRestoreInstanceState(state);

            // 判斷 PFLAG_SAVE_STATE_CALLED,代表 onRestoreInstanceState 只會被呼叫一次
            if ((mPrivateFlags & PFLAG_SAVE_STATE_CALLED) == 0) {
                throw new IllegalStateException(
                        "Derived class did not call super.onRestoreInstanceState()");
            }
        }
    }
}

@CallSuper
protected void onRestoreInstanceState(Parcelable state) {
    mPrivateFlags |= PFLAG_SAVE_STATE_CALLED;

    ... 省略部分
}

B. ViewGroup#dispatchRestoreInstanceState 方法:迭代該 ViewGroup 中所有的 View,並遞迴呼叫所有 View#dispatchRestoreInstanceState 方法來回復 View 的狀態

// ViewGroup.java

@Override
protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) {
    // ViewGroup 如果有 id 也會存取
    super.dispatchRestoreInstanceState(container);

    // 該 ViewGroup  ChildView 的數量
    final int count = mChildrenCount;
    final View[] children = mChildren;

    for (int i = 0; i < count; i++) {
        View c = children[i];
        // 只要沒有禁止儲存,就會用遞迴不斷呼叫 dispatchRestoreInstanceState 方法
        if ((c.mViewFlags & PARENT_SAVE_DISABLED_MASK) != PARENT_SAVE_DISABLED) {
            c.dispatchRestoreInstanceState(container);
        }
    }
}

更多的物件導向設計

物件導向的設計基礎如下,如果是初學者或是不熟悉的各位,建議可以從這些基礎開始認識,打好基底才能走個更穩(在學習的時候也需要不斷回頭看)!

設計建模 2 大概念- UML 分類、使用

物件導向設計原則 – 6 大原則(一)

物件導向設計原則 – 6 大原則(二)

創建、行為、結構型設計 8 個比較 | 包裝模式 | 最佳實踐

創建模式 Creation Patterns

創建模式 PK

創建模式 - Creation Patterns

結構模式 Structural Patterns

結構模式 PK

結構模式 - Structural Patterns

結構模式專注於「物件之間的組成」,以形成更大的結構。這些模式可以幫助你確保當系統進行擴展或修改時,不會破壞其整體結構。例如,外觀模式、代理模式… 等等,詳細解說請點擊以下連結

Bridge 橋接模式 | 解說實現 | 物件導向設計

Decorate 裝飾模式 | 解說實現 | 物件導向設計

Proxy 代理模式 | 解說實現 | 分析動態代理

Iterator 迭代設計 | 解說實現 | 物件導向設計

Facade 外觀、門面模式 | 解說實現 | 物件導向設計

Adapter 設計模式 | 解說實現 | 物件導向設計

Leave a Comment

Comments

No comments yet. Why don’t you start the discussion?

發表迴響