iOS 状态机 应用 TransitionKit
iOS 状态机
状态机 的 概念 可以从网上搜索。
此文章主要分析TransitionKit
TransitionKit 在iOS开发中的作用
- 适用于流程化,状态线性切换的场景;
- 状态切换有依赖和前提条件;
- 状态切换只能由特定状态切换到特定状态,不能随意切换,也不是可逆的;
- 可将特定状态下的业务逻辑集中到一起管理
先来看看应用,此处还是以直播应用举例子。比如播放器的状态,有 播放 暂停 加载中 加载错误这些。
一开始肯定是加载中,加载中的状态 可以 切换到 加载错误 和 播放 播放 可以切换到 加载 错误 暂停。暂停可以切换到 播放。
每个状态和下一个状态的依赖是有顺序的。每一个状态要展示的样子也有很大不同。接下来看我们的应用。
- (void)setupStateMachine
{
self.stateMachine = [[TKStateMachine alloc] init];
__weak typeof(self) weakSelf = self;
///加载中 状态
TKState *loading = [TKState stateWithName:kLoading];
[loading setDidEnterStateBlock:^(TKState *state, TKTransition *transition) {
/// TODO
}];
///播放状态
TKState *playing = [TKState stateWithName:kPlaying];
[playing setDidEnterStateBlock:^(TKState *state, TKTransition *transition) {
/// TODO
}];
[playing setDidExitStateBlock:^(TKState *state, TKTransition *transition) {
/// TODO
}];
///暂停状态
TKState *pause = [TKState stateWithName:kPause];
[pause setDidEnterStateBlock:^(TKState *state, TKTransition *transition) {
/// TODO
}];
///播放完成状态
TKState *finish = [TKState stateWithName:kFinish];
[finish setDidEnterStateBlock:^(TKState *state, TKTransition *transition) {
/// TODO
}];
[self.stateMachine addStates:@[loading, playing, pause, finish]];
[self.stateMachine setInitialState:finish];
///关联事件
TKEvent *loadingEvent = [TKEvent eventWithName:kLoading transitioningFromStates:@[playing, pause, finish] toState:loading];
TKEvent *playingEvent = [TKEvent eventWithName:kPlaying transitioningFromStates:@[loading, pause, finish] toState:playing];
TKEvent *pauseEvent = [TKEvent eventWithName:kPause transitioningFromStates:@[playing, loading] toState:pause];
TKEvent *finishEvent = [TKEvent eventWithName:kFinish transitioningFromStates:@[loading, playing, pause] toState:finish];
[_stateMachine addEvents:@[loadingEvent, playingEvent, pauseEvent, finishEvent]];
[_stateMachine activate];
}
这里状态是负责界面的变化什么的。比如加载中就在播放器上显示一个菊花转动,比如暂停,按钮状态就要变化。
接下来在状态变化的时候触发响应的事件就好了。比如,从暂停状态到播放状态,这个时候触发播放状态的变化。
[self.stateMachine fireEvent:kPlaying userInfo:nil error:nil];
回头看我们定义播放事件的代码
TKEvent *playingEvent = [TKEvent eventWithName:kPlaying transitioningFromStates:@[loading, pause, finish] toState:playing];
所以,只要前一个事件是 @[loading, pause, finish] 中的一个就能成功触发,否则,事件不响应!!!
我们看看源码
主要由下面几个类组成的
TKEvent ///事件对象
TKState ///状态
TKStateMachine ///状态机管理中心
TKTransition ///状态切换的过程的信息
<code>TKStateMachine</code>负责管理状态的,
初始化过程
- (id)init
{
self = [super init];
if (self) {
self.mutableStates = [NSMutableSet set];
self.mutableEvents = [NSMutableSet set];
self.lock = [NSRecursiveLock new];
}
return self;
}
使用 <code> NSMutableSet </code>管理状态和事件 lock 可以多线程调用
- (void)setInitialState:(TKState *)initialState
{
TKRaiseIfActive();
_initialState = initialState;
}
设置最开始的状态。<code> TKRaiseIfActive </code> 宏 用来判断当前的状态是不是激活状态,如果是激活状态,是不能修改的,因为,状态的动作有可能已经开始执行了~
重点看看 activate 和
- (BOOL)fireEvent:(id)eventOrEventName userInfo:(NSDictionary *)userInfo error:(NSError *__autoreleasing *)error
函数的实现过程
- (void)activate
{
///如果已经是激活状态,激活肯定是无效的。
if (self.isActive) [NSException raise:NSInternalInconsistencyException format:@"The state machine has already been activated."];
[self.lock lock];
///标记已经激活
self.active = YES;
///调用对应的blocks
///将要激活
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];
}
这里有点类似 KVO 时候做的事情,在属性改变之前和改变之后,通知一下关心属性的对象。
接下来看看触发某事件的过程
- (BOOL)fireEvent:(id)eventOrEventName userInfo:(NSDictionary *)userInfo error:(NSError *__autoreleasing *)error
{
[self.lock lock];
///设置激活状态
if (! self.isActive) [self activate];
///传入的 eventOrEventName 如果是字符串,就通过字符串转化成 TKEvent
if (! [eventOrEventName isKindOfClass:[TKEvent class]] && ![eventOrEventName isKindOfClass:[NSString class]]) [NSException raise:NSInvalidArgumentException format:@"Expected a `TKEvent` object or `NSString` object specifying the name of an event, instead got a `%@` (%@)", [eventOrEventName class], eventOrEventName];
TKEvent *event = [eventOrEventName isKindOfClass:[TKEvent class]] ? eventOrEventName : [self eventNamed:eventOrEventName];
if (! event) [NSException raise:NSInvalidArgumentException format:@"Cannot find an Event named '%@'", eventOrEventName];
///检查事件激活的条件是不是满足!
if (event.sourceStates != nil && ![event.sourceStates containsObject:self.currentState]) {
NSString *failureReason = [NSString stringWithFormat:@"An attempt was made to fire the '%@' event while in the '%@' state, but the event can only be fired from the following states: %@", event.name, self.currentState.name, [[event.sourceStates valueForKey:@"name"] componentsJoinedByString:@", "]];
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;
}
TKTransition *transition = [TKTransition transitionForEvent:event fromState:self.currentState inStateMachine:self userInfo:userInfo];
///询问外部接口,这个事件能不能触发。
if (event.shouldFireEventBlock) {
if (! event.shouldFireEventBlock(event, transition)) {
NSString *failureReason = [NSString stringWithFormat:@"An attempt to fire the '%@' event was declined because `shouldFireEventBlock` returned `NO`.", event.name];
NSDictionary *userInfo = @{ NSLocalizedDescriptionKey: @"The event declined to be fired.", NSLocalizedFailureReasonErrorKey: failureReason };
if (error) *error = [NSError errorWithDomain:TKErrorDomain code:TKTransitionDeclinedError userInfo:userInfo];
[self.lock unlock];
return NO;
}
}
/// 开始切换状态
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];
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
[notificationInfo addEntriesFromDictionary:@{ TKStateMachineDidChangeStateOldStateUserInfoKey: oldState,
TKStateMachineDidChangeStateNewStateUserInfoKey: newState,
TKStateMachineDidChangeStateEventUserInfoKey: event,
#pragma clang diagnostic pop
TKStateMachineDidChangeStateTransitionUserInfoKey: transition }];
[[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];
return YES;
}
if (event.sourceStates != nil && ![event.sourceStates containsObject:self.currentState])
如果当前要激活的事件存在sourceStates 并且 sourceStates 不包含 currentState 那么事件肯定不能触发了。因为违背了之前的讲的条件,状态的变化是线性化的,并且有前提条件,并且不能随意切换!
TKTransition 状态变化过程中的信息。
- (TKEvent *)eventNamed:(NSString *)name
{
for (TKEvent *event in self.mutableEvents) {
if ([event.name isEqualToString:name]) return event;
}
return nil;
}
eventNamed 函数 通过事件的名称查找 事件。前提条件是事件已经加入到了事件管理的 set 中。。 方便容错,其他地方如果使用到 TKEvent 的时候,直接定义参数类型为 id 类型,使用的时候再通过类型推导! 因为外部创建事件的时候,不一定要保存一份,比如我们上面创建的过程!
其他的代码,都比较好理解了。
总之,碰到跟状态相关的需求,可以考虑 TKTransition 这个第三库!