美文网首页iOSiOS技术点Objective-C
iOS 状态机 应用 TransitionKit

iOS 状态机 应用 TransitionKit

作者: 杨柳小易 | 来源:发表于2017-06-01 16:22 被阅读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 这个第三库!

    相关文章

      网友评论

        本文标题:iOS 状态机 应用 TransitionKit

        本文链接:https://www.haomeiwen.com/subject/pqeefxtx.html