状态模式和状态机

前言

HI,欢迎来到裴智飞的《每周一博》。今天是十一月第五周,我给大家介绍一下安卓系统中的状态机。为什么会介绍状态机呢?因为我在工作过程中遇到了问题,需要走读系统WiFi的源码,而WiFi的上层实现中状态机占了很大的比例,为了帮助理解WiFi工作过程,需要先学习一下状态机。

一. 状态模式

状态机是状态模式的一种应用,我们先来看一下状态模式。状态模式是一种行为模式,在不同的状态下有不同的行为。状态模式的行为是平行的,不可替换的,比如电梯状态可以分为开门状态,关门状态,运行中状态。状态模式把对象的行为包装在不同的状态对象里,对象的行为取决于它的状态,当一个对象内部状态改变时,行为也随之改变。

举个简单的例子,看微博时点击转发按钮,如果登录了就会跳转到转发界面,如果没登录就会跳转到登录界面,这就是转发行为在用户登录和未登录状态下的不同。再比如电视有开和关两种状态,有调音量,换台,开机,关机等行为,当电视处于关闭状态,只会响应开机指令,而电视处于打开状态,则会响应调音量,换台,关机的指令。通常我们会用if-else来判断电视状态,然后去执行相关指令,但是使用了状态模式之后就不用再写那么多if-else来进行判断了。我们就以此为例,写一个状态模式的代码。

定义抽象电视状态接口,面相抽象,避免依赖具体实现;

public interface TvState{
    public void nextChannerl();
    public void prevChannerl();
    public void turnUp();
    public void turnDown();
}

定义电视关机状态,它是抽象电视状态的一个具体实现,在关机状态下什么也不操作;

public class PowerOffState implements TvState{
    public void nextChannel(){}
    public void prevChannel(){}
    public void turnUp(){}
    public void turnDown(){}
}

定义电视开机状态,它是抽象电视状态的一个具体实现,在开机状态下可以响应各项指令;

public class PowerOnState implements TvState{
    public void nextChannel(){
        System.out.println("下一频道");
    }
    public void prevChannel(){
        System.out.println("上一频道");
    }
    public void turnUp(){
        System.out.println("调高音量");
    }
    public void turnDown(){
        System.out.println("调低音量"); 
    }
}

定义状态生效的环境类,用来切换状态,可以理解为状态的代理;

public class TvController {
    TvState mTvState;

    public void setTvState(TvStete tvState){
        mTvState=tvState;
    }

    public void powerOn(){
        setTvState(new PowerOnState());
        System.out.println("开机啦");
    }

    public void powerOff(){
        setTvState(new PowerOffState());
        System.out.println("关机啦");
    }

    public void nextChannel(){
        mTvState.nextChannel();
    }

    public void prevChannel(){
        mTvState.prevChannel();
    }

    public void turnUp(){
        mTvState.turnUp();
    }

    public void turnDown(){
        mTvState.turnDown();
    }    
}

编写测试类进行测试;

public class Client{
    public static void main(String[] args){
        TvController tvController=new TvController();
        tvController.powerOn();
        tvController.nextChannel();
        tvController.turnUp();        
        tvController.powerOff();
        //调高音量,此时不会生效
        tvController.turnUp();
    }
}

以上就是状态模式的一个简单实现,虽然类增加了不少,但是逻辑清晰简单,如果不用状态模式,我们的代码应该是下面的样子,有很多if-else;

    public void prevChannel(){
        if(POWER_ON){
              System.out.println("上一频道"); 
        }
    }
    public void turnUp(){
        if(POWER_ON){
              System.out.println("调高音量"); 
        }
    }

二. 状态机

接下来我们看下状态机的原理。状态机是一组状态的集合,是协调相关信号动作,完成特定操作的控制中心。状态机可归纳为4个要素,即当前状态,条件,动作,下个状态。这样的归纳主要出于对状态机的内在因果关系的考虑,当前状态和条件是因,动作和下个状态是果。对于复杂些的逻辑,用状态机会有助于代码比较清晰,容易维护和调试

状态机的用法一般是这样的,发生了某个事件后,根据当前状态,决定执行的动作,并设置下一个状态。

1. 状态声明

接下来我们看下安卓里面状态机的具体实现,首先是状态的抽象接口IState和具体实现State。

public interface IState {
    static final boolean HANDLED = true;
    static final boolean NOT_HANDLED = false;
    void enter();
    void exit();
    boolean processMessage(Message msg);
    String getName();
}

public class State implements IState {
  
    protected State() {}

    @Override
    public void enter() {}
 
    @Override
    public void exit() { }
 
    @Override
    public boolean processMessage(Message msg) {
        return false;
    }

