如何在Swift中實現狀態機?

語言: CN / TW / HK

序章

什麼是狀態機?

我們還是直接使用wikiPedia上的定義:

A finite-state machine (FSM) or finite-state automaton (FSA, plural: automata), finite automaton, or simply a state machine, is a mathematical model of computation.

簡言之:我們通常稱作的狀態機是有限狀態機的簡稱,它是一種數學計算模型。

有限狀態機(也就是有限自動機)如果進行一個分類的話,分類如下:

這張圖只是看一個大概的分類,實際上這個有一個簡單的認知就可以了,因為我們要探討的種類主要就是DFA(Deterministic Finite Automaton):確定性有限狀態自動機。它也是最簡單的一種狀態機模型,下文中我們簡稱為狀態機的都是確定性有限狀態自動機。

注:分類本身是有各種分類方式的,這裡採取是以是否有輸出作為分類的前提,Wikipedia上的分類方式和此種分類不太一致,大家可以參考對比一下。

那麼確定性有限狀態自動機有哪些特點呢?簡單來講就是每一種輸入引起狀態的變化是確定的,即一個狀態對於同樣的輸入,只能有一種轉換規則。

從一個簡易的狀態機模型開始

很多人每天上班都要刷卡進出地鐵,我們就以有旋轉柵門的地鐵站閘機入口作為例子。

這個閘機口在開始的時候有一個“locked”的狀態, 在這個狀態下它並不會讓乘客旋轉柵欄通過進站口。當有人刷卡了,那麼這個閘機口的狀態就會變為“unlocked”,但是它並不會自己轉動,必須得等到有人推動旋轉柵欄通過閘機口,且在這之後閘機口的狀態會再次變為“locked”。

從上面的描述中可以看到這個閘機口總是處於兩種狀態之一:Locked 或者 Unlocked。同時也只有兩種轉化的觸發條件:刷卡 或者 通過旋轉柵欄。

接下來,我們可以通過圖形的方式來給這個閘機系統建模,使用圓角矩形代表狀態,使用箭頭代表狀態之間的轉換:

上述系統大概描述出了該系統是如何工作的,但是還有一些場景是這個系統無法明確定義的。比如刷了卡之後馬上又刷了一次卡,那麼狀態如何轉變呢?比如還沒有刷卡之前就有人要通過旋轉欄,那麼狀態是如何轉變呢?在上述的兩種情況,我們建的狀態機模型會保留在它的當前狀態。

同時我們還得給這個系統加上一個初始的狀態,這樣我們就可以繼續完善這個模型了:

有限狀態機的定義

上述模型就是一個簡單的狀態機模型,基於此,我們可以從中總結出這個系統幾個關鍵的特徵:

  • 這個系統包含有限的幾個狀態
  • 這個系統包含有限的幾個輸入(或者事件)可以觸發狀態之間的轉移
  • 這個系統有一個特定的初始狀態
  • 這個系統在某一時刻的 行為 取決於當前的狀態以及當前的輸入
  • 對於系統可能處於的每一個狀態,每一個輸入(或者事件) 都會有對於的行為

• The system must be describable by a finite set of states.

• The system must have a finite set of inputs and/or events that can trigger transitions between states.

• The system has a particular initial state

• The behavior of the system at a given point in time depends upon the current state and the input or event that occur at that time.

• For each state the system may be in, behavior is defined for each possible input or event.

這個時候我們的定義已經夠多了,有些理論,但是還是還不夠,作為工程師,我們通常需要使用更加正式的,或者說更數學的方式來描述該系統,一個有限自動機M通常被五元組(Σ, Q, q0, F, δ)所定義:

  • Σ is the set of symbols representing input to M
  • Q is the set of states of M
  • q0 ∈ Q is the start state of M
  • F ⊆ Q is the set of final states of M
  • δ : Q × Σ → Q is the transition function

簡單來說,該五元組為(輸入,狀態集,初始狀態,結果狀態集,轉換方法), 那麼這裡的轉換方法如何理解呢?它的引數就是當前狀態 以及 輸入 輸出就是 結果狀態 即 δ(q, x) = p。 一個有限狀態機 M 處於狀態 q 中,接受到 x 輸入,在處理完轉換過程中的Action之後,就會變為狀態 p 。

而對於這個轉化方法的抽象,在我們自定義狀態機的時候將尤為重要。

中章

實現狀態機最簡單的方法是使用列舉類和swtich-case語句來實現,但是我們今天不會使用這種方式,而是會嘗試用兩種不同的方式來定義一個狀態機。

如何實現狀態機 - 面向物件(OOP)

基本抽象

第一種方式是使用OOP的思路來完成一個通用的狀態機,那麼首當其衝的要考慮可以抽象出哪些類呢?結合上方的五元組,抽象如下:

