危險的UITableView
如果把我們所做的UI做個簡單分類,大致上可以分為列表界面和非列表界面。對于列表類UI,我們可以選擇UITableView或者UICollectionView來實現。UICollectionView出現之前,UITableView幾乎是唯一的選擇,這每日可見人人都用的UITableView里隱藏著容易忽視的危險。
同步VS異步
同步和異步是基礎的編程概念,也是貫穿于我們日常的兩種代碼書寫方式。理解sync和async不僅僅在于明白代碼執行順序上的差異,更重要的是理解這兩種方式的差異對我們代碼健壯性的影響。
同步的代碼書寫方式很直觀,也是大部分初學者潛意識所選擇的方式。我們只需要把心中的思路按部就班的轉換成代碼,就形成了一段同步的邏輯,比如下面一段同步代碼:
self.arr = @[].mutableCopy;
for (int i = 0; i < 1000; i ++) {
[self.arr addObject:@(i)];
}
同步強調的流程是:我 此刻 擁有哪些數據, 此刻 對這些數據進行一些計算,進而利用計算結果在 此刻 產生更多的行為。同步意味著在 當下 一步一步按順序的完成邏輯。
當我們代碼越寫越多,手感變好之后,我們會寫更多的異步代碼。異步在執行的時間上和同步剛好相反,異步強調的是代碼 當下 并不執行,而是等待 未來 某個時機到來之后再發生。比如下面一段異步代碼:
self.arr = @[].mutableCopy;
//async
[operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
for (int i = 0; i < 1000; i ++) {
[wself.arr addObject:@(i)];
}
} failure:nil];
對于_arr的修改操作,是在網絡請求完成之后再執行的。
異步的好處在于表達能力更靈活更強,我們對于跨越某個時間段的流程,有了更好的表達方式。這幾年很受技術圈熱捧的Reactive Programming,精髓之一就在于異步。
Wild beast is dangerous,異步的缺點也很明顯,由于跨域了一定的時間區域,在異步操作真正發生的時候,我們程序所依賴的狀態很有可能在這一時間跨度內發生了意料之外的變化,進而導致奇奇怪怪的bug。具體到上面這段代碼,很有可能在執行 [_arr addObject:@(i)]; 的時候, self.arr 已經被某處代碼改為nil了。我在之前介紹函數式編程的文章中也提到了這點,賦值操作會隨著時間的變化而危險起來,而Functional Programming恰好可以幫助我們解決狀態維護在時間維度上的問題,這也是為什么 異步的響應式編程 總是和 無狀態的函數式編程 結對出現,雙劍合并成FRP。
我們再來看看UITableView中的同步與異步。
UITableView
標題中所說的危險之處正是在于 異步 。更具體點來說,是 reloadData 這個調用中所包含的異步操作。先來看看執行 reloadData 都發生了什么。
當我們 reloadData 的時候,我們本意是刷新 UITableView ,隨后會進入一系列 UITableViewDataSource 和 UITableViewDelegate 的回調,其中有些是和 reloadData 同步發生的,有些則是異步發生的。
我們熟悉的下面兩個回調是同步的:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
return 20;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return _arr.count;
}
而另一個最常使用的回調則是異步的:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
//...
NSNumber* content = _arr[indexPath.row];
//...
}
經過上面的分析,我們不難UITableView的危險之處在于哪了,在于異步執行cellForRowAtIndexPath的時候,我們所依賴的狀態可能會發生變化,上面代碼中的_arr如果元素被修改過,極有可能發生數組越界的異常。
當列表界面數據不怎么變化的時候,幾乎感知不到這種異常的存在,因為reloadData返回之后,下一次loop就開始執行異步的操作了。但是當列表界面的數據有可能經常變化的時候,尤其是在多線程的場景下,就會出現偶現的bug了。
實際上,所有在函數內部處理外部狀態的場景,我們都需要假設狀態是不安全的,有可能被修改過了,使用前盡可能的檢查各種邊界條件,這樣的代碼才足夠robust,當然啦,能不依賴外部狀態是最好不過了。
如何解決
解決的方式五花八門,我相信很多人都有自己的獨門秘技,不過關鍵應該都在于消除異步帶來的狀態不穩定。
方式一:
最直觀的,我們可以在執行_arr[indexPath.row];的時候,做下長度檢查,如果越界則返回空:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
if (indexPath.row > self.arr.count - 1 || indexPath.row == 0) {
return [UITableViewCell new];
}
//...
NSNumber* content = _arr[indexPath.row];
//...
}
這種方式粗暴有效,可以避免crash。
方式二:
或者我們可以采用前面文章中提到的throttle機制,控制刷新事件的產生頻率,建立一個Queue以一定的時間間隔來調用reloadData。事實上這是一種很常見的界面優化機制,對于一些刷新頻率可能很高的列表界面,比如微信的會話列表界面,如果很長時間沒有登錄了,打開App時,堆積了很久的離線消息會在短時間內,導致大量的界面刷新請求,頻繁的調用reloadData還會造成界面的卡頓,所以此時建立一個FIFO的Queue,以一定的間隔來刷新界面就很有必要了,這種做法代碼量會多一些,但體驗更好更安全,具體代碼我就不展示了,實現起來也不難。
總結
這篇文章雖然是分析UITableView,但本意其實是想和大家分享異步代碼中所存在的隱患。無論是系統API調用,還是平常做業務時所寫的異步流程,異步的思想隨處可見,我們要極其小心當中可能存在的狀態維護上的坑。
來自:http://mrpeak.cn/blog/tableview-danger/