iOS開發之UITableView中計時器的幾種實現方式(NSTimer、DispatchSource、CADisplayLink),uitableviewnstimer
最近工作比較忙,但是還是出來更新部落格了。今天部落格中所涉及的內容並不複雜,都是一些平時常見的一些問題,通過這篇部落格算是對UITableView中使用定時器的幾種方式進行總結。本篇部落格會給出在TableView中使用NSTimer或者DispatchSourcer中常見的五種方式。當然下方第一種方式是常規做法,不過也是UITableView中使用NSTimer的一個坑。其他三種方式是為了繞過這個坑的解決方案。
當然,本篇部落格共涉及到了UITableView中使用定時器的四種實現方式,當然應該也還有其他實現方式,只不過目前我沒有涉及到。歡迎在評論區提供其他實現方式,我會及時的整合到目前的Demo中。
接下來我們先來總結一下本篇部落格所涉及的四種方式:
第一種就是直接在TableView的Cell上使用NSTimer,當然這種方式是有問題的,稍後會介紹。
第二種是將NSTimer添加到當前線程所對應的RunLoop中的commonModes中。
第三種是通過Dispatch中的TimerSource來實現定時器。
第四種是開啟一個新的子線程,將NSTimer添加到這個子線程中的RunLoop中,並使用DefaultRunLoopModes來執行。
第五種方式就是使用CADisplayLink來實現。
下方我們將會根據具體的樣本來詳細的介紹以上這五種實現方式。
一、在Cell中直接使用NSTimer
首先我們按照常規做法,直接在UITableView的Cell上添加相應的NSTimer, 並使用scheduledTimer執行相應的代碼塊。這種方式沒有什麼特殊的就是對Timer的直接使用。下方是我們本部分的Timer的使用代碼,當然是使用Swift來實現的,不過與OC的代碼差不多。代碼如下所示 :
上述代碼比較簡單,就是在Cell上添加了一個定時器,然後沒1秒更新一次時間,並在Cell的timeLabel上顯示,運行效果如下所示。從該運行效果中我們不難發現,當我們滑動TableView時,該定時器就停止了工作。具體原因就是當前線程的RunLoop在TableView滑動時將DefaultMode切換到了TrackingRunLoopMode。因為Timer預設是添加在RunLoop上的DefaultMode上的,當Mode切換後Timer就停止了運行。
但是當停止滑動後,Mode又切換了回來,所以Timer有可以正常工作了。
為了進一步看一下Mode的切換,我們可以在相應的地方擷取當前線程的RunLoop並且列印對應的Mode。下方代碼就是在TableView所對應的VC上添加的,我們在viewDidLoad()、viewDidAppear()以及scrollViewDidScroll()這個代理方法中對當前線程所對應的RunLoop下的currentMode進行了列印,其代碼如下。
下方就是最終的運行結果。從輸出結果中我們不難看出,在viewDidLoad()方法中列印的Current Mode為UIInitializationRunLoopMode, 從該Mode的名字中我們不難發現,該Mode負責UI的初始化。在viewDidApperar()方法中,也就是UI顯示後,RunLoop的Mode切換成了kCFRunLoopDefaultMode。緊接著,我們去滑動TableView,然後在scrollViewDidScroll()代理方法中列印滑動時當前RunLoop所對應的Mode。從下方運行結果不難看出,當TableView滑動時,列印出的currentModel為UITrackingRunLoopMode。當停止滑動後,點擊Show Current Mode按鈕擷取當前Mode時,列印的有時RunLoopDefaultMode。具體如下所示:
二、將Timer添加到CommonMode中
上一部分的定時器是不能正常啟動並執行,因為NSTimer對象預設添加到了當前RunLoop的DefaultMode中,而在切換成TrackingRunLoopMode時,定時器就停止了工作。解決該問題最直接方法是,將NSTimer在TrackingRunLoopMode中也添加一份。這樣的話無論是在DefaultMode還是TrackingRunLoopMode中,定時器都會正常的工作。
如果你對RunLoop比較熟悉的話,可以知道CommonModes就是DefaultMode和TrackingRunLoopMode的集合,所以我們只需要將NSTimer對象與當前線程所對應的RunLoop中的CommonModes關聯即可,具體代碼如下所示:
上述代碼與第一部分的代碼不同的地方在於我們將建立好的定時器添加到了當前RunLoop中的CommonModes中,這樣的話可以保證TableView在滑動時定時器也可以正常運行。上述代碼最終的運行效果如下所示。
從該運行效果我們不難發現,當該TableView滾動式,其Cell上的定時器是可以正常工作的。但是當我們滑動右上方的這個TableView時,第一個的TableView中的定時器也是不能正常工作的,因為這些TableView都在主線程中工作,也就是說這些TableView所在的RunLoop是同一個。
三、將Timer添加到子線程的RunLoop下的DefaultMode中
接下來我們來看另一種解決方案,就是開啟一個新的子線程,然後將Timer添加到這個子線程所對應的RunLoop中。當然因為是子線程的RunLoop,在添加Timer時,我們可以將Timer添加到子線程中的RunLoop中的DefaultMode中。添加完畢後,手動運行該RunLoop。
因為是在子線程中添加的Timer, Timer肯定是在子線程中工作的,所以在更新UI時,我們需要在主線程中進行更新,具體代碼如下所示:
在上述代碼中我們可以看到我們使用全域的並行隊列來非同步建立了一個Timer對象,然後將該對象添加進了該非同步線程中的DefaultRunLoopMode中,然後運行該RunLoop。當然在子線程中更新UI還是需要在主線程中去操作的。下方就是上述代碼的運行效果。從該效果中我們不難看出,當滑動TableView時定時器是可以正常工作的。
四、DispatchTimerSource
接下來我們就不使用NSTimer來實現定時器了。在之前的部落格中聊GCD時其中用到了DispatchTimerSource來實現定時器。接下來我們就在TableView的Cell上添加DispatchTimerSource,然後看一下運行效果。當然下方程式碼片段我們是在全域隊列中添加的DispatchTimerSource,在主線程中進行更新。當然我們也可以在mainQueue中添加DispatchTimerSource,這樣也是可以正常工作的。當然我們不建議在MainQueue中做,因為在編程時盡量的把一些和主線程關聯不太大的操作放到子線程中去做。代碼如下所示:
接下來我們來看一下上述的代碼的運行效果,從該效果中我們可以看出該定時器是可以正常工作的。
五、CADisplayLink
接下來我們來使用CADisplayLink來實現定時器功能,在之前的部落格中我們也使用過CADisplayLink,不過是用來計算FPS的。下方程式碼片段中我們就使用CADisplayLink來實現的定時器。CADisplayLink可以添加到RunLoop中,RunLoop的每一次迴圈都會觸發CADisplayLink所關聯的方法。在螢幕不卡頓的情況下,每次迴圈的時間時1/60秒。
下方代碼,為了不讓螢幕的卡頓等引起的主線程所對應的RunLoop阻塞所造成的定時器不精確的問題。我們開啟了一個新的線程,並且將CADisplayLink對象添加到這個子線程的RunLoop中,然後在主線程中更新UI即可。具體代碼如下:
我們對上述代碼運行,下方是其對應的運行結果。從下方運行結果中我們不難看出,在TableView滾動時該定時器也是可以正常啟動並執行。當然該方式實現的定時器的精度是比較高的。
經過上述五大部分,我們羅列了定時器的幾種實現方式,通過對比我們不難發現其優劣性。上述定時器中DispatchSourceTime以及CADisplayLink的精度要比NSTimer的精度要高。從代碼實現中我們不難看出CADisplayLink的精度是比較高的。
本篇部落格所涉及代碼的github分享地址為:https://github.com/lizelu/NSTimerWithRunLoop