``` protocol Event: Hashable { }

class State {

}

class StateMachine {

} ```

第一是事件:因為狀態的轉移是需要事件觸發的,所以事件集在這裡定義為一個Event協議,後面誰使用,誰定義相應的事件列舉即可。

第二是狀態:這裡把五元組中的Transition Function封裝進狀態類,直接由狀態類本身來管理轉換行為。

第三是狀態機:這個就類似我們MVC中的view controller,是狀態機的中樞,需要由它來管理狀態的切換。

以上就是我定義的狀態機模型的基建了,那麼我們整體的設計思路呢?

1、狀態機類接受事件觸發

2、將事件傳遞給當前狀態,由當前狀態觸發事件

3、事件觸發後:如果需要跳轉到其它狀態,當前狀態離開,而目標狀態進入

設計非常簡單,接下來看具體的實現。

狀態類

``` class State {

weak open var stateMachine: StateMachine<E>?

open func trigger(event: E) {}

open func enter() {
    stateMachine?.currentState = self
}

open func exit() { }

} ```

這裡來分析一下具體有什麼:

  • 狀態機屬性

當狀態機傳入事件的時候,當前狀態在進行事件判斷之後,需要呼叫狀態機以進入目標狀態。

  • 觸發條件

此處相當於註冊transition!當前狀態接受每一個輸入事件的轉移都將註冊在此處。

  • Transition 方法

具體來說就是當前狀態的離開方法,以及目標狀態的進入方法。

狀態機類

``` class StateMachine { typealias FSMState = State

open var currentState: FSMState!

private var states: [FSMState] = []

init(initialState: FSMState) {
    currentState = initialState
}

open func setupStates(_states: [FSMState]) {
    states = _states

    states.forEach { $0.stateMachine = self }
}

open func trigger(event: E) {
    currentState.trigger(event: event)
}

open func enter(_ stateClass: AnyClass) {
    states.forEach {
        if type(of: $0) == stateClass {
            $0.enter()
        }
    }
}

} ```

那麼接下來分析一下具體需要些什麼:

  • 構造方法

對於狀態機類,我們需要給它一個初始狀態,這樣的話,它的構造方法就需要一個初始狀態的引數。

  • 狀態集

因為狀態機需要管理狀態之間的轉移,那麼它就需要儲存這些狀態。

  • 觸發方法

外界事件觸發的時候,狀態機就觸發當前狀態的事件檢測方法,因為這裡,我們是讓每一個狀態內部來針對不同的輸入,來完成對應的轉換規則的。

  • 轉換狀態方法

當要進入新狀態時,依照上述的設計,我們需要通過狀態機類來處理進入新狀態的方法。

如何使用?

當我去思考哪些軟體可能會使用狀態機的時候,第一反應就是遊戲,因為遊戲中一個角色有各種各樣的狀態,如果是使用if-else的話那未免太過於繁瑣,這個時候,通過狀態機來控制狀態的變化一定是最優解,比如idle狀態,walk狀態,run狀態,attack狀態,hurt狀態等等等等。

所以這裡我們只用一個簡單的例子,通過兩個按鈕來控制角色walk和run的狀態切換。

所以使用的時候先定義事件

enum RoleEvent: Event { case clickRunButton case clickWalkButton }

再定義狀態,這裡我們定義RunState 和 WalkState:

``` class RunState: State { override func trigger(event: RoleEvent) { super.trigger(event: event)

    switch event {
    case .clickRunButton:
        break
    case .clickWalkButton:
        self.exit()
        stateMachine?.enter(WalkState.self)
    }
}

override func enter() {
    super.enter()

    NSLog("====run enter=====")
}

override func exit() {
    super.exit()

    NSLog("====run exit=====")
}

}

class WalkState: State { override func trigger(event: RoleEvent) { super.trigger(event: event)

    switch event {
    case .clickRunButton:
        self.exit()
        stateMachine?.enter(RunState.self)
    case .clickWalkButton:
        break
    }
}
override func enter() {
    super.enter()

    NSLog("====walk enter=====")
}

override func exit() {
    super.exit()

    NSLog("====walk exit=====")
}

} ```

最後就是初始化了:

``` func initialStateMachine() { let run = RunState.init() let walk = WalkState.init()

stateMachine = StateMachine.init(initialState: run)
stateMachine.setupStates(_states: [run, walk])

} ```

然後實際使用的時候,通過stateMachine觸發不同的事件即可:

stateMachine.trigger(event: .clickRunButton) // stateMachine.trigger(event: .clickWalkButton)

如何實現狀態機 - 函式式

