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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,793评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,567评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,342评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,825评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,814评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,680评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,033评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,687评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,175评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,668评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,775评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,419评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,020评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,206评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,092评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,510评论 2 343

推荐阅读更多精彩内容