    @Override
    public String getName() {
        String name = getClass().getName();
        int lastDollar = name.lastIndexOf('$');
        return name.substring(lastDollar + 1);
    }
}

状态定义了三个主要的方法,enter,exit,processMessage,状态机中的每一个状态是State的具体实现,enter/exit 等价于类的构造方法和销毁方法,processMessage方法用来处理消息,返回true即为已处理。接下来我们看下状态机StateMachine的实现;

2. 状态机初始化

StateMachine在初始化的时候创建了Looper和HandlerThread,内部维护了一个SmHandler对象,通过Handler机制来传递消息,SmHandler是消息处理派发和状态控制切换的核心,运行在单独的线程上。

    mSmThread = new HandlerThread(name);
    mSmThread.start();
    Looper looper = mSmThread.getLooper();
    mSmHandler = new SmHandler(looper, this);

接下来我们看下SmHandler这个重要的内部类。首先它提供了addState来添加状态。状态机中的每个状态使用State来封装,对于每个状态的信息又采用StateInfo来描述;

private final StateInfo addState(State state, State parent) {
    if (mDbg) {
        Log.d(TAG, "addStateInternal: E state=" + state.getName()
                + ",parent=" + ((parent == null) ? "" : parent.getName()));
    }
    StateInfo parentStateInfo = null;
    if (parent != null) {
        parentStateInfo = mStateInfo.get(parent);
        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) Log.d(TAG, "addStateInternal: X stateInfo: " + stateInfo);
    return stateInfo;
}

状态添加过程其实就是为每个State创建相应的StateInfo对象,通过该对象来建立各个状态之间的关系,并以一个State-StateInfo键值对的方式保存到mStateInfo这个Hash表中,它用来保存State Machine中的所有State;

state是当前状态,parent是父状态,经过这样一系列的添加,就可以把所有状态按照树形层次结构进行组织。

sm.addState(S0);
sm.addState(S1,S0);
sm.addState(S2,S0);
sm.addState(S3,S1);
sm.addState(S4,S1);
sm.addState(S5,S2);
sm.addState(S6,S2);
sm.addState(S7,S2);

这样便生成了下图中的树。

接着我们需要设置初始状态,这个不要求是根元素

setInitialState(S4);  
3. 状态机启动

当向状态机中添加完所有状态时,通过函数start来启动状态机。

public void start() {
    // mSmHandler can be null if the state machine has quit.
    if (mSmHandler == null) return;
    mSmHandler.completeConstruction();
}
private final void completeConstruction() {
    if (mDbg) Log.d(TAG, "completeConstruction: E");
    //查找状态树的深度
    int maxDepth = 0;
    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) Log.d(TAG, "completeConstruction: maxDepth=" + maxDepth);
    //创建mStateStack,mTempStateStack状态栈
    mStateStack = new StateInfo[maxDepth];
    mTempStateStack = new StateInfo[maxDepth];
    //设置状态堆栈
    setupInitialStateStack();
    /** Sending SM_INIT_CMD message to invoke enter methods asynchronously */
    sendMessageAtFrontOfQueue(obtainMessage(SM_INIT_CMD, mSmHandlerObj));
    if (mDbg) Log.d(TAG, "completeConstruction: X");
}

在completeConstruction方法里先计算状态树的最大深度:
A. 遍历状态树中的所有节点;
B. 以每一个节点为起始点,根据节点父子关系查找到根节点;
C. 累计起始节点到根节点之间的节点个数;
D. 查找最大的节点个数,根据查找到的树的最大节点个数来创建两个状态堆栈,并调用函数setupInitialStateStack来填充该堆栈;

private final void setupInitialStateStack() {
    //在mStateInfo中取得初始状态mInitialState对应的StateInfo
    StateInfo curStateInfo = mStateInfo.get(mInitialState);
    //从初始状态mInitialState开始根据父子关系填充mTempStateStack堆栈
    for (mTempStateStackCount = 0; curStateInfo != null; mTempStateStackCount++) {
        mTempStateStack[mTempStateStackCount] = curStateInfo;
        curStateInfo = curStateInfo.parentStateInfo;
    }
    // Empty the StateStack
    mStateStackTopIndex = -1;
    //将mTempStateStack中的状态按反序方式移动到mStateStack栈中
    moveTempStateStackToStateStack();
}

mStateStack和mTempStateStack是一个数组栈,用于保存状态机中的链式状态关系。

从图中可以看出当初始状态为S4时,保存到mTempStateStack的节点为:
mTempStateStack={S4,S1,S0}
mTempStateStackCount = 3;
mStateStackTopIndex = -1;
然后调用函数moveTempStateStackToStateStack将节点以反序方式保存到mStateStack中;