這裡我參考了一個很輕量級的Swift狀態機框架Stateful來實現一個更swift風格的狀態機。

Transition

我們先拋開OOP的思路,回想一下確定性有限自動機的數學定義,有一個東西很關鍵,那就是transition function,所以這裡我們先把transition抽象為一個結構體,包含一個轉換規則的所有元素。

``` public typealias ExecutionBlock = (() -> Void) public struct Transition { public let event: Event public let source: State public let destination: State let preAction: ExecutionBlock? let postAction: ExecutionBlock?

public init(with event: Event,
            from: State,
            to: State,
            preBlock: ExecutionBlock?,
            postBlock: ExecutionBlock?) {
    self.event = event
    self.source = from
    self.destination = to
    self.preAction = preBlock
    self.postAction = postBlock
}

func executePreAction() {
    preAction?()
}

func executePostAction() {
    postAction?()
}

} ```

來看這一段程式碼的話,會發現,我們將觸發事件,開始狀態,結束狀態,以及轉換過程中需要執行的Action都封裝進了該Transition中。

接下來出個簡單的數學題,假設有3個狀態,4個事件,最多需要有多少個Transition例項呢?

State Machine

為了保證執行緒安全,我們將使用三個佇列,同時和上述狀態機一致,我們將設定一個初始化狀態:

``` class StateMachine { public var currentState: State { return { workingQueue.sync { return internalCurrentState } }() } private var internalCurrentState: State

private let lockQueue: DispatchQueue
private let workingQueue: DispatchQueue
private let callbackQueue: DispatchQueue

public init(initialState: State, callbackQueue: DispatchQueue? = nil) {
    self.internalCurrentState = initialState
    self.lockQueue = DispatchQueue(label: "com.statemachine.queue.lock")
    self.workingQueue = DispatchQueue(label: "com.statemachine.queue.working")
    self.callbackQueue = callbackQueue ?? .main
}

} ```

處理transition事件預設是在主佇列中,同時也可以自定義一個佇列並在初始化時傳入,即callbackQueue。

接下來要做的就是註冊狀態表了,這裡我們使用一個字典:

private var transitionsByEvent: [Event : [Transition<State, Event>]] = [:]

為什麼這裡用一個字典呢?

這裡應該很好理解,因為狀態機接受Event,然後結合當前State 以及 Event 去查詢有沒有對應的Transition,如果有就進行相關處理,如果沒有就不執行。同時因為每一個Event,對應的都是一組Transition(因為每一個狀態都可以接受該Event轉換為另一個狀態),那麼這個字典的Value值很自然的就應該是一個Transition陣列了。

註冊Transition

public func add(transition: Transition<State, Event>) { lockQueue.sync { if let transitions = self.transitionsByEvent[transition.event] { if (transitions.filter { return $0.source == transition.source }.count > 0) { assertionFailure("Transition with event '(transition.event)' and source '(transition.source)' already existing.") } self.transitionsByEvent[transition.event]?.append(transition) } else { self.transitionsByEvent[transition.event] = [transition] } } }

很簡單,使用序列佇列實現一個讀寫鎖,保證執行緒安全。

接受Event

當註冊完狀態表之後,剩下的就是接受相應Event,然後做出相應的處理了。要注意的一點就是類似前面OOP中呼叫的exit 方法 以及 enter 方法,初始化transition的時候也有相應的preAction和postAction, 呼叫方法的時候需要注意順序。

``` /// 接受事件,做出相應處理 /// - Parameters: /// - event: 觸發的事件 /// - execution: 狀態轉移的過程中需要執行的 操作 /// - callBack: 狀態轉移的回撥 public func process(event: Event, execution: (() -> Void)? = nil, callback: TransitionBlock? = nil) { var transitions: [Transition]? lockQueue.sync { transitions = self.transitionByEvent[event] }

    workingQueue.async {
        let performableTransitions = transitions?.filter { $0.source == self.internalCurrentState } ?? []

        if performableTransitions.count == 0 {
            self.callbackQueue.async { callback?(.failure) }
            return
        }

        assert(performableTransitions.count == 1, "Found multiple transitions with event '(event)' and source '(self.internalCurrentState)'.")

        let transition = performableTransitions.first!

        self.callbackQueue.async {
            transition.executePreAction()
        }

        self.callbackQueue.async {
            execution?()
        }

        self.internalCurrentState = transition.destination

        self.callbackQueue.async {
            transition.executePostAction()
        }

        self.callbackQueue.async {
            callback?(.success)
        }
    }
}

```

這裡接受Event觸發之後,首先當然是判斷該事件對應的Transition是否註冊,如果註冊失敗,那就執行失敗的callback,反之按照以下順序執行:

1、先執行該Transition中的preAction

