状态模式和状态机

前言

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342

推荐阅读更多精彩内容