iOSiOS技术点Objective-C

iOS 状态机 应用 TransitionKit

2017-06-01  本文已影响878人  杨柳小易

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 这个第三库!

上一篇下一篇

猜你喜欢

热点阅读