private final int moveTempStateStackToStateStack() {
    //startingIndex= 0
    int startingIndex = mStateStackTopIndex + 1;
    int i = mTempStateStackCount - 1;
    int j = startingIndex;
    while (i >= 0) {
        if (mDbg) Log.d(TAG, "moveTempStackToStateStack: i=" + i + ",j=" + j);
        mStateStack[j] = mTempStateStack[i];
        j += 1;
        i -= 1;
    }
    mStateStackTopIndex = j - 1;
    return startingIndex;
}

mStateStack={S0,S1,S4}
mStateStackTopIndex = 2
初始化完状态栈后,SmHandler将向消息循环中发送一个SM_INIT_CMD消息;

sendMessageAtFrontOfQueue(obtainMessage(SM_INIT_CMD, mSmHandlerObj))

处理该消息的方法如下;

else if (!mIsConstructionCompleted &&(mMsg.what == SM_INIT_CMD) && (mMsg.obj == mSmHandlerObj)) {
    mIsConstructionCompleted = true;
    invokeEnterMethods(0);
}
performTransitions();

消息处理过程首先调用invokeEnterMethods函数将mStateStack栈中的所有状态设置为激活状态,同时调用每一个状态的enter函数;

private final void invokeEnterMethods(int stateStackEnteringIndex) {
    for (int i = stateStackEnteringIndex; i <= mStateStackTopIndex; i++) {
        if (mDbg) Log.d(TAG, "invokeEnterMethods: " + mStateStack[i].state.getName());
        mStateStack[i].state.enter();
        mStateStack[i].active = true;
    }
}

最后调用performTransitions函数来切换状态,同时设置mIsConstructionCompleted为true,表示状态机已经启动完成,SmHandler在以后的消息处理过程中就不在重新启动状态机了。

4. 状态切换

SmHandler在处理每个消息时都会调用performTransitions来检查状态切换。

private synchronized void performTransitions() {
  while (mDestState != null){
    //当前状态切换了 存在于mStateStack中的State需要改变
    //仍然按照链式父子关系来存储
    //先从当前状态S3找到 最近的被激活的parent状态S0
    //未被激活的全部保存起来(S3,S1) 返回S0
    StateInfo commonStateInfo = setupTempStateStackWithStatesToEnter(destState);
    //将mStateStack中 不属于当前状态(S3),
    //关系链上的State(S5,S2)退出(执行exit方法)
    invokeExitMethods(commonStateInfo);
    //将S3关系链 加入到栈中(S3,S1)
    int stateStackEnteringIndex = moveTempStateStackToStateStack();
    //将新加入到mStateStack中 未被激活的State激活(S3,S1)
    invokeEnterMethods(stateStackEnteringIndex);
    //将延迟的消息移动到消息队列的前面,以便快速得到处理               
    moveDeferredMessageAtFrontOfQueue();
  }
}

初始时mDestState是空的,当我们调用transitionTo切换状态的时候,mDestState就会被赋值,这里只是简单地设置了mDestState变量,并未真正更新状态栈mStateStack。

private final void transitionTo(IState destState) {
    mDestState = (State) destState;
}

以上图中,初始状态为S4,现在我们要切换到S7。前面介绍了保存在mStateStack数组中的节点为:
mStateStack={S0,S1,S4}
mStateStackTopIndex = 2
这是以初始状态节点为起点遍历节点树得到的节点链表。

现在要切换到S7状态节点,则以S7为起始节点,同样遍历状态节点树,查找未激活的所有节点,并保存到mTempStateStack数组中
mTempStateStack={S7,S2,S0}
mTempStateStackCount = 3

接着调用mStateStack中除S0节点外的其他所有节点的exit函数,并且将每个状态节点设置为未激活状态,因此S4,S1被设置为未激活状态;将切换后的状态节点链表mTempStateStack移动到mStateStack
mStateStack={S0,S2,S7}
mStateStackTopIndex = 2
并调用节点S2,S7的enter函数,同时设置为激活状态。

理解了整个状态切换过程后,就能更好地理解代码,首先根据目标状态建立状态节点链路表。

private final StateInfo setupTempStateStackWithStatesToEnter(State destState) {
    mTempStateStackCount = 0;
    StateInfo curStateInfo = mStateInfo.get(destState);
    do {
        mTempStateStack[mTempStateStackCount++] = curStateInfo;
        if (curStateInfo != null) {
            curStateInfo = curStateInfo.parentStateInfo;
        }
    } while ((curStateInfo != null) && !curStateInfo.active);
    return curStateInfo;
}

然后弹出mStateStack中保存的原始状态;

