天天看點

iOS APP 架構漫談(二)

上一篇《iOS APP 架構漫談(一)》簡單介紹了information flow的概念。這篇文章簡單介紹另一個在程式設計中非常重要的思想或工具——狀态機(State machine)。 對大多數計算機專業的家夥們來說,這應該是一門比較難學的課程,裡面包含一大堆揪心的名字比如DFA,NFA,還有一大堆各種各樣的數學符号,又是編譯原理的基礎。不過很遺憾,似乎在做完編譯原理課程作業之後,很多人再也沒有實作過或是用過狀态機了。本文通過一個遊戲demo來簡單描述一下狀态機在實踐中的應用。demo code

背景

首先看下我們的使用場景,假如我們需要設計一套聯網對戰的小遊戲。第一個難題可能是如何建立一個通道,讓2個手機互相發送消息。這裡我并不打算引入server端開發,希望隻是通過用戶端來實作這個邏輯,這裡使用LeanCloud API來簡化這個過程。這樣我們可以暫時不考慮技術細節,直接站在業務角度去思考如何建立這個遊戲。

業務場景–邀請

正式開始遊戲之前,總會有一個邀請的環節。假如我們有2個使用者,分别是Host,Guest。Host建立遊戲,Guest加入遊戲。遊戲的整個流程和我們平時玩的對戰遊戲流程并沒有多大不同。

iOS APP 架構漫談(二)
  1. Host建立遊戲,他就相當于進入一個等待隊列裡面。
  2. Guest加入遊戲,他從等待隊列中找到一個比對,比如Host。然後對Host發送join message
  3. Host會收到很多join message。由于我們隻是選擇1vs1。這裡假定Host同意Guest加入遊戲。Host向Guest發送join confirm message
  4. Guest收到join confirm message, 向Host發送Go消息,表示Guest已經進入遊戲
  5. Host收到Go消息。也進入遊戲。

具體實作業務邏輯

現在的構想的邏輯隻有5步,但其實還會包含很多邏輯,比如逾時機制,重發機制。由于中間狀态很多,還可能有我們沒有想到過的問題。在面對這種複雜邏輯時,會通過狀态機來幫助我們理順邏輯。這時,我們腦中思考的業務其實是一個狀态到一個狀态的圖。 如下

iOS APP 架構漫談(二)

上半部分是遊戲的建立者,下半部分是遊戲的加入者。

一開始,盡量簡化模型,這裡紅色剪頭表示我們的正确主流路線,黑色表現出錯路線。也就是說,一旦錯誤,就回到原始Idle狀态。

開始寫代碼

在想清楚所有邏輯,并考慮清楚正常路線和錯誤路線之後,就可以開始寫代碼了。為了友善,這裡直接使用第三方的狀态機架構TransitionKit。

定義State(HOST)

TKState *idleState = [TKState stateWithName:@”idle”];

TKState *waitingJoinState = [TKState stateWithName:@”waitingJoin”];

TKState *waitingConfirmState = [TKState stateWithName:@”waitingConfirm”];

TKState *goState = [TKState stateWithName:@”go”];

[waitingConfirmState setDidEnterStateBlock:^(TKState *state, TKTransition *transition) {

[selfWeak sendJoinConfirm];

}];

[goState setDidEnterStateBlock:^(TKState *state, TKTransition *transition) {

NSLog(@”happy ending”);

[SVProgressHUD showSuccessWithStatus:@"ok"];
           

}];

定義Event(HOST)

Event 是建立State到State的路徑

TKEvent *waitingJoinEvent = [TKEvent eventWithName:CUHostGameManagerWaitingJoinEvent

transitioningFromStates:@[idleState]

toState:waitingJoinState];

TKEvent *receiveInviteEvent = [TKEvent eventWithName:CUHostGameManagerReceiveInviteEvent

transitioningFromStates:@[waitingJoinState]

toState:waitingConfirmState];

TKEvent *receiveConfirmEvent = [TKEvent eventWithName:CUHostGameManagerReceiveConfirmEvent

transitioningFromStates:@[waitingConfirmState]

toState:goState];

TKEvent *disconnectedEvent = [TKEvent eventWithName:CUHostGameManagerDisconnectedEvent

transitioningFromStates:nil

toState:idleState];

