前文回顾
这一篇会进一步介绍一些高级应用场景;
State的层级关系
State切换时的调用时序
根据之前的了解,我们知道在State
切入时会调用其enter()
方法,在退出时会调用其exit()
方法;
但是那都是基于评级关系而言的,当State
存在层级关系的时候,enter()
/exit()
的调用时序是否会存在差异呢?
为了探究这点,我们先假设有如下State层级关系的状态机:
然后,我们设置State1
为初始状态,并沿着如下顺序进行状态切换:
State1
-> State2
-> State3
-> State1
其调用时序如下:
虚线箭头表示切换的方向,虚线旁的调用栈即为调用时序;
并列关系的状态切换规律已经理清了,那么我们接下来看看层级关系内的状态切换:
从结果来看,规律似乎是:“状态A到状态B切换时的调用栈就是从状态A回到空节点,再切到状态B的最短路线” (这里我划掉了,可见是错误的,要看结论的请直接移步文档末尾;)
State1
->State2
->State3
/State4
的现象均如此,但如果在此时,我们从State4
切换到State3
,就会发现不满足:
(下图省略了已经讨论完毕的State1
与State2
,并再添加了一个State5
,作为State4
的子状态)
可以看到,具有层级关系的状态之间切换会有如下两个特征:
- 从父状态进入子状态,直接依次向下调用子状态的
enter()
,直到目标状态为止; - 从子状态进入父状态,会从自身开始,依次向上调用
exit()
,直到调用目标状态的exit()
后再调入目标状态的enter()
;
可能这里会让人很疑惑:为什么进入父状态时需要先退出再进入;
为了更好理解,这里再引入一个State6
,并指定其为State3
的子状态,即与State4
并列:
其各个状态切换的调用栈如下图:
除去State4
与State5
的状态切换情况(上面已经讨论过了),其余状态切换的调用栈如下图:
可见,State3
下的两个子状态:State4
与State6
,两者切换时并不会调用State3
的exit()
;因此可以确定,调用栈并不是单纯的路线顺序;
根据分析代码,我注意到一段关键代码:
private void performTransitions(State msgProcessedState, Message msg) {
...
/**
* Determine the states to exit and enter and return the
* common ancestor state of the enter/exit states. Then
* invoke the exit methods then the enter methods.
*/
StateInfo commonStateInfo = setupTempStateStackWithStatesToEnter(destState);
// flag is cleared in invokeEnterMethods before entering the target state
mTransitionInProgress = true;
invokeExitMethods(commonStateInfo);
...
}
这里告诉了我状态切换的一个向上查找的标准:退到非两个状态本身的公共父状态为止
两个重点:
- 切换前后两个状态本身不能作为这里的公共父状态;
- 找到的这个公共父状态不会退出;
举例:
State5
->State4
State4
为State5
的父状态,但是由于上面提到,切换前后两个状态本身不能作为公共父状态,因此还需要向上找,即State3
为State5
与State4
的公共父状态;- 因此,调用栈为:
State5.exit()
->State4.exit()
-> (State3
,但不会走任何回调) ->State4.enter()
,与上面结论一致;
State5
->State6
State5
与State6
没有直接联系,因此向上寻找,可以发现最早将二者联系起来的是State3,即为公共父状态;- 因此,调用栈为:
State5.exit()
->State4.exit()
-> (State3
,但不会走任何回调) ->State6.enter()
,与上面结论一致;
State4
->State6
- 显而易见,
State4
与State6
的公共父状态为State3
; - 因此,调用栈为:
State4.exit()
-> (State3
,但不会走任何回调) ->State6.enter()
,与上面结论一致;
- 显而易见,
结论: 一个比较容易的记忆方法就是:
"状态A到状态B切换时的调用栈就是从状态A回到两者的公共父状态,再走到状态B的最短路线"
State切换时内部实现
上面已经介绍了现象即规律,不想深究的可以就此打住;
但是为了将状态机吃透,便于后续开发时可以有更多参考的模型,因此这里会着重介绍下这个机制实现所依赖的数据结构:
private StateInfo mStateStack[];
private int mStateStackTopIndex = -1;
private StateInfo mTempStateStack[];
private int mTempStateStackCount;
前面已经介绍过,状态切换本质是performTransitions()
方法实现的,其核心实现截取如下:
//mDestState不为null表示有外部调用transitionTo
//此处用本地变量是因为这是在Handler内部异步执行,
//而在接下来时间内,mDestState可能会再次改变
State destState = mDestState;
if (destState != null) {
while (true) {
//求得切换前后两个状态的公共父状态;
StateInfo commonStateInfo = setupTempStateStackWithStatesToEnter(destState);
//这个状态标记会在invokeEnterMethods中清除,在清除之前,如果发生transitionTo的调用,会通过Log.wtf打印信息;(对于eng版本就会crash)
mTransitionInProgress = true;
//从当前状态一直向上调用exit()方法,直到上方求得的公共父状态为止(不包含)
invokeExitMethods(commonStateInfo);
//更新状态栈mStateStack
int stateStackEnteringIndex = moveTempStateStackToStateStack();
//从状态栈第一个成员变量active不为true的状态开始,调用enter()方法;
invokeEnterMethods(stateStackEnteringIndex);
//由于状态发生变化,因此将待处理的消息移至消息队列最前端,防止被后续消息插队导致时序错误;
moveDeferredMessageAtFrontOfQueue();
//如果本地变量destState与成员变量mDestState不相等,则表示在这之前外部调用
//transitionTo再次被调用;重新赋值destState并重复执行上述while语句;
if (destState != mDestState) {
// A new mDestState so continue looping
destState = mDestState;
} else {
// No change in mDestState so we're done
break;
}
}
//状态切换完毕,置空mDestState
mDestState = null;
}
这里稍微展开一点,依次看看setupTempStateStackWithStatesToEnter
和moveTempStateStackToStateStack
的实现:
private final StateInfo setupTempStateStackWithStatesToEnter(State destState) {
mTempStateStackCount = 0;
//首先获取目标状态,并从此逆向查询
StateInfo curStateInfo = mStateInfo.get(destState);
do {
//将查询到的结果添加到mTempStateStack中
mTempStateStack[mTempStateStackCount++] = curStateInfo;
curStateInfo = curStateInfo.parentStateInfo;
//直到没有父状态,或者找到第一个活跃的父状态;这里就对应上面提到的
//“非两个状态本身的公共父状态”
} while ((curStateInfo != null) && !curStateInfo.active);
//因此返回的就是上面所谓的“非两个状态本身的公共父状态”
return curStateInfo;
}
private final int moveTempStateStackToStateStack() {
//简单来说,是将mTempStateStack反序录入到mStateStack中;
//注意,mStateStackTopIndex在invokeExitMethods时已经退到公共父状态对应的下标处了,此时
//并非指代当前状态的在mStateStack中的下标;
int startingIndex = mStateStackTopIndex + 1;
//因为是反序录入,因此从mTempStateStack的末尾开始;
int i = mTempStateStackCount - 1;
int j = startingIndex;
while (i >= 0) {
mStateStack[j] = mTempStateStack[i];
j += 1;
i -= 1;
}
//录入完成后的mStateStackTopIndex 应为目标状态在mStateStack中的下标;即
//mStateStack最后一个有效元素的下标;
mStateStackTopIndex = j - 1;
//返回的startingIndex 代表公共父状态后的第一个子状态在mStateStack中的下标,
//后续invokeEnterMethods方法中,会以此为起始点调用后面所有State的enter()方法,
//完成上方《State切换时的调用时序》中讨论的调用栈;
return startingIndex;
}
小结:
虽然在上方 《State切换时的调用时序》 章节中,我的结论是:
"状态A到状态B切换时的调用栈就是从状态A回到两者的公共父状态,再走到状态B的最短路线"
但是从这里的代码来看,实际上逻辑并非现象看上去那么线性,具体说来是经过了这几个步骤:
- 找到当前状态与目标状态的公共父状态;--
setupTempStateStackWithStatesToEnter()
- 将公共父状态到目标状态的层级关系录入mTempStateStack;--
setupTempStateStackWithStatesToEnter()
- 从当前状态开始,依次执行exit()方法,直到退到公共父状态(公共父状态不会调用exit(),即不会退出);--
invokeExitMethods()
- 从公共父状态在mStateStack的下标为止开始,将mTempStateStack中的状态栈反序追加到mStateStack中;--
moveTempStateStackToStateStack()
- mStateStack追加完成后,再以公共父状态在mStateStack的下标为止开始至mStateStack有效数据末尾,依次执行State的enter()方法;--
invokeEnterMethods()
后记
暂时就写这么多吧,实际上StateMachine
还有很多细节可以挖掘,包括日志的记录,并发的处理,以及processMessage()
方法返回值决定是否调用父状态的processMessage()
等。但这些都不会影响我们对StateMachine
工作机制的理解,因此这里就暂不展开了。
感兴趣的可以自行了解。