preface
I have maintained the company’s barrage library before, but due to its heavy historical burden and high transformation cost, IT has not been transformed into a library in the ideal state in my mind. In addition, ON weekends, I also need to do some things to kill time, so I wrote a barrage library that matches my ideal state and open source it: github.com/qyz777/Danm…
At present, DanmakuKit has some basic capabilities, and I have listed some TODO. In the future, I will continue to improve its functions in my spare time on weekends.
Introduction to the
DanmakuKit is a high-performance bullet-screen framework that provides basic bullet-screen functionality and allows you to render a bullet-screen in an asynchronous queue. It provides three types of barrage, namely floating, top and bottom barrage. It currently supports the following features:
- Speed regulation
- Track height control
- Display area adjustment
- Click on the callback
The principle of
Before saying the principle, first put the class diagram of the barrage library. DanmakuKit uses DanmakuView as the main body carrying the barrage, including different DanmakuTrack as the object managing the barrage in view. For users, DanmakuView can automatically create or reuse a DanmakuCell to display danmaku by passing DanmakuCellModel protocol object to DanmakuView.
Performance issues
Before writing the actual code, the performance of the bullet screen must be considered, because if this problem is not considered, once the bullet screen quantity is large, the app use experience will be greatly affected. So in iOS, to get the best performance experience, we can quickly think of a process, that is, asynchronous queue render a bullet screen image, put it in layer.content, and then play it with Core Animation. In addition, there is some overhead of repeatedly creating and destroying objects to manage the barrage, and we need to manage these objects in a proper way.
So to sum up, if we want to achieve a high performance barrage, we must use the following three points:
- reuse
- Asynchronous queue drawing
- Core Animation
reuse
Reuse is an easy idea to think of. In DanmakuKit, the rendering of barrage is implemented by DanmakuCell, which is a subclass of View, so reuse is also in view dimension. The purpose of reusing the view is to reduce the overhead of repeatedly adding subViews and removeFromSuperView, although this performance overhead is not particularly high in actual testing.
Asynchronous queue drawing
In DanmakuKit, CGContext is used to draw the content as a picture and place it in layer.content. If this logic is synchronized in the main thread, then it must be a small overhead, in this case choose asynchronous queue drawing is a good choice. Of course, asynchronous queue drawing has its own disadvantage, which is that thread safety must be considered during code writing.
The principle of asynchronous queue rendering can be found at github.com/ibireme/YYA… There are also a number of blogs on the web that have explained the principles.
Using the Core Animation
If the animation uses Pop, you don’t need to worry about gestures responding to events, but since Pop is implemented with CADisplayLink, it is executed in the main thread, so if the main thread freezes, the animation will also freeze. However, the Animation of Core Animation is rendered by the system with a special process, and the benefits of using it are needless to say.
Click on the event
Because DanmakuKit uses Core Animation, DanmakuKit actually displays layer instead of view during Animation. Layer does not support gesture responses, so click events need to be implemented specifically.
It is not a difficult thing to realize the bullet screen click in the animation, we can make full use of the knowledge of gesture response chain to achieve. As we all know, the system is to find the view that is the most suitable for responding to the event at the top, and then find the view that can respond, and the method to find the view is hitTest.
In hitTest, the system will first judge whether the current point is within the range of the view, if so, it will traverse the subViews array of the current view from back to front, convert the incoming point into the point of the sub-view and pass in the hitTest method of the sub-view until it is found. Instead of finding the child view, replace it with the layer that is currently playing the animation.
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] / / if the current layer is the animation if subView. Layer. AnimationKeys ()! = nil, Let presentationLayer = subView. Layer. The presentation () {/ / animation layer use to judge whether within the scope of the click let newPoint = layer. The convert (point, to: PresentationLayer) if presentationLayer. The contains (newPoint) {/ / is to find the view return subView}} else {let newPoint = convert(point, to: subView) if let findView = subView.hitTest(newPoint, with: event) { return findView } } } return nil }Copy the code
Note that the layer in Core Animation that gets real-time coordinates is layer.presentation(). In addition, during development I found that the actual size of the presentationLayer is not exactly the same as the layer, so it is best to use only the coordinates of the presentationLayer in calculations, otherwise there will always be some weird problems.
Queue pool
As mentioned earlier, we should use asynchronous queue for rendering barrage, so can we use parallel queue of GCD directly? The answer is no, because random use of parallel queues in the GCD can easily explode the number of threads, cause memory problems or cause the main thread to freeze. You can try to execute parallel queue tasks in the GCD by iterating through the for loop 1000 times.
To solve this kind of problem, we must implement a queue pool to solve our concurrency requirements within a controllable number of queues. The implementation principle is very simple, is to create a certain number of serial queues exist in the array, each time to obtain the queue by counting to obtain different queues, below is a simple implementation code:
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 } }Copy the code
Write in the last
You are welcome to use DanmakuKit or provide suggestions for it.