1. 案例
案例:我們常見的汽車,我們可以使用它行駛,也可以將它停止在路邊。當它在行駛的過程中,需要不斷的檢測油量,一旦油量不足的時候,就將陷入停止狀態。而停止在路邊的汽車,需要點火啟動,此時將檢測車中的油量,當油量不足的時候,汽車就需要去加油站加油。
當我們對汽車的狀態和行為進行抽象,汽車的狀態可以有 :
- 停車 STOP
- 行駛 RUN
- 檢測油量 CHECK_OIL
- 加油 ADDING_OIL
而我們可以對汽車的操作可以是:
- 停車 ACTION_STOP
- 行駛 ACTION_RUN
- 加油 ACTION_ADD_OIL
我們建立一個二維表,將狀態和可操作的行為組合在一起:
2. HSM
我們通過這個狀態表構建我們的狀態引用關係模型:
這幅狀態圖實際上一個相對複雜的網狀圖形,當構建一個更為複雜的系統的時候,這種網狀圖將會以成倍的複雜性遞增。為了解決這個問題,我們需要將這種網狀的狀態機轉化為一個樹狀的層次狀態機,也叫 HSM (Hierarchical State Machine)。我們可以將上述的狀態模型轉化為:
這張圖裡,將 STOP
作為根節點,從層次上作為其他狀態節點的父節點。
STOP
作為初始狀態- 發生了
ACTION_ADD_OIL
動作,STOP
狀態就變成了ADDING_OIL
狀態 - 當
ADDING_OIL
結束,發生了ACTION_RUN
動作,就需要彈出ADDING_OIL
狀態 ,傳入到CHECK_OIL
,然後傳入RUN
狀態。
3. [StateMachine] 初始化
StateMachine
是 Android
系統提供的 HSM
狀態機的實現,它的原始碼在包com.android.internal.util
下。StateMachine
提供了三個構造方法,但這三個方法大同小異:
protected StateMachine(String name) {
mSmThread = new HandlerThread(name);
mSmThread.start();
Looper looper = mSmThread.getLooper();
initStateMachine(name, looper);
}
複製程式碼
構造器呼叫 initStateMachine
函式,這個函式需要傳入了一個 Looper
物件,StateMachine
物件所有的操作都需要在這個 Looper
所在的執行緒中執行。而之間的通訊是通過 SmHandler
物件傳遞。
private void initStateMachine(String name, Looper looper) {
mName = name;
mSmHandler = new SmHandler(looper, this);
}
複製程式碼
上面我們說了,StateMachine
是 HSM
狀態機,構造它的時候,需要指定它的層次關係,這需要呼叫 addState
函式,這個函式有兩個引數,第第二個引數代表的是第一個引數的父節點:
protected final void addState(State state, State parent) {
mSmHandler.addState(state, parent);
}
複製程式碼
而根節點,又稱為初始狀態節點,需要通過 setInitialState
函式指定:
protected final void setInitialState(State initialState) {
mSmHandler.setInitialState(initialState);
}
複製程式碼
這裡,不論設定什麼樣的節點,都需要通過 mSmHandler
物件設定,比如,當通過呼叫 StateMachine.addState
新增節點的時候,需要呼叫到 SmHandler.addState
函式:
//code SmHandler
private final StateInfo addState(State state, State parent) {
if (mDbg) {
//debug開關可以通過 StateMachine.setDbg介面設定開啟
mSm.log("addStateInternal: E state=" + state.getName() + ",parent="
+ ((parent == null) ? "" : parent.getName()));
}
StateInfo parentStateInfo = null;
// StateInfo 表示在 HSM 樹中的狀態節點
if (parent != null) {
parentStateInfo = mStateInfo.get(parent);
//mStateInfo 是一個hashmap物件
if (parentStateInfo == null) {
//當父節點不存在的時候,新增該節點
// Recursively add our parent as it's not been added yet.
parentStateInfo = addState(parent, null);
}
}
StateInfo stateInfo = mStateInfo.get(state);
//通過狀態構建一個狀態節點
if (stateInfo == null) {
stateInfo = new StateInfo();
mStateInfo.put(state, stateInfo);
}
// Validate that we aren't adding the same state in two different hierarchies.
if ((stateInfo.parentStateInfo != null)
&& (stateInfo.parentStateInfo != parentStateInfo)) {
//不允許一個節點存在兩個父節點
throw new RuntimeException("state already added");
}
stateInfo.state = state;
stateInfo.parentStateInfo = parentStateInfo;
//構建父子的層次關係
stateInfo.active = false;
if (mDbg) mSm.log("addStateInternal: X stateInfo: " + stateInfo);
return stateInfo;
}
複製程式碼
mStateInfo
是一個 HashMap<State, StateInfo>
型別的物件, 而 StateInfo
類是用於記錄狀態 State
物件資訊,和父節點資訊的 HSM
節點物件
private class StateInfo {
/** The state */
State state;
/** The parent of this state, null if there is no parent */
StateInfo parentStateInfo;
/** True when the state has been entered and on the stack */
boolean active;
}
複製程式碼
按照我們剛才對 Car
這個模型的抽象,我們可以定義出一個 Car
的狀態機:
public class Car extends StateMachine {
....
public Car(String name) {
super(name);
this.addState(mStopState,null);
//mStopState 作為根節點狀態,沒有父節點
this.addState(mAddOilState,mStopState);
//mAddOilState 作為mStopState 的子狀態
this.addState(mCheckOilState,mStopState);
//mCheckOilState 作為mStopState 的子狀態
this.addState(mRunState,mCheckOilState);
//mRunState 作為mCheckOilState 的子狀態
this.setInitialState(mStopState);
// mStopState 為初始狀態
}
}
複製程式碼
當我們構造完我們的樹形結構了以後,我們就可以將我們的狀態機啟動起來,這個啟動依賴於 StateMachine.start
函式:
public void start() {
// mSmHandler can be null if the state machine has quit.
SmHandler smh = mSmHandler;
if (smh == null) return;
/** Send the complete construction message */
smh.completeConstruction();//呼叫SmHandler.completeConstruction
}
複製程式碼
StateMachine.start
中呼叫 SmHandler.completeConstruction
用於提交我們之前的所有操作:
private final void completeConstruction() {
if (mDbg) mSm.log("completeConstruction: E");
/**
* Determine the maximum depth of the state hierarchy
* so we can allocate the state stacks.
*/
int maxDepth = 0;// step1
for (StateInfo si : mStateInfo.values()) {
int depth = 0;
for (StateInfo i = si; i != null; depth++) {
i = i.parentStateInfo;
}
if (maxDepth < depth) {
maxDepth = depth;//找到一個最深的堆疊
}
}
if (mDbg) mSm.log("completeConstruction: maxDepth=" + maxDepth);
mStateStack = new StateInfo[maxDepth];
mTempStateStack = new StateInfo[maxDepth];//用於計算的臨時變數
setupInitialStateStack();//以初始狀態為棧底儲存到 mStateStack
/** Sending SM_INIT_CMD message to invoke enter methods asynchronously */
sendMessageAtFrontOfQueue(obtainMessage(SM_INIT_CMD, mSmHandlerObj));
if (mDbg) mSm.log("completeConstruction: X");
}
複製程式碼
按照我們的 HSM
模型,以 STOP
狀態為基礎狀態的時候,那麼我們以這個狀態為棧底向上延伸,我們可以得到兩個棧,分別是:
stack1: [STOP,CHECK_OIL,RUN]
stack2: [STOP,ADD_OIL]
複製程式碼
stack1
的最大深度為 3 , stack2
的最大深度為 2 。那麼 stack1
就可以應用於 stack2
的情況。 completeConstruction
程式碼中 step1
段的程式碼就是這個目的,找到一個最大的棧,用於給所有的棧情況使用。
private final void setupInitialStateStack() {
if (mDbg) {
mSm.log("setupInitialStateStack: E mInitialState=" + mInitialState.getName());
}
StateInfo curStateInfo = mStateInfo.get(mInitialState);
for (mTempStateStackCount = 0; curStateInfo != null; mTempStateStackCount++) {
mTempStateStack[mTempStateStackCount] = curStateInfo;
curStateInfo = curStateInfo.parentStateInfo;
}
//將初始狀態位根狀態以0->N的順序存入 tempStack
// Empty the StateStack
mStateStackTopIndex = -1;
moveTempStateStackToStateStack();//將 tempstack 倒敘複製給 stateStack
}
複製程式碼
mTempStateStack
是一箇中間變數,它存的是倒敘的 mStateStack
。比如我們的初始狀態是 RUN
。那麼我們需要不斷迴圈將 RUN
的父節點存入 mTempStateStack
得到:
mTempStateStack :[RUN,CHECK_OIL,STOP]
複製程式碼
這時候我們需要呼叫 moveTempStateStackToStateStack
函式將它倒敘複製到 mStateStack
物件中,保證當前狀態 RUN
位於棧頂:
mStateStack: [STOP,CHECK_OIL,RUN]
複製程式碼
mStateStackTopIndex
變數指向 mStateStack
的棧頂。剛才的這個例子,mStateStackTopIndex
的值為 2 ,指向 RUN
所在的陣列索引位置。
到了 start
函式呼叫的這一步,我們就完成了一個樹形資料結構和初始狀態的設定,接下來,我們就可以往我們的狀態機上傳送我們的指令。
4. [StateMachine] 處理訊息
我們通過上面的手段構造完一個狀態機以後,就可以通過指令讓這個狀態機去處理訊息了。我們先給我們的狀態機開一些外部呼叫的介面:
public interface ICar {
public void run();
public void stop();
public void addOil();
}
public class Car extends StateMachine implements ICar{
....
}
public void func() {
ICar car = new Car("Ford");
car.addOil();
car.run();
car.stop();
}
複製程式碼
當我們要向我們的狀態機傳送指令的時候,需要呼叫狀態機的 sendMessage(...)
函式,這套函式跟 android.os.Handler
提供的 api
的含義一模一樣。實際上,狀態機在處理這種訊息的時候,也是採用 Handler
的方式,而我們上面反覆提到的 SmHandler
物件實際上就是 Handler
物件的子類。
public final void sendMessage(int what) {
// mSmHandler can be null if the state machine has quit.
SmHandler smh = mSmHandler;
if (smh == null) return;
smh.sendMessage(obtainMessage(what));//通過Handler方式傳送訊息
}
複製程式碼
這樣,我們就可以通過這個函式去實現我們的幾個介面方法:
public class Car extends StateMachine implements ICar{
...
public void run() {
this.sendMessage(ACTION_RUN);
}
public void stop() {
this.sendMessage(ACTION_STOP);
}
public void addOil() {
this.sendMessage(ACTION_ADD_OIL);
}
}
複製程式碼
根據我們對 Handler
類的瞭解,每當我們通過 Handler.sendMessage
函式傳送一個訊息的時候,都將在 Looper
的下個處理訊息執行的時候,回撥 Handler.handleMessage(Message msg)
方法。由於 SmHandler
繼承於 Handler
,並且它複寫了 handleMessage
函式,因此 , 訊息傳送之後,最後將回撥到SmHandler.handleMessage
方法中。
//code SmHandler
public final void handleMessage(Message msg) {
if (!mHasQuit) {
if (mDbg) mSm.log("handleMessage: E msg.what=" + msg.what);
/** Save the current message */
mMsg = msg;
/** State that processed the message */
State msgProcessedState = null;
if (mIsConstructionCompleted) {
/** Normal path */
msgProcessedState = processMsg(msg);
//由當前狀態處理
} else if (!mIsConstructionCompleted && (mMsg.what == SM_INIT_CMD)
&& (mMsg.obj == mSmHandlerObj)) {
/** Initial one time path. */
//執行初始化操作函式
mIsConstructionCompleted = true;
invokeEnterMethods(0);
//當呼叫
} else {
throw new RuntimeException("StateMachine.handleMessage: "
+ "The start method not called, received msg: " + msg);
}
performTransitions(msgProcessedState, msg);
// We need to check if mSm == null here as we could be quitting.
if (mDbg && mSm != null) mSm.log("handleMessage: X");
}
}
複製程式碼
SmHandler.handleMessage
函式主要執行以下幾個操作:
- 根據
mHasQuit
判斷是否退出,如果退出將不執行後續指令 - 判斷是否初始完成(根據變數
mIsConstructionCompleted
),如果初始化完成呼叫processMsg
將訊息拋給當前狀態執行 - 如果尚未初始化,並且接受的是初始化命令
SM_INIT_CMD
將執行一次初始化操作 - 當命令執行結束後,執行
performTransitions
函式用於轉變當前狀態和mStateStack
我們先接著上面第三個主題 [StateMachine] 初始化
看下第三步。 SM_INIT_CMD
指令的發出位於 SmHandler.completeConstruction
函式中:
//code SmHandler
private final void completeConstruction() {
...
/** Sending SM_INIT_CMD message to invoke enter methods asynchronously */
sendMessageAtFrontOfQueue(obtainMessage(SM_INIT_CMD, mSmHandlerObj));
...
}
複製程式碼
處理初始化訊息的時候會先將 mIsConstructionCompleted
設定為 true
,告訴狀態機已經初始化過了,可以讓狀態處理訊息了。然後呼叫了個 invokeEnterMethods
函式。這個函式的目的是回撥當前 mStateStack
棧中所有的活動狀態的 enter
方法。並且將非活躍狀態設定為活躍態:
private final void invokeEnterMethods(int stateStackEnteringIndex) {
for (int i = stateStackEnteringIndex; i <= mStateStackTopIndex; i++) {
mStateStack[i].state.enter();
mStateStack[i].active = true;
}
}
複製程式碼
這樣,如果我們的初始狀態是 STOP
的話,我們就可以在後臺列印中看到:
//console output:
output: [StateMachine] StopState enter
複製程式碼
如果我們的初始狀態是 RUN
狀態的話就可以看到:
//console output:
output: [StateMachine] StopState enter
output: [StateMachine] CheckOilState enter
output: [StateMachine] RunState enter
複製程式碼
上面就是處理初始化訊息的過程,到這一步,初始化的過程算是完整走完。我們繼續來看初始化後的邏輯,當初始化已經結束之後,再收到的訊息將通過 processMsg
函式提交給合適的狀態執行。
private final State processMsg(Message msg) {
StateInfo curStateInfo = mStateStack[mStateStackTopIndex];
//獲取當前狀態節點
if (isQuit(msg)) {
//判斷當前訊息是否是退出訊息
transitionTo(mQuittingState);
} else {
while (!curStateInfo.state.processMessage(msg)) {
//當該狀態不處理當前訊息的時候,將委託給父狀態處理
curStateInfo = curStateInfo.parentStateInfo;
if (curStateInfo == null) {
mSm.unhandledMessage(msg);
//當沒有狀態可以處理當前訊息的時候回撥unhandledMessage
break;
}
}
}
return (curStateInfo != null) ? curStateInfo.state : null;
}
複製程式碼
processMsg
會先判斷當前是否是退出訊息,如果 isQuit
成立,將轉入 mQuittingState
狀態。我們將在後面分析如何執行退出操作,這塊東西,我們暫且有個印象。當並非退出訊息時候,將會分配給當前狀態處理,如果當前狀態處理不了,將委託給父狀態處理。比如當前我們的初始狀態是 RUN
。那麼對應的 mStateStack
為:
[STOP,CHECK_OIL,RUN]
複製程式碼
我們給狀態的測試程式碼是:
private class BaseState extends State {
@Override
public void enter() {
log(" enter "+this.getClass().getSimpleName());
super.enter();
}
@Override
public void exit() {
log(" exit "+this.getClass().getSimpleName());
super.exit();
}
}
public class StopState extends BaseState {
@Override
public boolean processMessage(Message msg) {
log("StopState.processMessage");
return HANDLED;//處理訊息
}
}
public class CheckOilState extends BaseState {
@Override
public boolean processMessage(Message msg) {
log("CheckOilState.processMessage");
return NOT_HANDLED;// 不處理訊息
}
}
public class RunState extends BaseState {}
複製程式碼
我們往狀態機 Car
傳送一條訊息:
Car car = new Car();
car.sendMessage(0x01);
複製程式碼
我們將在後臺列印出log:
--> enter StopState
--> enter CheckOilState
--> enter RunState
// 初始化結束
-->[StateMachine]:handleMessage 1
-->CheckOilState.processMessage // run狀態不處理,扔給checkoil狀態
-->StopState.processMessage // checkoil狀態不處理,扔給stop 狀態
複製程式碼
當然,如果你並不希望訊息被委託呼叫,你可以在初始狀態呼叫 processMessage
函式的時候,返回 HANDLED
常量,這樣就不會往下呼叫。
5. [StateMachine] 狀態轉換
通常,我們會在 State.processMessage
內部,通過呼叫 transitionTo
函式執行一次狀態轉換,而呼叫這個函式只是將你要轉換的狀態存入一個臨時的物件中:
protected final void transitionTo(IState destState) {
mSmHandler.transitionTo(destState);
}
private final void transitionTo(IState destState) {
mDestState = (State) destState;
}
複製程式碼
真正的狀態轉換將發生在 SmHandler.handleMessage
函式執行之後:
public final void handleMessage(Message msg) {
if (!mHasQuit) {
...
performTransitions(msgProcessedState, msg);//變更狀態
}
}
複製程式碼
這裡將呼叫 performTransitions
函式完成狀態轉換,假如,現在的狀態是 RUN
狀態,當需要轉成 ADD_OIL
狀態的時候,將進行一下轉變:
/**
初始:
mStateStack : [ STOP,CHECK_OIL,RUN]
*/
private void performTransitions(State msgProcessedState, Message msg) {
State orgState = mStateStack[mStateStackTopIndex].state;
//orgState記錄當前狀態
State destState = mDestState;
//destState 記錄要轉變的目標狀態
if (destState != null) {
while (true) {
StateInfo commonStateInfo = setupTempStateStackWithStatesToEnter(destState);
//查詢跟目標狀態的公共節點狀態,此時為 STOP 狀態節點
invokeExitMethods(commonStateInfo);
//從棧頂一直到commonStateInfo(不包含) 所在的位置執行退出操作
int stateStackEnteringIndex = moveTempStateStackToStateStack();
invokeEnterMethods(stateStackEnteringIndex);
moveDeferredMessageAtFrontOfQueue();
//將Deferred 訊息放入佇列頭部優先執行
if (destState != mDestState) {
destState = mDestState;
} else {
break;
}
}
mDestState = null;
}
if (destState != null) {
if (destState == mQuittingState) {
//TODO clean
} else if (destState == mHaltingState) {
//TODO halt
}
}
}
複製程式碼
這段程式碼執行的時候,會先去尋找目標節點和當前節點的公共祖先節點,這是通過呼叫 setupTempStateStackWithStatesToEnter
呼叫的。StateMachine
的函式名起的見名知意,*Temp*
代表這個函式中要使用中間變數 mTempStateStack
。*ToEnter
代表需要對新增進的狀態執行 State.enter
操作。
private final StateInfo setupTempStateStackWithStatesToEnter(State destState) {
mTempStateStackCount = 0;//重置 mTempStateStack
StateInfo curStateInfo = mStateInfo.get(destState);
do {
mTempStateStack[mTempStateStackCount++] = curStateInfo;
curStateInfo = curStateInfo.parentStateInfo;
} while ((curStateInfo != null) && !curStateInfo.active);
//找到第一個 active 的狀態節點。
return curStateInfo;
}
複製程式碼
setupTempStateStackWithStatesToEnter
函式就是將目標節點的堆疊複製到 mTempStateStack
變數中,然後將最終相交的節點返回。這裡採用 do-while
的寫法,說明這個函式的執行,至少包含一個 destState
元素。剛才從 RUN->ADD_OIL
的例子中,setupTempStateStackWithStatesToEnter
將返回 STOP
狀態,mTempStateStack
的為:
mTempStateStack: {ADD_OIL}
複製程式碼
我們回到 performTransitions
的流程,執行 setupTempStateStackWithStatesToEnter
完,將執行 invokeExitMethods
函式。
private final void invokeExitMethods(StateInfo commonStateInfo) {
while ((mStateStackTopIndex >= 0)
&& (mStateStack[mStateStackTopIndex] != commonStateInfo)) {
State curState = mStateStack[mStateStackTopIndex].state;
curState.exit();
mStateStack[mStateStackTopIndex].active = false;
mStateStackTopIndex -= 1;
}
}
複製程式碼
這個函式相當於將 mStateStack
棧中的非 commonStateInfo
進行出棧。
mStateStack: {STOP,CHECK_OIL,RUN} ->
invokeExitMethods(STOP) ->
mStateStack: {STOP}
複製程式碼
執行完出棧後,只需要將我們剛才構建的 mTempStateStack
拷貝到 mStateStack
就可以構建新的狀態棧了,而這個操作是通過 moveTempStateStackToStateStack
函式完成,而 moveTempStateStackToStateStack
我們剛才說過,實際上就是將 mTempStateStack
逆序賦值到 mStateStack
。這樣,我們就構建了一個新的 mStateStack
:
mStateStack: {STOP,ADD_OIL}
複製程式碼
這個時候,我們構建了一個新的狀態棧,相當於已經切換了狀態。performTransitions
在執行完 moveTempStateStackToStateStack
之後,呼叫 invokeEnterMethods
函式,執行非 active
狀態的 enter
方法。之後執行 moveDeferredMessageAtFrontOfQueue
將通過 deferMessage
函式快取的訊息佇列放到 Handler
訊息佇列的頭部:
...
int stateStackEnteringIndex = moveTempStateStackToStateStack();
invokeEnterMethods(stateStackEnteringIndex);
moveDeferredMessageAtFrontOfQueue();
//將Deferred 訊息放入佇列頭部優先執行
if (destState != mDestState) {
destState = mDestState;
} else {
break;
}
...
複製程式碼
當我們完成狀態的轉換了以後,需要對兩種特殊的狀態進行處理,在 performTransitions
函式的末尾會判斷兩個特殊的狀態:
1. HaltingState
2. QuittingState
複製程式碼
6. [StateMachine] 狀態機的退出
狀態機的退出,StateMachine
提供了幾個方法:
- quit: 執行完訊息佇列中所有的訊息後執行退出和清理操作
- quitNow: 拋棄掉訊息佇列中的訊息,直接執行退出和清理操作
- transitionToHaltingState: 拋棄掉訊息佇列中的訊息,直接執行退出,不做清理
從上面的表述中看,quit
相對 halt
操作來說更加的安全。這個 Thread
的 intercept
和 stop
方法很類似,很好理解。上面我們說到,退出狀態 HaltingState
和 QuittingState
是在performTransitions
函式的末尾判斷和執行的,我們來看下程式碼:
if (destState != null) {
if (destState == mQuittingState) {
mSm.onQuitting();
cleanupAfterQuitting();//清理操作
} else if (destState == mHaltingState) {
mSm.onHalting();//只是執行回撥
}
}
private final void cleanupAfterQuitting() {
if (mSm.mSmThread != null) {
getLooper().quit();//退出執行緒
mSm.mSmThread = null;
}
/*清空資料*/
mSm.mSmHandler = null;
mSm = null;
mMsg = null;
mLogRecords.cleanup();
mStateStack = null;
mTempStateStack = null;
mStateInfo.clear();
mInitialState = null;
mDestState = null;
mDeferredMessages.clear();
mHasQuit = true;
}
複製程式碼
當 destState == mQuittingState
語句成立,將回撥 StateMachine.onQuitting
函式,之後將執行 cleanupAfterQuitting
進行清理操作。清理操作中,會將執行緒清空,和其他資料變數清空,而如果 destState == mHaltingState
成立,StateMachine
將不執行任何的清理操作,通過回撥 onHalting
函式來通知狀態機退出。
7. 總結
Android
裡面的這個 StateMachine
狀態機在很多原始碼中都有涉及,程式碼也很簡單,沒有什麼太大的難度,希望以上的總結能幫各位看官理解 StateMachine
原始碼的含義,並且能基於它,開發更多個性化的功能