美文网首页每周一博
状态模式和状态机

状态模式和状态机

作者: 健身营养爱好者 | 来源:发表于2018-12-01 16:32 被阅读21次

    前言

    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状态机的源码我会在下一篇文章中介绍,感谢阅读,我们下周再见。

    相关文章

      网友评论

        本文标题:状态模式和状态机

        本文链接:https://www.haomeiwen.com/subject/zafifqtx.html