原始碼解析之–YYAsyncLayer非同步繪製

Shelin發表於2016-07-14

前言

YYAsyncLayer是非同步繪製與顯示的工具。最初是從YYKitDemo中接觸到這個工具,為了保證列表滾動流暢,將檢視繪製、以及圖片解碼等任務放到後臺執行緒,在YYAsyncLayer之前還是想從YYKitDemo中效能優化說起,雖然些跑題了…

YYKitDemo

對於列表主要對兩個代理方法的優化,一個與繪製顯示有關,另一個與計算佈局有關:

常規邏輯可能覺得應該先呼叫tableView : cellForRowAtIndexPath :返回UITableViewCell物件,事實上呼叫順序是先返回UITableViewCell的高度,是因為UITableView繼承自UIScrollView,滑動範圍由屬性contentSize來確定,UITableView的滑動範圍需要通過每一行的UITableViewCell的高度計算確定,複雜cell如果在列表滾動過程中計算可能會造成一定程度的卡頓。
假設有20條資料,當前螢幕顯示5條,tableView : heightForRowAtIndexPath :方法會先執行20次返回所有高度並計算出滑動範圍,tableView : cellForRowAtIndexPath :執行5次返回當前螢幕顯示的cell個數。

 

1121012-d95b3294b3ca0fc3

TableViewOfPerformanceOptimization.png

 

從圖中簡單看下流程,從網路請求返回JSON資料,將Cell的高度以及內部檢視的佈局封裝為Layout物件,Cell顯示之前在非同步執行緒計算好所有佈局物件,並存入陣列,每次呼叫tableView: heightForRowAtIndexPath :只需要從陣列中取出,可避免重複的佈局計算。同時在呼叫tableView: cellForRowAtIndexPath :對Cell內部檢視非同步繪製佈局,以及圖片的非同步繪製解碼,這裡就要說到今天的主角YYAsyncLayer。

YYAsyncLayer

首先介紹裡面幾個類:

  • YYAsyncLayer:繼承自CALayer,繪製、建立繪製執行緒的部分都在這個類。
  • YYTransaction:用於建立RunloopObserver監聽MainRunloop的空閒時間,並將YYTranaction物件存放到集合中。
  • YYSentinel:提供獲取當前值的value(只讀)屬性,以及- (int32_t)increase自增加的方法返回一個新的value值,用於判斷非同步繪製任務是否被取消的工具。

 

1121012-86524da67fb8b094

AsyncDisplay.png

上圖是整體非同步繪製的實現思路,後面一步步說明。現在假設需要繪製Label,其實是繼承自UIView,重寫+ (Class)layerClass ,在需要重新繪製的地方呼叫下面方法,比如setterlayoutSubviews

YYTransaction有selectortarget的屬性,selector其實就是contentsNeedUpdated方法,此時並不會立即在後臺執行緒去更新顯示,而是將YYTransaction物件本身提交儲存在transactionSet的集合中,上圖中所示。

同時在YYTransaction.m中註冊一個RunloopObserver,監聽MainRunloop在kCFRunLoopCommonModes(包含kCFRunLoopDefaultModeUITrackingRunLoopMode)下的kCFRunLoopBeforeWaitingkCFRunLoopExit的狀態,也就是說在一次Runloop空閒時去執行更新顯示的操作。

kCFRunLoopBeforeWaiting:Runloop將要進入休眠。
kCFRunLoopExit:即將退出本次Runloop。

下面是RunloopObserver的回撥方法,從transactionSet取出transaction物件執行SEL的方法,分發到每一次Runloop執行,避免一次Runloop執行時間太長。

接下來是非同步繪製,這裡用了一個比較巧妙的方法處理,當使用GCD時提交大量併發任務到後臺執行緒導致執行緒被鎖住、休眠的情況,建立與程式當前啟用CPU數量(activeProcessorCount)相同的序列佇列,並限制MAX_QUEUE_COUNT,將佇列存放在陣列中。
YYAsyncLayer.m有一個方法YYAsyncLayerGetDisplayQueue來獲取這個佇列用於繪製(這部分YYKit中有獨立的工具YYDispatchQueuePool)。建立佇列中有一個引數是告訴佇列執行任務的服務質量quality of service,在iOS8+之後相比之前系統有所不同。

  • iOS8之前佇列優先順序:

DISPATCH_QUEUE_PRIORITY_HIGH 2 高優先順序
DISPATCH_QUEUE_PRIORITY_DEFAULT 0 預設優先順序
DISPATCH_QUEUE_PRIORITY_LOW (-2) 低優先順序
DISPATCH_QUEUE_PRIORITY_BACKGROUND INT16_MIN 後臺優先順序

  • iOS8+之後:

QOS_CLASS_USER_INTERACTIVE 0x21, 使用者互動(希望儘快完成,不要放太耗時操作)
QOS_CLASS_USER_INITIATED 0x19, 使用者期望(不要放太耗時操作)
QOS_CLASS_DEFAULT 0x15, 預設(用來重置對列使用的)
QOS_CLASS_UTILITY 0x11, 實用工具(耗時操作,可以使用這個選項)
QOS_CLASS_BACKGROUND 0x09, 後臺
QOS_CLASS_UNSPECIFIED 0x00, 未指定

接下來是關於繪製部分的程式碼,對外介面YYAsyncLayerDelegate代理中提供- (YYAsyncLayerDisplayTask *)newAsyncDisplayTask方法用於回撥繪製的程式碼,以及是否非同步繪製的BOOl型別屬性displaysAsynchronously,同時重寫CALayer的display 方法來呼叫繪製的方法- (void)_displayAsync:(BOOL)async
這裡有必要了解關於後臺的繪製任務何時會被取消,下面兩種情況需要取消,並呼叫了YYSentinel的increase方法,使value值增加(執行緒安全):

  • 在檢視呼叫setNeedsDisplay時說明檢視的內容需要被更新,將當前的繪製任務取消,需要重新顯示。
  • 以及檢視被釋放呼叫了dealloc方法。

在YYAsyncLayer.h中定義了YYAsyncLayerDisplayTask類,有三個block屬性用於繪製的回撥操作,從命名可以看出分別是將要繪製,正在繪製,以及繪製完成的回撥,可以從block傳入的引數BOOL(^isCancelled)(void)判斷當前繪製是否被取消。

下面是部分- (void)_displayAsync:(BOOL)async繪製的程式碼,主要是一些邏輯判斷以及繪製函式,在非同步執行之前通過YYAsyncLayerGetDisplayQueue建立的佇列,這裡通過YYSentinel判斷當前的value是否等於之前的值,如果不相等,說明繪製任務被取消了,繪製過程會多次判斷是否取消,如果是則return,保證被取消的任務能及時退出,如果繪製完畢則設定圖片到layer.contents

最後

關於具體使用可以看看程式的示例,這是從YYAsyncLayer中學到的一些技巧,自己還試著簡單實現一遍,專案中遇到的效能問題可也以依據這些思路去找到最合適的解決方案,挺想說一句閱讀原始碼是件比較要耐心的事,但確實可以收穫頗多。最近也有換環境工作的計劃,座標帝都,歡迎騷擾https://github.com/ShelinShelin

打賞支援我寫出更多好文章,謝謝!

打賞作者

打賞支援我寫出更多好文章,謝謝!

原始碼解析之–YYAsyncLayer非同步繪製

相關文章