定義過程(HOST)

  • (void)startGame {

    NSAssert(self.session.peerId != nil, @”“);

    //這裡,如果不是idle,我們切換狀态機到idle

    if (![self.stateMachine.currentState.name isEqual:@”idle”]) {

    [self fireEvent:CUHostGameManagerDisconnectedEvent userInfo:nil];

    }

    //這裡調用LeanCloud 入隊

    AVObject *waitingId = [AVObject objectWithClassName:@”waiting_join_Ids”];

    [waitingId setObject:self.session.peerId forKey:@”peerId”];

    [waitingId saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) {

    //enqueue 之後,進入waitingJoin狀态

    [self fireEvent:CUHostGameManagerWaitingJoinEvent userInfo:nil];

    }];

    }

    • (void)sendJoinConfirm {

      //發送加入确認消息給Guest

      AVMessage *message = [AVMessage messageForPeerWithSession:self.session

      toPeerId:self.peerId

      payload:@”join_confirm”];

      [self.session sendMessage:message transient:YES];

      }

    • (void)session:(AVSession )session didReceiveMessage:(AVMessage )message

      {

      if ([message.payload isEqualToString:@”join”]) {

      //收到Join(邀請)之後,發送确認消息

      self.peerId = message.fromPeerId;

      //因為LeanCloud的API比較挫,watch 之後才能發送消息,但是我們不知道什麼時候才watch成功。。。。

      //好在隻是demo,我們隻好用這種方式work around,延遲2s發送消息

      [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(sendInviteConfirmRequest:) object:nil];

      [self performSelector:@selector(sendInviteConfirmRequest:)

      withObject:@[message.fromPeerId]

      afterDelay:2.0f];

      }

      else if ([message.payload isEqualToString:@”go”]) {

      //收到go消息,流程結束

      [self fireEvent:CUHostGameManagerReceiveConfirmEvent userInfo:nil];

      }

      }

    • (void)sendInviteConfirmRequest:(NSArray *)watchPeerIds {

      [self.session watchPeerIds:watchPeerIds];

      [self fireEvent:CUHostGameManagerReceiveInviteEvent userInfo:nil];

      }

      定義State(Guest)

    TKState *idleState = [TKState stateWithName:@”idle”];

    TKState *waitingReplyState = [TKState stateWithName:@”waitingReply”];

    TKState *goState = [TKState stateWithName:@”go”];

    [waitingReplyState setWillEnterStateBlock:^(TKState *state, TKTransition *transition) {

    [selfWeak searchingGames];

    }];

    [goState setDidEnterStateBlock:^(TKState *state, TKTransition *transition) {

    [selfWeak sendGo];

    NSLog(@”happy ending”);

    [SVProgressHUD showSuccessWithStatus:@”ok”];

    }];

    定義Event(Guest)

    TKEvent *searchingEvent = [TKEvent eventWithName:CUGestGameManagerSearchingEvent

    transitioningFromStates:@[idleState]

    toState:waitingReplyState];

    TKEvent *receiveConfirmEvent = [TKEvent eventWithName:CUGestGameManagerReceiveConfirmEvent

    transitioningFromStates:@[waitingReplyState]

    toState:goState];

    TKEvent *disconnectedEvent = [TKEvent eventWithName:CUGestGameManagerDisconnectedEvent

    transitioningFromStates:nil

    toState:idleState];

    定義過程(Guest)

    • (void)joinGame {

    if (![self.stateMachine.currentState.name isEqual:@”idle”]) {

    [self fireEvent:CUGestGameManagerDisconnectedEvent userInfo:nil];

    }

    [self fireEvent:CUGestGameManagerSearchingEvent userInfo:nil];

    }

    • (void)searchingGames {

      AVQuery *query = [AVQuery queryWithClassName:@”waiting_join_Ids”];

      [query orderByDescending:@”updatedAt”];

      [query setLimit:1];

    [query findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {

    NSMutableArray *installationIds = [[NSMutableArray alloc] init];

    for (AVObject *object in objects) {

    if ([object objectForKey:@”peerId”]) {

    [installationIds addObject:[object objectForKey:@”peerId”]];

    }

    }

    [self.session watchPeerIds:installationIds];

    [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(sendJoinRequest) object:nil];

    [self performSelector:@selector(sendJoinRequest)

    withObject:nil

    afterDelay:2.0f];

    }];

    }

    • (void)sendJoinRequest {

    for (NSString *item in self.session.watchedPeerIds) {

    AVMessage *message = [AVMessage messageForPeerWithSession:self.session

    toPeerId:item

    payload:@”join”];

    [self.session sendMessage:message transient:YES];

    }

    }

    • (void)sendGo{

      AVMessage *message = [AVMessage messageForPeerWithSession:self.session

      toPeerId:self.otherPeerId

      payload:@”go”];

      [self.session sendMessage:message transient:YES];

      }

      最後

state machine 是一個蠻厲害的錘子,隻要是一個工具,就肯定會被濫用。。。state machine最大的好處是在于,友善我們思考清楚所有細節,主線,和錯誤流程。避免因為考慮不周全而産生的bug。結合之前的information flow的思路,會讓我們的軟體設計更加清楚。