MJRefresh中異步更改UI爲Refreshing狀態導致內部狀態和UI狀態不一致的問題

1. 前言

    項目中,使用 MJRefresh 作爲下拉刷新控件。在手動觸發下拉刷新時候遇到了一個 bug,看了一下 MJRefresh 的源碼,發現 MJRefresh 的實現不太健壯。

2. 問題描述

    如果我們這樣使用MJRefresh,最後MJRefresh Header將會保持下拉刷新的狀態,而不能恢復到Idle的狀態。

    MJRefreshNormalHeader *header = [MJRefreshNormalHeader headerWithRefreshingBlock:^{
        @strongify(self);
        [[self.viewModel reloadData] subscribeNext:^(id x) {
            @strongify(self);
            [self endRefreshing];
        } error:^(NSError *error) {
            @strongify(self);
            [self endRefreshing];
        }];
    }];
    self.tableView.mj_header = header;
    ...
    [self.tableView.mj_header endRefreshing];
    [self.tableView.mj_header beginRefreshing];

    以上代碼中調用beginRefreshing是爲了觸發下拉刷新。調用endRefreshing是項目業務需要,兩條語句連續執行會導致 MJRefresh 表現不正確(即不能恢復到Idle狀態)。下面具體分析一下。

3.問題原因

    問題核心原因是:在 MJRefreshHeader 類 setState 方法中“更改UI爲 refreshing 狀態”的操作是異步的。也就是說,設置 Refreshing 狀態時,設置內部狀態和設置UI狀態被分離開了,如果在中間插入了設置內部狀態(比如 Idle )的操作可能會導致內部狀態和UI狀態不一致的問題。另外,MJRefreshendRefreshing方法中“設置狀態爲 Idle ”操作是異步的。
出現問題的原因就是兩次異步,由於執行順序的原因,導致內部狀態和UI狀態不一致。

源碼如下:

- (void)beginRefreshing
{
    ...
    self.state = MJRefreshStateRefreshing;
    ...
}

---

- (void)endRefreshing
{
    dispatch_async(dispatch_get_main_queue(), ^{
        self.state = MJRefreshStateIdle;
    });
}

- (void)setState:(MJRefreshState)state
{
    // 根據狀態做事情
    if (state == MJRefreshStateIdle) {
        ...
        // 恢復inset和offset
        [UIView animateWithDuration:MJRefreshSlowAnimationDuration animations:^{
            self.scrollView.mj_insetT += self.insetTDelta;

            // 自動調整透明度
            if (self.isAutomaticallyChangeAlpha) self.alpha = 0.0;
        } completion:^(BOOL finished) {
            self.pullingPercent = 0.0;

            if (self.endRefreshingCompletionBlock) {
                self.endRefreshingCompletionBlock();
            }
        }];
    } else if (state == MJRefreshStateRefreshing) {
         dispatch_async(dispatch_get_main_queue(), ^{
            [UIView animateWithDuration:MJRefreshFastAnimationDuration animations:^{
                CGFloat top = self.scrollViewOriginalInset.top + self.mj_h;
                // 增加滾動區域top
                self.scrollView.mj_insetT = top;
                // 設置滾動位置
                [self.scrollView setContentOffset:CGPointMake(0, -top) animated:NO];
            } completion:^(BOOL finished) {
                [self executeRefreshingCallback];
            }];
         });
    }
}

按照我們在問題描述中的調用方式,最後執行順序如下:

  1. 設標誌爲 Idle 操作入主線程隊列
  2. 同步執行設標誌爲 Refreshing,將設 UI 爲 Refreshing 樣式操作入主線程隊列
  3. 主線程隊列中的操作開始執行,執行設標誌爲 Idle 操作,同時執行將 UI 設爲 Idle 樣式
  4. 主線程繼續執行隊列中的操作,設 UI 爲 Refreshing 樣式

    至此,內部狀態爲idle,UI狀態爲refreshing。 內部狀態爲Idle狀態,之後的endRefreshing將不會生效(發現newState與oldState一致就直接返回了),UI無法恢復爲Idle狀態。

4. 問題解決

    最好的解決辦法是把setState中“更改UI爲refreshing狀態”的操作變成同步的。避免設置內部狀態和設置UI狀態的分離,因爲兩者分離之後,如果中間執行了“設置狀態爲Idle”,那麼將導致最終內部狀態爲Idle、UI狀態爲Refreshing的問題,也就是標題所說的內部狀態和UI狀態不一致的問題。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章