[Android] 狀態機 StateMachine 源碼剖析

1. 案例

案例:咱們常見的汽車,咱們可使用它行駛,也能夠將它中止在路邊。當它在行駛的過程當中,須要不斷的檢測油量,一旦油量不足的時候,就將陷入中止狀態。而中止在路邊的汽車,須要點火啓動,此時將檢測車中的油量,當油量不足的時候,汽車就須要去加油站加油。android

當咱們對汽車的狀態和行爲進行抽象,汽車的狀態能夠有 :api

  • 停車 STOP
  • 行駛 RUN
  • 檢測油量 CHECK_OIL
  • 加油 ADDING_OIL

而咱們能夠對汽車的操做能夠是:數組

  • 停車 ACTION_STOP
  • 行駛 ACTION_RUN
  • 加油 ACTION_ADD_OIL

咱們創建一個二維表,將狀態和可操做的行爲組合在一塊兒:緩存

狀態行爲對應表

2. HSM

咱們經過這個狀態表構建咱們的狀態引用關係模型:安全

狀態轉化模型

這幅狀態圖實際上一個相對複雜的網狀圖形,當構建一個更爲複雜的系統的時候,這種網狀圖將會以成倍的複雜性遞增。爲了解決這個問題,咱們須要將這種網狀的狀態機轉化爲一個樹狀的層次狀態機,也叫 HSM (Hierarchical State Machine)。咱們能夠將上述的狀態模型轉化爲:bash

層次狀態圖

這張圖裏,將 STOP 做爲根節點,從層次上做爲其餘狀態節點的父節點。數據結構

  • STOP 做爲初始狀態
  • 發生了 ACTION_ADD_OIL 動做,STOP 狀態就變成了 ADDING_OIL 狀態
  • ADDING_OIL 結束,發生了 ACTION_RUN 動做,就須要彈出 ADDING_OIL狀態 ,傳入到 CHECK_OIL,而後傳入 RUN 狀態。

3. [StateMachine] 初始化

StateMachineAndroid 系統提供的 HSM 狀態機的實現,它的源碼在包com.android.internal.util下。StateMachine 提供了三個構造方法,但這三個方法大同小異:async

protected StateMachine(String name) {
        mSmThread = new HandlerThread(name);
        mSmThread.start();
        Looper looper = mSmThread.getLooper();

        initStateMachine(name, looper);
}
複製代碼

構造器調用 initStateMachine 函數,這個函數須要傳入了一個 Looper 對象,StateMachine 對象全部的操做都須要在這個 Looper 所在的線程中運行。而之間的通信是經過 SmHandler 對象傳遞。ide

private void initStateMachine(String name, Looper looper) {
        mName = name;
        mSmHandler = new SmHandler(looper, this);
    }
複製代碼

上面咱們說了,StateMachineHSM 狀態機,構造它的時候,須要指定它的層次關係,這須要調用 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 函數主要執行如下幾個操做:

  1. 根據 mHasQuit 判斷是否退出,若是退出將不執行後續指令
  2. 判斷是否初始完成(根據變量mIsConstructionCompleted),若是初始化完成調用 processMsg 將消息拋給當前狀態執行
  3. 若是還沒有初始化,而且接受的是初始化命令 SM_INIT_CMD 將執行一次初始化操做
  4. 當命令執行結束後,執行 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 提供了幾個方法:

  1. quit: 執行完消息隊列中全部的消息後執行退出和清理操做
  2. quitNow: 拋棄掉消息隊列中的消息,直接執行退出和清理操做
  3. transitionToHaltingState: 拋棄掉消息隊列中的消息,直接執行退出,不作清理

從上面的表述中看,quit 相對 halt 操做來講更加的安全。這個 Threadinterceptstop 方法很相似,很好理解。上面咱們說到,退出狀態 HaltingStateQuittingState 是在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 源碼的含義,而且能基於它,開發更多個性化的功能

相關文章
相關標籤/搜索