我以前維護過公司的彈幕庫,但因爲它的歷史包袱太重,改形成本太高,一直沒有將它改形成我心中理想狀態的一個庫。另外在週末,我也須要作一些事情來消磨時間,因此我寫了一個比較符合我心中理想狀態的彈幕庫並將它開源:github.com/qyz777/Danm…git
目前DanmakuKit已經具有一些基礎的能力,我還列了一些TODO,將來我會利用週末的空餘時間持續完善它的功能。github
DanmakuKit是一個高性能彈幕框架,它提供了基礎的彈幕功能,可以讓你經過異步隊列的方式渲染彈幕。它提供三種彈幕類別,分別是浮動、置頂和置底彈幕。目前它支持的功能以下:數組
在說原理以前先把彈幕庫的類圖放上,DanmakuKit由DanmakuView做爲承載彈幕的主體,其中包含不一樣的DanmakuTrack做爲管理view中彈幕的對象。對於使用者而言,只須要給DanmakuView傳入了實現DanmakuCellModel協議的對象,它就能根據協議自動建立或複用一個DanmakuCell來展現彈幕。安全
在實際寫代碼以前就必須考慮到彈幕的性能問題,由於若是不考慮這個問題的話一旦彈幕量很大那就會極大的影響app使用體驗。那麼在iOS中,想要得到最佳的性能體驗,咱們能夠很快的想到一個流程,那就是異步隊列渲染出一張彈幕圖片,把它放在layer.content中,再用Core Animation播放出來。另外,反覆的建立和銷燬管理彈幕的對象也有一些開銷,咱們要用合適的方法來管理這些對象。markdown
所以咱們總結一下,若是想要實現一個高性能的彈幕,那咱們確定會用到如下3點:app
複用是一個很容易想到的想法,在DanmakuKit中,彈幕的繪製是由DanmakuCell實現的,而它是一個view的子類,因此複用也是以view爲維度的。複用view是爲了減小反覆addSubview以及removeFromSuperView的開銷,固然,在實際測試來看這塊性能開銷並不會特別大。框架
在DanmakuKit中,繪製使用的是CGContext
,將內容繪製成一張圖片放在layer.content中。若是這塊的邏輯是在主線程同步的話那麼必然會是個不小的開銷,此時選擇異步隊列繪製就是一個很好的選擇。固然,異步隊列繪製也有它的劣勢,那就是在寫代碼的過程當中必須注意線程安全問題。異步
異步隊列渲染的原理能夠參考github.com/ibireme/YYA… ,網上也有很多的博客解析過原理。ide
若是動畫使用Pop,那就不用操心手勢響應事件了,但因爲Pop是基於CADisplayLink實現的動畫,它的執行是在主線程中,因此主線程一旦卡頓,那麼動畫也必然卡。而Core Animation的動畫是由系統用一個專用的進程來進行渲染,使用它的好處不用多說了。oop
因爲DanmakuKit使用了Core Animation,所以彈幕在動畫過程當中的展現的實際上是layer而不是view。layer是不支持手勢響應的,所以點擊事件必然也須要特別的實現一下。
實現彈幕在動畫中的點擊並非一件很難的事情,咱們能夠充分利用手勢響應鏈的知識來實現。衆所周知,系統是先找到最上層最適合響應事件的view,再往下找可以響應的view,其中找view的方法就是hitTest。
在hitTest中,系統先會判斷當前的point是否在view的範圍內,若是在的話會從後往前遍歷當前view的subViews數組,將傳入的point轉化爲子view的point繼續傳入調用子view的hitTest方法,直到找到爲止。那麼咱們只要將其中找子view的部分替換爲找當前在播放動畫的layer就行了,代碼以下:
public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
guard self.point(inside: point, with: event) else { return nil }
for i in (0..<subviews.count).reversed() {
let subView = subviews[i]
//若是當前的layer正在播放動畫
if subView.layer.animationKeys() != nil, let presentationLayer = subView.layer.presentation() {
//用動畫的layer判斷一下是否在點擊範圍內
let newPoint = layer.convert(point, to: presentationLayer)
if presentationLayer.contains(newPoint) {
//是的話就找到這個view了
return subView
}
} else {
let newPoint = convert(point, to: subView)
if let findView = subView.hitTest(newPoint, with: event) {
return findView
}
}
}
return nil
}
複製代碼
須要注意的是,Core Animation動畫中獲取實時座標的layer是layer.presentation()。另外,在開發過程當中我發現presentationLayer的實際size與layer並非徹底相同的,所以在計算中最好只使用presentationLayer的座標,不然總會出一些奇奇怪怪的問題。
以前說到渲染彈幕要使用異步隊列,那咱們能不能直接使用GCD的並行隊列呢?答案是不行的,由於隨意使用GCD的並行隊列很容易形成線程數量爆炸,引起內存問題或者使主線程卡死,你們能夠用for循環遍歷1000次來執行GCD的並行隊列任務試試看。
爲了解決這類的問題,咱們必須實現一個隊列池來解決在可控數量的隊列內知足咱們的並行需求。實現原理很簡單,就是建立必定數量的串行隊列存在數組中,每次獲取隊列時經過計數來獲取到不一樣的隊列,下方是一個簡單的實現代碼:
import Foundation
class DanmakuQueuePool {
public let name: String
private var queues: [DispatchQueue] = []
public let queueCount: Int
private var counter: Int = 0
public init(name: String, queueCount: Int, qos: DispatchQoS) {
self.name = name
self.queueCount = queueCount
for _ in 0..<queueCount {
let queue = DispatchQueue(label: name, qos: qos, attributes: [], autoreleaseFrequency: .inherit, target: nil)
queues.append(queue)
}
}
public var queue: DispatchQueue {
return getQueue()
}
private func getQueue() -> DispatchQueue {
if counter == Int.max {
counter = 0
}
let queue = queues[counter % queueCount]
counter += 1
return queue
}
}
複製代碼
歡迎你們使用DanmakuKit,或爲它提供建議。