private final void invokeExitMethods(StateInfo commonStateInfo) {
    while ((mStateStackTopIndex >= 0) &&
            (mStateStack[mStateStackTopIndex] != commonStateInfo)) {
        State curState = mStateStack[mStateStackTopIndex].state;
        if (mDbg) Log.d(TAG, "invokeExitMethods: " + curState.getName());
        curState.exit();
        mStateStack[mStateStackTopIndex].active = false;
        mStateStackTopIndex -= 1;
    }
}

将新建立的状态节点链表保存到mStateStack栈中;

private final int moveTempStateStackToStateStack() {
    //startingIndex= 0
    int startingIndex = mStateStackTopIndex + 1;
    int i = mTempStateStackCount - 1;
    int j = startingIndex;
    while (i >= 0) {
        if (mDbg) Log.d(TAG, "moveTempStackToStateStack: i=" + i + ",j=" + j);
        mStateStack[j] = mTempStateStack[i];
        j += 1;
        i -= 1;
    }
    mStateStackTopIndex = j - 1;
    return startingIndex;
}

初始化入栈的所有新状态,并设置为激活状态;

private final void invokeEnterMethods(int stateStackEnteringIndex) {
    for (int i = stateStackEnteringIndex; i <= mStateStackTopIndex; i++) {
        if (mDbg) Log.d(TAG, "invokeEnterMethods: " + mStateStack[i].state.getName());
        mStateStack[i].state.enter();
        mStateStack[i].active = true;
    }
}

SmHandler在每次处理消息时都会自动更新一次mStateStack,无论mDestState变量值是否改变,所以目标状态的设置与状态栈的更新是异步的。

5. 消息处理

StateMachine处理的核心就是SmHandler,就是一个Handler,运行在单独线程中。Handler是用来异步处理派发消息,这里使用Handler管理各个状态,派发消息处理到各个状态中去执行。StateMachine提供了sendMessage来发送消息,SmHandler会接受并处理消息。

public final void sendMessage(int what) {
    // mSmHandler can be null if the state machine has quit.
    if (mSmHandler == null) return;
    mSmHandler.sendMessage(obtainMessage(what));
}

SmHandler处理消息的过程如下;

public final void handleMessage(Message msg) {
    /** Save the current message */
    mMsg = msg;
    if (mIsConstructionCompleted) {
        //派发当前消息到state中去处理
        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);
    }
    //消息处理完毕更新mStateStack
    performTransitions();
}

变量mIsConstructionCompleted在状态机启动完成后被设置为true,因此这里将调用processMsg函数来完成消息处理。

private final void processMsg(Message msg) {
    StateInfo curStateInfo = mStateStack[mStateStackTopIndex];
    //如果当前状态未处理该消息
    while (!curStateInfo.state.processMessage(msg)) {
        //将消息传给当前状态的父节点处理
        curStateInfo = curStateInfo.parentStateInfo;
        if (curStateInfo == null) {
             //当前状态无父几点,则丢弃该消息
            mSm.unhandledMessage(msg);
            if (isQuit(msg)) {
                transitionTo(mQuittingState);
            }
            break;
        }
    }
    //记录处理过的消息
    if (mSm.recordProcessedMessage(msg)) {
        if (curStateInfo != null) {
            State orgState = mStateStack[mStateStackTopIndex].state;
            mProcessedMessages.add(msg, mSm.getMessageInfo(msg), curStateInfo.state,orgState);
        } else {
            mProcessedMessages.add(msg, mSm.getMessageInfo(msg), null, null);
        }
    }
}

消息处理过程是从mStateStack栈顶派发到栈底,直到该消息被处理。然后通过invokeEnterMethods调用新状态的enter方法,最后再调用performTransitions来切换状态,这个函数在上面已经分析过了。

6. 常用方法

状态机里常用的方法有以下6个:
start:用于启动状态机
addState:建立状态树
transitionTo:用于设置新状态
sendMessage:用于发送消息,然后当前状态会执行proccessMessage方法来处理消息
deferMessage:推迟消息,该消息将在下一个状态执行

三. WiFi状态机

接下来我们实际看一下WiFi工作中的状态机,WifiStateMachine通过addState构建的状态树如下;


状态比较多,每个状态的proccessMessage消息都不相同,因此在分析源码时需要看到当前是什么状态,然后再去看它的proccessMessage方法,比如处理扫描消息的主要就是DriverStartedState这个状态;

状态之间的切换是一个一个来的,不是直接跳的,比如从初始化状态到连接状态需要走过树上相关联的状态,依次执行他们的生命周期,具体的关于WiFi状态机的源码我会在下一篇文章中介绍,感谢阅读,我们下周再见。

你可能感兴趣的:(状态模式和状态机)