iOS 狀態(tài)機(jī)
狀態(tài)機(jī) 的 概念 可以從網(wǎng)上搜索曲横。
此文章主要分析TransitionKit
TransitionKit 在iOS開發(fā)中的作用
- 適用于流程化,狀態(tài)線性切換的場景不瓶;
- 狀態(tài)切換有依賴和前提條件禾嫉;
- 狀態(tài)切換只能由特定狀態(tài)切換到特定狀態(tài),不能隨意切換蚊丐,也不是可逆的熙参;
- 可將特定狀態(tài)下的業(yè)務(wù)邏輯集中到一起管理
先來看看應(yīng)用,此處還是以直播應(yīng)用舉例子麦备。比如播放器的狀態(tài)孽椰,有 播放 暫停 加載中 加載錯誤這些。
一開始肯定是加載中凛篙,加載中的狀態(tài) 可以 切換到 加載錯誤 和 播放 播放 可以切換到 加載 錯誤 暫停黍匾。暫停可以切換到 播放呛梆。
每個狀態(tài)和下一個狀態(tài)的依賴是有順序的锐涯。每一個狀態(tài)要展示的樣子也有很大不同。接下來看我們的應(yīng)用填物。
- (void)setupStateMachine
{
self.stateMachine = [[TKStateMachine alloc] init];
__weak typeof(self) weakSelf = self;
///加載中 狀態(tài)
TKState *loading = [TKState stateWithName:kLoading];
[loading setDidEnterStateBlock:^(TKState *state, TKTransition *transition) {
/// TODO
}];
///播放狀態(tài)
TKState *playing = [TKState stateWithName:kPlaying];
[playing setDidEnterStateBlock:^(TKState *state, TKTransition *transition) {
/// TODO
}];
[playing setDidExitStateBlock:^(TKState *state, TKTransition *transition) {
/// TODO
}];
///暫停狀態(tài)
TKState *pause = [TKState stateWithName:kPause];
[pause setDidEnterStateBlock:^(TKState *state, TKTransition *transition) {
/// TODO
}];
///播放完成狀態(tài)
TKState *finish = [TKState stateWithName:kFinish];
[finish setDidEnterStateBlock:^(TKState *state, TKTransition *transition) {
/// TODO
}];
[self.stateMachine addStates:@[loading, playing, pause, finish]];
[self.stateMachine setInitialState:finish];
///關(guān)聯(lián)事件
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];
}
這里狀態(tài)是負(fù)責(zé)界面的變化什么的纹腌。比如加載中就在播放器上顯示一個菊花轉(zhuǎn)動霎终,比如暫停,按鈕狀態(tài)就要變化升薯。
接下來在狀態(tài)變化的時候觸發(fā)響應(yīng)的事件就好了莱褒。比如,從暫停狀態(tài)到播放狀態(tài)涎劈,這個時候觸發(fā)播放狀態(tài)的變化广凸。
[self.stateMachine fireEvent:kPlaying userInfo:nil error:nil];
回頭看我們定義播放事件的代碼
TKEvent *playingEvent = [TKEvent eventWithName:kPlaying transitioningFromStates:@[loading, pause, finish] toState:playing];
所以,只要前一個事件是 @[loading, pause, finish] 中的一個就能成功觸發(fā)蛛枚,否則炮障,事件不響應(yīng)!@ず颉!
我們看看源碼
主要由下面幾個類組成的
TKEvent ///事件對象
TKState ///狀態(tài)
TKStateMachine ///狀態(tài)機(jī)管理中心
TKTransition ///狀態(tài)切換的過程的信息
<code>TKStateMachine</code>負(fù)責(zé)管理狀態(tài)的企蹭,
初始化過程
- (id)init
{
self = [super init];
if (self) {
self.mutableStates = [NSMutableSet set];
self.mutableEvents = [NSMutableSet set];
self.lock = [NSRecursiveLock new];
}
return self;
}
使用 <code> NSMutableSet </code>管理狀態(tài)和事件 lock 可以多線程調(diào)用
- (void)setInitialState:(TKState *)initialState
{
TKRaiseIfActive();
_initialState = initialState;
}
設(shè)置最開始的狀態(tài)白筹。<code> TKRaiseIfActive </code> 宏 用來判斷當(dāng)前的狀態(tài)是不是激活狀態(tài),如果是激活狀態(tài)谅摄,是不能修改的徒河,因?yàn)椋瑺顟B(tài)的動作有可能已經(jīng)開始執(zhí)行了~
重點(diǎn)看看 activate 和
- (BOOL)fireEvent:(id)eventOrEventName userInfo:(NSDictionary *)userInfo error:(NSError *__autoreleasing *)error
函數(shù)的實(shí)現(xiàn)過程
- (void)activate
{
///如果已經(jīng)是激活狀態(tài)送漠,激活肯定是無效的顽照。
if (self.isActive) [NSException raise:NSInternalInconsistencyException format:@"The state machine has already been activated."];
[self.lock lock];
///標(biāo)記已經(jīng)激活
self.active = YES;
///調(diào)用對應(yīng)的blocks
///將要激活
if (self.initialState.willEnterStateBlock) self.initialState.willEnterStateBlock(self.initialState, nil);
///設(shè)置當(dāng)前狀態(tài)
self.currentState = self.initialState;
///已經(jīng)激活
if (self.initialState.didEnterStateBlock) self.initialState.didEnterStateBlock(self.initialState, nil);
[self.lock unlock];
}
這里有點(diǎn)類似 KVO 時候做的事情,在屬性改變之前和改變之后闽寡,通知一下關(guān)心屬性的對象代兵。
接下來看看觸發(fā)某事件的過程
- (BOOL)fireEvent:(id)eventOrEventName userInfo:(NSDictionary *)userInfo error:(NSError *__autoreleasing *)error
{
[self.lock lock];
///設(shè)置激活狀態(tài)
if (! self.isActive) [self activate];
///傳入的 eventOrEventName 如果是字符串,就通過字符串轉(zhuǎn)化成 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];
///詢問外部接口植影,這個事件能不能觸發(fā)。
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;
}
}
/// 開始切換狀態(tài)
TKState *oldState = self.currentState;
TKState *newState = event.destinationState;
/// 切換狀態(tài)中的事件通知涎永。
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])
如果當(dāng)前要激活的事件存在sourceStates 并且 sourceStates 不包含 currentState 那么事件肯定不能觸發(fā)了思币。因?yàn)檫`背了之前的講的條件,狀態(tài)的變化是線性化的羡微,并且有前提條件谷饿,并且不能隨意切換!
TKTransition 狀態(tài)變化過程中的信息妈倔。
- (TKEvent *)eventNamed:(NSString *)name
{
for (TKEvent *event in self.mutableEvents) {
if ([event.name isEqualToString:name]) return event;
}
return nil;
}
eventNamed 函數(shù) 通過事件的名稱查找 事件博投。前提條件是事件已經(jīng)加入到了事件管理的 set 中。启涯。 方便容錯贬堵,其他地方如果使用到 TKEvent 的時候恃轩,直接定義參數(shù)類型為 id 類型,使用的時候再通過類型推導(dǎo)黎做! 因?yàn)橥獠縿?chuàng)建事件的時候叉跛,不一定要保存一份,比如我們上面創(chuàng)建的過程蒸殿!
其他的代碼筷厘,都比較好理解了。
總之宏所,碰到跟狀態(tài)相關(guān)的需求酥艳,可以考慮 TKTransition 這個第三庫!