开源学习之TransitionKit

TransitionKit是iOS中对状态机的一个实现,系统本身是有一个状态机的,不过是在GamePlayKit中的,但是针对平常的业务来说并不实用。

状态机

状态机有四个概念:

  • state,状态,一个状态机至少要有2个状态,
  • event,事件,事件就是执行某一个指令,触发某一个状态,会引起状态的变换
  • action,动作,事件发生之后要执行的动作,
  • transition,变换,一个状态变化到另一个状态的抽象过程

TransitionKit也根据以上几个概念,抽象出来4个类:

  • TKStateMachine,状态机的抽象类,负责管理状态的变化
  • TKState,事件的状态的抽象,在不同时间事物拥有不同的状态
  • TKEvent,引起事件状态变化的指令
  • TKTransition,状态与状态之间变化的过渡

在学习这个框架之前,先回忆一下初中学习的知识:水有三种状态:气态、液态、固态,一个比较严谨的说法是气态水(水蒸气)、液态水(常说的水)、固态水(冰)。他们之间可以相互转化,转化之间具体的描述为:

TKStateMachine

这个类是整个状态机的抽象,负责管理状态与状态之间的切换,因此状态机中会有多种状态,以及多种引起状态变化的事件。

根据上面对水的三种状态的描述,使用状态机进行抽象就是,一个状态机(水)有三个状态:gas、liquid、solid,六种事件:g2l(液化)、g2s(凝华)、l2s(凝固)、l2g(气化)、s2l(熔化)、s2g(升华)。

三种状态使用该库进行抽象的结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
TKState *gas = [TKState stateWithName:@"gas"];
[gas setDidEnterStateBlock:^(TKState *state, TKTransition *transition) {
/// TODO-gas
}];

TKState *liquid = [TKState stateWithName:@"liquid"];
[liquid setDidEnterStateBlock:^(TKState *state, TKTransition *transition) {
/// TODO-liquid
}];

TKState *solid = [TKState stateWithName:@"solid"];
[solid setDidEnterStateBlock:^(TKState *state, TKTransition *transition) {
/// TODO-solid
}];

而六种事件的抽象如下:

1
2
3
4
5
6
7
8
9
10
11
TKEvent *g2l = [TKEvent eventWithName:@"g2l" transitioningFromStates:@[gas] toState:liquid];

TKEvent *g2s = [TKEvent eventWithName:@"g2l" transitioningFromStates:@[gas] toState:solid];

TKEvent *l2s = [TKEvent eventWithName:@"l2s" transitioningFromStates:@[liquid] toState:solid];

TKEvent *l2g = [TKEvent eventWithName:@"l2g" transitioningFromStates:@[liquid] toState:gas];

TKEvent *s2l = [TKEvent eventWithName:@"s2l" transitioningFromStates:@[solid] toState:liquid];

TKEvent *s2g = [TKEvent eventWithName:@"s2g" transitioningFromStates:@[solid] toState:gas];

有了状态和事件,就可以去设置状态机了:

1
2
3
4
5
6
7
TKStateMachine *stateMachine = [[TKStateMachine alloc] init];

[stateMachine addStates:@[liquid, gas, solid]];
[stateMachine setInitialState:liquid];

[stateMachine addEvents:@[g2l, g2s, l2s, l2g, s2l, s2g]];
[stateMachine activate];

以上就构建起了水的状态机,前两部分创建对应的状态事件都是类的实例化,没有什么可探究的。在第三部分,我们创建了状态机,并且将状态和事件都放入状态机中,先看将所有状态都放入状态机以及设置初始状态中都做了什么。

addState

状态机添加状态的api有两个,一个是添加单个,一个是添加多个,不过核心api是添加单个状态。

有一点要先说明一下,状态机本身也有两种状态:活跃非活跃,只有在状态机处于非活跃状态下才可以对其添加state和event,只有在活跃状态下才可以执行event

在添加单个state的时候,会对要确保其是TKState或者其子类,并且没有被添加过,然后会放入到状态机的状态集合(mutableStates)中:

1
2
3
4
5
6
7
8
9
10
11
- (void)addState:(TKState *)state
{
TKRaiseIfActive();/// 只能在当前状态机不是活跃的时候才可以添加state

// 1.传入的state实例必须要是TKState或者其子类
// 2.state在状态机中没有被添加过

//
if (self.initialState == nil) self.initialState = state;
[self.mutableStates addObject:state];
}

addEvent

添加事件要复杂有点儿,先看其api:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
- (void)addEvent:(TKEvent *)event
{
// 1.状态机活跃判断、event非空判断

// 2.如果event有来源state,要确保当前状态机中已经包含这些state
if (event.sourceStates) {
for (TKState *state in event.sourceStates) {
if (! [self.mutableStates containsObject:state]) {
// 这里由于状态机的状态集合中没有event的来源state,抛出异常
}
}
}

// 3.确保当前状态机的states数组中包含 目的state
// 也就是说event所表示的状态变化只能在当前状态机的state之间
if (! [self.mutableStates containsObject:event.destinationState]) {
// 这里由于该event的目的state不包含在状态机的状态集合中,抛出异常
}
[self.mutableEvents addObject:event];
}