2、然後是Process方法中傳入的執行操作Block

3、接著改變狀態機當前狀態

4、然後是執行該Transition中的postAction

5、最後是執行成功的回撥

當然這裡有一個要注意的地方,就是callbackQueue預設為主佇列,如果是初始化時傳入自定義佇列的話,最好是序列佇列!因為這樣可以確保preAction, postAction, callBack等等可以按照順序執行。

如何使用呢?

還是上述那個簡單的例子,但是這裡我們直接定義事件和狀態的列舉即可:

``` enum EventType { case clickWalkButton case clickRunButton }

enum StateType { case walk case run }

typealias StateMachineDefault = StateMachine typealias TransitionDefault = Transition ```

然後就是初始化狀態機了:

``` func initialStateMachine() { stateMachine = StateMachineDefault.init(initialState: .walk)

let a = TransitionDefault.init(with: .clickWalkButton, from: .run, to: .walk) {
    NSLog("準備走")
} postBlock: {
    NSLog("開始走了")
}

let b = TransitionDefault.init(with: .clickRunButton, from: .walk, to: .run) {
    NSLog("準備跑")
} postBlock: {
    NSLog("開始跑了")
}

stateMachine.add(transition: a)
stateMachine.add(transition: b)

} ```

最後就是直接傳入事件觸發即可:

stateMachine.process(event: .clickRunButton)

實際案例

在實際的開發中,我們經常會遇到鍵盤的各種狀態切換的場景,這種場景其實就很適合使用狀態機,因為一旦超過三種狀態,使用狀態機就會非常高效。

首先確定要鍵盤點選的幾個狀態:

/// 鍵盤狀態 enum KeyboardState: State { case prepareToEditText // 準備編輯 case prepareToRecord // 準備錄音 case editingText // 正在編輯 case showingPannel // 正在展示pannel }

然後是鍵盤點選的觸發事件:

/// 觸發事件 enum KeyboardEvent: Event { case clickSwitchButton // 點選切換按鈕 case clickMoreButton // 點選more按鈕 case tapTextField // 點選輸入框 case vcDidTapped // 點選控制器空白區域 }

那麼這個時候,在觸發事件和鍵盤狀態之間就出現了一個關聯關係,我們可以UML化這種關聯

| 觸發事件\初始狀態 | prepareToEdit | prepareToRecord | editingText | showingPannel | | --------------------- | ----------------- | ------------------- | --------------- | ----------------- | | clickSwitchButton | prepareToRecord | editingText | prepareToRecord | prepareToRecord | | clickMoreButton | showingPannel | showingPannel | showingPannel | prepareToEdit | | tapTextField | editingText | ** | \ | editingText | | vcDidTapped | * | * | prepareToEdit | prepareToEdit |

接下來就可以將不同狀態之間改變的事件新增入狀態機中:

``` stateMachine.listen(.clickSwitchButton, transform: .prepareToEditText, toState: .prepareToRecord) { [weak self] (transition) in

}

stateMachine.listen(.clickSwitchButton, transform: .prepareToRecord, toState: .editingText) { [weak self] (transition) in

} ...... ```

最後就是觸發了,當點選按鈕等互動時,即可呼叫該觸發條件:

``` //MARK: - //MARK: 點選事件 @objc func switchButtonClicked(_ sender: UIButton) { stateMachine.trigger(.clickSwitchButton) }

@objc func morefeaturesButtonClicked(_ sender: UIButton) {
    stateMachine.trigger(.clickMoreButton)
}

```

這樣一個鍵盤的切換事件就搞定了。這種方式比大量的判斷程式碼根據可讀性,而且可拓展性也更強,如果後續想要再增加一種狀態的話,那無非就是狀態機狀態多新增幾種狀態轉移的條件而已,這並不複雜。

終章

好了,狀態機介紹完了。那為什麼要寫一篇文章來介紹狀態機呢?主要原因是因為我覺得它在管理UI的多種狀態時非常高效,特別是當狀態數大於三個以上的時候,比如管理IM模組中鍵盤的各種狀態時。

而如果是僅僅通過列舉來管理多種狀態,那程式碼中就會有大量的當前狀態判斷的程式碼,很不易維護;如果要新增一個新的狀態,那麼原有狀態判斷的程式碼需要大量的修改,不滿足開閉原則(對修改關閉,對拓展開放)等等。

最後希望大家在合適的場景中使用狀態機吧。

引用

[1] Finite State Machines (PDF)

[2] A More Object-oriented State Machine

[3] wikipedia: Finite-state machine

[4] 評鑑Maze原始碼(2):GamePlayKit的狀態機

[5] Implementing a Finite State Machine Using C# in Unity

[6] Finite State Machine (Finite Automata)