Overview of Content
備忘錄(Memo
)設計是一種行為模式,被儲存的物件不會被外部讀取,在不破壞封裝的前提下,獲取、儲存一個物件的內部狀態
本文探討 Memo 備忘錄模式的應用和實現,以及在 Android Framework 中的相關設計。首先,我們會深入了解 Memo 模式的使用場景,並解釋其定義和 UML 示意圖,進一步討論其優缺點。接著,我們會探討 Memo 模式的具體實現方法,以及相關的標準和最佳實踐。此外,我們還會介紹 Memo 模式的一個變形,即使用 Clone 取代 Memo
最後,我們將重點關注於 Android Framework 中的相關部分,包括 Activity 中的 onSaveInstanceState
和 onRestoreInstanceState
方法,以及它們在資料暫存和恢復方面的應用。
Memo 使用場景
● 需要儲存一個物件在某一個時刻的狀態 (並非執行步驟),並提供使用者 rollback 操作
● 當一個物件不希望直接被讀取到其狀態 (private
),就可 透過「中間物件」存取 (這個中間物件就是 Memo
)
Memo 定義 & Memo UML
● Memo 定義:在不破獲封裝的前提下,保存一個物件的狀態,好讓之後可以恢復該對象的狀態
● Memo UML 角色關係
Memo 優缺點
● Memo 設計優點 :
● 提供使用者快速方便的恢復機制,方便找回歷史狀態或是 rollback 資料,而這些操作都是安全性的操作
● 從上面可以看出 使用者不必關心細節 (getter
、setter
)
● Memo 設計缺點 :
● 消耗資源,類的增加 (這個問題基本上是設計模式的通病),每次儲存都會消耗資源空間
Memo 實現
Memo 標準
A. Originator
類:GameProvider
讓使用者直接操控的類 (對外暴露),同時包括 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;
}
可以選擇使用 Memo 變形、反射、抽象… 等等
Originator
是跟Memo
類有相同的 member,所以它必須擁有該類的所有屬性 (可用data class
),在這部分可以用另外一個抽象優化
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 實做 | 說明 |
---|---|---|
Memo | Bundle | 設定要緩存、攜帶的資料 |
Originator | View、ViewGroup | 一般的 View 操作,不過 多了 Bundle 存取控制 |
Caretaker | Activity、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<
並以 id 作為 key、Parcelable 作為 value (儲存 View 的訊息)Parcelable
>
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);
}
}
}
更多的物件導向設計
物件導向的設計基礎如下,如果是初學者或是不熟悉的各位,建議可以從這些基礎開始認識,打好基底才能走個更穩(在學習的時候也需要不斷回頭看)!
創建模式 Creation Patterns
● 創建模式 PK
● 創建模式 - Creation Patterns
:
創建模式用於「物件的創建」,它關注於如何更靈活、更有效地創建物件。這些模式可以隱藏創建物件的細節,並提供創建物件的機制,例如單例模式、工廠模式… 等等,詳細解說請點擊以下連結
● Singleton 單例模式 | 解說實現 | Android Framework Context Service
● Abstract Factory 設計模式 | 實現解說 | Android MediaPlayer
● Factory 工廠方法模式 | 解說實現 | Java 集合設計
● Builder 建構者模式 | 實現與解說 | Android Framwrok Dialog 視窗
● Clone 原型模式 | 解說實現 | Android Framework Intent
行為模式 Behavioral Patterns
● 行為模式 PK
● 行為模式 - Behavioral Patterns
:
行為模式關注物件之間的「通信」和「職責分配」。它們描述了一系列物件如何協作,以完成特定任務。這些模式專注於改進物件之間的通信,從而提高系統的靈活性。例如,策略模式、觀察者模式… 等等,詳細解說請點擊以下連結
● Stragety 策略模式 | 解說實現 | Android Framework 動畫
● Interpreter 解譯器模式 | 解說實現 | Android Framework PackageManagerService
● Chain 責任鏈模式 | 解說實現 | Android Framework View 事件傳遞
● Specification 規格模式 | 解說實現 | Query 語句實做
● Command 命令、Servant 雇工模式 | 實現與解說 | 物件導向設計
● Memo 備忘錄模式 | 實現與解說 | Android Framwrok Activity 保存
● Visitor 設計模式 | 實現與解說 | 物件導向設計
● Template 設計模式 | 實現與解說 | 物件導向設計
● Mediator 模式設計 | 實現與解說 | 物件導向設計
● Composite 組合模式 | 實現與解說 | 物件導向設計
● Observer 觀察者模式 | JDK Observer | Android Framework Listview
結構模式 Structural Patterns
● 結構模式 PK
● 結構模式 - Structural Patterns
:
結構模式專注於「物件之間的組成」,以形成更大的結構。這些模式可以幫助你確保當系統進行擴展或修改時,不會破壞其整體結構。例如,外觀模式、代理模式… 等等,詳細解說請點擊以下連結
● Decorate 裝飾模式 | 解說實現 | 物件導向設計
● Iterator 迭代設計 | 解說實現 | 物件導向設計