和添加状态一样,添加事件也需要在状态机处于非活跃状态下进行。由于事件表示的是一个状态变化到另一个状态的原因,所以其来源state目的state都应该在状态机的状态集合中存在,否则就属于逻辑出错, 会抛出异常。

activate

开始执行状态机,等待对应的事件来临从而改变状态,其内部将一个表示是否活跃的布尔值进行修改,以及修改当前状态变量(currentState),并且使用递归锁进行锁定,在修改当前状态的时候会调用block将状态传递出去:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 使状态机变活跃,内部会使用锁来确保数据的变化不混乱
- (void)activate
{
// 如果状态机已经变活跃了,就不能再调用该方法,属于逻辑错误
if (self.isActive) [NSException raise:NSInternalInconsistencyException format:@"The state machine has already been activated."];

[self.lock lock];

self.active = YES;

// 节点事件回调,方便外部处理逻辑
if (self.initialState.willEnterStateBlock) self.initialState.willEnterStateBlock(self.initialState, nil);

// 由于状态机是从冷却变为活跃的,所以一定是从初始状态开始的
self.currentState = self.initialState;

// 节点事件回调,方便外部处理逻辑
if (self.initialState.didEnterStateBlock) self.initialState.didEnterStateBlock(self.initialState, nil);

[self.lock unlock];
}

fireEvent

经过以上步骤,一个状态机就创建好了,并且处于活跃状态。初始状态机内部的状态改变的原因是状态机收到了某些事件

1
[stateMachine fireEvent:l2g userInfo:nil error:nil];

调用这个代码会执行上面的 /// TODO-gas 回调,由于状态机的当前状态为liquid,所以能引起状态发生改变的事件只有:l2sl2g这两个,这个完全符合常识,也完全符合我们这个状态机的规则。

这个api的实现很长,下面会分为三部分来进行讲解,由于这里涉及到对状态机里面集合的get操作,所以会加锁,加锁这部分独立于三部分。

1.获取一个正确的event

由于第一个参数是TKEvent实例或者event的名字(“g2l”、“g2s”、、),所以这里会先通过这个代码获取到合法的event实例对象

1
TKEvent *event = [eventOrEventName isKindOfClass:[TKEvent class]] ? eventOrEventName : [self eventNamed:eventOrEventName];

然后要确保这个event的源state存在,并且当前状态机的状态存在于event的来源state中,如果不存在则说明不能响应这个event来改变状态:

1
2
3
4
5
6
7
8
if (event.sourceStates != nil &&
![event.sourceStates containsObject:self.currentState]) {
NSString *failureReason;//
NSDictionary *userInfo = @{ NSLocalizedDescriptionKey: @"The event cannot be fired from the current state.", NSLocalizedFailureReasonErrorKey: failureReason };
if (error) *error = [NSError errorWithDomain:TKErrorDomain code:TKInvalidTransitionError userInfo:userInfo];
[self.lock unlock];
return NO;
}

2.抽象状态切换

在上一个部分之后,已经获得了一个可以用来改变状态的event,接下来就是要执行状态的修改了,但是在执行状态的修改之前还有一件事要做。

由于event是引起一系列状态到另一个状态改变的抽象,并不能表示两个具体状态之间的切换,因此该库使用TKTransition这个类来抽象两个状态之间的转换。

在这一部分,会通过上面的参数来创建一个TKTransition实例对象,然后通过event的一个block回调来决定是否执行该切换:

1
2
3
4
5
6
7
8
9
10
11
12
TKTransition *transition = [TKTransition transitionForEvent:event fromState:self.currentState inStateMachine:self userInfo:userInfo];

if (event.shouldFireEventBlock) {
if (! event.shouldFireEventBlock(event, transition)) {

// 源代码中这里有一些用于创建error的代码,但是其并没有用,这里忽略

[self.lock unlock];

return NO;
}
}

3.执行切换状态

在经历了一些列艰难险阻之后,状态机终于可以切换状态了。

状态切换,就是要将状态机的currentState属性修改为event的destinationState,因此这里先取出来这两个变量。本来直接将event的destinationState设置给状态机的currentState即可,但是该库提供了很丰富的事件回调,让外部可以有更加细的粒度来处理事件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
TKState *oldState = self.currentState;
TKState *newState = event.destinationState;

if (event.willFireEventBlock) event.willFireEventBlock(event, transition);

if (oldState.willExitStateBlock) oldState.willExitStateBlock(oldState, transition);
if (newState.willEnterStateBlock) newState.willEnterStateBlock(newState, transition);
self.currentState = newState;

NSMutableDictionary *notificationInfo = [userInfo mutableCopy] ?: [NSMutableDictionary dictionary];

// 省略对notiInfo的设置

[[NSNotificationCenter defaultCenter] postNotificationName:TKStateMachineDidChangeStateNotification object:self userInfo:notificationInfo];

if (oldState.didExitStateBlock) oldState.didExitStateBlock(oldState, transition);
if (newState.didEnterStateBlock) newState.didEnterStateBlock(newState, transition);

if (event.didFireEventBlock) event.didFireEventBlock(event, transition);
[self.lock unlock];

至此,状态已经完成了改变。

以上也就是整个状态机框架的实现原理。

参考文章

  • 参考文章列表