十二、備忘錄設計模式
1. 備忘錄設計模式介紹
在不破壞封閉的前提下,捕獲一個物件的內部狀態,並在該物件之外儲存這個狀態,這樣,以後就可將該物件恢復到原先儲存的狀態。
2. 備忘錄設計模式使用場景
- 需要儲存一個物件在某一個時刻的狀態或部分狀態。
- 如果用一個介面來讓其它物件得到這些狀態,將會暴露物件的實現細節並破壞物件的封裝性,一個物件不希望外界直接訪問其內部狀態,通過中間物件可以間接訪問其內部狀態。
3. 備忘錄設計模式UML類圖
UML類圖角色介紹
Originator:負責建立一個備忘錄,可以記錄、恢復自身的內部狀態。同時Originator可以根據需要決定Memento儲存自身的那些內部狀態。
Memento: 備忘錄角色,用於儲存Originator的內部狀態,並且可以防止Originator以外的物件訪問Memento。
Caretaker: 負責儲存備忘錄,不能對備忘錄的內容進行操作和訪問,只能夠將備忘錄傳遞給其它物件。
4. 備忘錄設計模式的簡單實現
情景如下:我們都玩過單機遊戲,單機裡面有一個很重要的功能就是存檔,儲存當前遊戲進度。下次再進入遊戲時,恢復上一次的進度,繼續遊戲。
- (1)、遊戲類:Game
public class Game {
private int mCheckpoint = 1; //分數
private int mLifeValue = 100; //生命
//玩遊戲
public void play() {
mLifeValue -= 10;
mCheckpoint++;
}
public void quit() {
System.out.println("退出遊戲");
}
//顯示當前遊戲資訊
public void showInfos() {
System.out.println(this.toString());
}
public Memoto createMemoto () {
//建立存檔
Memoto memoto = new Memoto();
memoto.mLifeValue = mLifeValue;
memoto.mChackpoint = mCheckpoint;
return memoto;
}
//恢復存檔
public void restoreMemoto(Memoto memoto) {
this.mCheckpoint = memoto.mChackpoint;
this.mLifeValue = memoto.mLifeValue;
System.out.println("恢復進度");
}
@Override
public String toString() {
return "當前生命值:" + mLifeValue + ",分數:" + mCheckpoint;
}
}
上面的程式碼,最終要的部分就是createMemoto()方法,將當前狀態資訊儲存在Memoto物件裡面。
- (2)、備忘錄類:
public class Memoto {
public int mChackpoint;
public int mLifeValue;
public String mWeapon;
}
備忘錄類用來儲存遊戲類裡面一個或至多個資訊。
- (3)、備忘錄操作者:Caretaker角色:
public class Caretaker {
Memoto memoto;//備忘錄
public void archive(Memoto memoto) {
this.memoto = memoto;
}
//獲取存檔
public Memoto getMemoto() {
return memoto;
}
}
該類的作用就是操作備忘錄類Memoto本身的,並不對備忘錄裡面的資訊讀取操作。
- (4)、測試類:
public class Client {
public static void main(String[] args) {
Game game = new Game();
//打遊戲
game.play();
//存檔
Caretaker caretaker = new Caretaker();
caretaker.archive(game.createMemoto());
game.showInfos();
//退出遊戲
game.quit();
System.out.println("-----");
//恢復遊戲
Game newGame = new Game();
newGame.restoreMemoto(caretaker.getMemoto());
//顯示當前遊戲資訊
newGame.showInfos();
}
}
上面的測試類就是建立一個遊戲類,接著遊戲類修改自身的屬性,接著遊戲類建立備份,然後新建遊戲類,恢復備份,顯示遊戲進度和之前的遊戲物件進度一模一樣。
5. 備忘錄設計模式在Android原始碼中
在Android原始碼中,狀態模式的應用表現在Activity的狀態儲存,在onSaveIinstanceState和onRestoreInstanceState方法中
當Activity不是正常方式退出,且Activity在隨後的時間內被系統殺死之前會呼叫這兩個方法讓開發人員可以有機會儲存Activity相關資訊,並且下次再返回式恢復這些資料。
首先我們來說下onSaveInstanceState()方法裡面幹了什麼事:
這是onSaveInstanceState()裡面的方法,
protected void onSaveInstanceState(Bundle outState) {
//1.儲存視窗的檢視樹狀態
outState.putBundle(WINDOW_HIERARCHY_TAG,
mWindow.saveHierarchyState());
//2.儲存Fragment的狀態
Parcelable p = mFragments.saveAllState();
if (p != null) {
outState.putParcelable(FRAGMENTS_TAG, p);
}
//3.呼叫Activity的ActivityLifecycleCallbacks的onSaveInstanceState函式進行狀態儲存
getApplication().dispatchActivitySaveInstanceState(this, outState);
}
- (1)、儲存視窗的檢視樹狀態
- (2)、儲存Fragment的狀態
- (3)、呼叫Activity的ActivityLifecycleCallbacks的onSaveInstanceState函式進行狀態儲存
上面三個步驟都是儲存,我們首先來分析下儲存視窗的檢視狀態:
mWindow.saveHierarchyState());
這句程式碼就是儲存視窗的檢視樹狀態,這裡的mwindow物件是PhoneWindow ,我們在PhoneWindow找到這個方法:
saveHierachyState()方法簡化如下:
@Override
public Bundle saveHierarchyState() {
Bundle outState = new Bundle();
if (mContentParent == null) {
return outState;
}
//儲存整顆檢視樹的結構
SparseArray<Parcelable> states = new SparseArray<Parcelable>();
mContentParent.saveHierarchyState(states);
outState.putSparseParcelableArray(VIEWS_TAG, states);
// 儲存當前獲取了焦點的View
//儲存整個面板的狀態
//儲存ActionBar的狀態
return outState;
}
在以上saveHierarchyState函式中,主要儲存了與當前UI、ActionBar相關的View狀態。
這裡我們分析儲存整顆檢視樹:
- 程式碼中的mContentParent就是我們通過Activity的setContentView設定的內容檢視,它是整個檢視樹的根節點。
- mContentParent是一個ViewGroup物件,我們在ViewGroup的父類View中到saveHierarchyState()方法:
public void saveHierarchyState(SparseArray<Parcelable> container) {
dispatchSaveInstanceState(container);
}
protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) {
//如果View沒有設定Id,那麼該View的狀態資訊將不會被儲存
if (mID != NO_ID && (mViewFlags & SAVE_DISABLED_MASK) == 0) {
mPrivateFlags &= ~PFLAG_SAVE_STATE_CALLED;
//呼叫那個onSaveInstanceState()方法獲取自身狀態資訊
Parcelable state = onSaveInstanceState();
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);
//存入View狀態資訊,key為view的id。
container.put(mID, state);
}
}
}
上面的程式碼意思大致如下:
- 如果View沒有設定id,那麼該View的狀態資訊將不會被儲存
- 呼叫onSaveInsttanceState()方法獲取自身狀態資訊。
- 當前View的id為key,狀態資訊為value,存入之前建立的SparseArray 中,本質上是一個Object陣列。
以上是View類的中saveHierarchySate函式中dispatchSaveInstanceState函式來儲存自身的狀態
如果是ViewGroup呢?下面是ViewGroup中的dispatchSaveInstanceState函式:
protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) {
//呼叫父類View的dispatchInstanceState方法儲存自身狀態
super.dispatchSaveInstanceState(container);
final int count = mChildrenCount;
final View[] children = mChildren;
//遍歷所有的子檢視呼叫其dispatchInstanceState方法儲存它們的狀態
for (int i = 0; i < count; i++) {
View c = children[i];
if ((c.mViewFlags & PARENT_SAVE_DISABLED_MASK) != PARENT_SAVE_DISABLED) {
c.dispatchSaveInstanceState(container);
}
}
}
上面的程式碼做了以下兩件事:
- 首先呼叫父類View的dispatchSaveInstanceState方法儲存了自身的狀態資訊
- 接著遍歷所有的子類,呼叫其dispatchSaveInstanceState方法儲存它們的狀態資訊
在View的saveHierarchyState方法裡面有如下程式碼:
//呼叫那個onSaveInstanceState()方法獲取自身狀態資訊
Parcelable state = onSaveInstanceState();
這句程式碼的意思是獲取自身狀態資訊,我們點進去檢視原始碼如下:
protected Parcelable onSaveInstanceState() {
mPrivateFlags |= PFLAG_SAVE_STATE_CALLED;
if (mStartActivityRequestWho != null) {
BaseSavedState state = new BaseSavedState(AbsSavedState.EMPTY_STATE);
state.mStartActivityRequestWhoSaved = mStartActivityRequestWho;
return state;
}
return BaseSavedState.EMPTY_STATE;
}
上面的程式碼意思是:返回一個儲存了當前View狀態資訊的Parcelable物件,如果沒有任何資訊,返回null,預設返回null。我們可以得出以下資訊
- View檢視的狀態資訊的儲存在Parcelable物件裡面
- 如果我們要儲存View的狀態資訊,需要覆寫onSaveInstanceState()方法,將需要儲存的資訊存放在Parcelable裡面,然後返回。
到這裡我們畫張圖小結一下View和ViewGroup儲存的流程:
上面分析了儲存了Window的檢視樹狀態資訊。
儲存了Window的檢視樹狀態資訊後,便會執行儲存Fragment中的狀態資訊、回退棧等 。
下面我們來分析儲存了狀態資訊的Bundle資料儲存在哪裡?
我們知道onSveInstanceState是在Activity被銷燬之前,onStop呼叫之前。onStop方法在ActivityThread的performStopActivity函式中,這裡就不列出程式碼了,主要的步驟大致如下:
- (1)、判斷是否需要儲存Activity
- (2)、如果需要儲存Activity狀態,呼叫onSaveInstanceState函式獲取狀態資訊。
- (3)、將狀態資訊儲存到ActivityClientRecord物件的state欄位
- (3)、系統維護了一個Acitivity資訊表mActivities,將AcitivityClientRecord物件儲存到Acitivity資訊表中。
- (4)、呼叫Activity的onStop()函式
當Activity重新啟時:
- 從mActivities查詢對應的ActivityClientRecord,如果這個記錄物件中包含有狀態資訊,那麼呼叫Activity的onRestoreInstanceState函式,然後將這些狀態資訊傳遞給onCreat方法
總結一下onSaveInstanceState的呼叫時機:
當系統未經我們允許時銷燬了Acitivity,onSaveInstanceState()方法會被呼叫。常見的幾種場景:
- 當用戶按下Home鍵時
- 按下電源鍵時
- 啟動一個新的Activity時
- 來電話時
- 螢幕發生旋轉時
6. 備忘錄設計模式在Android開發中
如下場景:簡單實現上面的onSaveInstanceState()的使用,當我我們輸入使用者名稱和密碼後,通過旋轉螢幕Activity會被銷燬,如果不儲存,相關資訊可能會丟失,所以在onSaveInstanceState儲存相關資訊,在onCreate方法裡面獲取資訊,重新填充即可。
簡單演示:
程式碼簡單實現如下:
public class MainActivity extends AppCompatActivity {
private EditText et_name;
private EditText et_psw;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
et_name = (EditText) findViewById(R.id.et_name);
et_psw = (EditText) findViewById(R.id.et_psw);
if (savedInstanceState!=null){
String username = savedInstanceState.getString("username");
String psw = savedInstanceState.getString("psw");
System.out.println("username:" + username + ",psw:" + psw);
et_name.setText(username);
et_psw.setText(psw);
}
}
@Override
protected void onSaveInstanceState(Bundle outState) {
outState.putString("username", et_name.getText().toString());
outState.putString("psw", et_psw.getText().toString());
super.onSaveInstanceState(outState);
}
}
7、總結
- 優點:
- 給使用者提供了一種可以恢復狀態的機制。可以是使用者能夠比較方便地回到某個歷史的狀態。
- 實現了資訊的封裝。使得使用者不需要關心狀態的儲存細節。
- 缺點:
- 消耗資源。如果類的成員變數過多,勢必會佔用比較大的資源,而且每一次儲存都會消耗一定的記憶體。