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];
}];
});
}
}
按照我們在問題描述中的調用方式,最後執行順序如下:
- 將
設標誌爲 Idle 操作
入主線程隊列 - 同步執行
設標誌爲 Refreshing
,將設 UI 爲 Refreshing 樣式
操作入主線程隊列 - 主線程隊列中的操作開始執行,執行
設標誌爲 Idle 操作
,同時執行將 UI 設爲 Idle 樣式
- 主線程繼續執行隊列中的操作,
設 UI 爲 Refreshing 樣式
至此,內部狀態爲idle,UI狀態爲refreshing。 內部狀態爲Idle狀態,之後的endRefreshing
將不會生效(發現newState與oldState一致就直接返回了),UI無法恢復爲Idle狀態。
4. 問題解決
最好的解決辦法是把setState中“更改UI爲refreshing狀態”的操作變成同步的。避免設置內部狀態和設置UI狀態的分離,因爲兩者分離之後,如果中間執行了“設置狀態爲Idle”,那麼將導致最終內部狀態爲Idle、UI狀態爲Refreshing的問題,也就是標題所說的內部狀態和UI狀態不一致的問題。