iOS performance optimization - asynchronous drawing and asynchronous underlying View processing

Foreword:

The performance optimization based on UIKit seems to have reached a bottleneck. Whether it is using frame proxy snpakit, cache height, reducing layout levels, diff refreshing, compressing pictures, selecting appropriate queues, and selecting high-performance locks, it cannot satisfy the current large and complex project optimization. . The VC loads for a long time every time it is loaded and refreshed, or the collectionView freezes when it is refreshed, I am really at a loss. So, is there a more advanced optimization method than the above? The answer is yes, that is asynchronous drawing.

(For asynchronous drawing content, a better treatment is to choose mature works such as YYText or AsyncKit. This article is limited to the introduction, please do not use the examples directly in the production environment!)

Draw asynchronously:

UIButton, UILabel, UITableViewand other spaces are drawn synchronously on the main thread, that is to say, if these controls are complex in business, it may cause the interface to freeze. Fortunately, the system has opened a hole that allows us to draw by ourselves, of course, it is processed on the sub-thread and then returned to the main thread for display.

The general principle is that UIViewas a display class, it is actually responsible for event and touch transmission, and the real class is responsible for display CALayer. layerAs long as the drawing on the child thread can be controlled , the asynchronous drawing operation is completed.

1. Principle:

Follow the sequence below:

  • Inherited from CALayercreating an asynchronous Layer. Since the representation of Label is text and the representation of ImageView is image, it can be used Contextto draw geometric information such as text and pictures.
  • Create and manage threads dedicated to asynchronous drawing.
  • Every time the drawing for a certain control receives the drawing signal (such as setting text, color and other attributes), it will draw.

The rough steps are the above. However, the actual situation will be more complicated, the following is the introduction of each operation.

2. Queue pool:

Regarding the processing of sub-threads, choose here GCDinstead of other multi-threaded classes. The principle of selection is as follows: too many sub-threads continuously switching contexts will obviously bring performance loss, so you can choose an asynchronous serial queue to execute the drawing tasks serially to avoid the overhead caused by frequent context switching.

As for the number of queues, this can be based on the number of cores the processor works on (small cores are not counted). Each queue can roughly set an attribute of the current number of tasks to facilitate finding the lightest queue for the current drawing task to process.

In this way, every time a drawing task comes, one will be taken from the queue pool, and if there is no one, it will be created. When the drawing task is canceled, the number of current tasks in the current queue is given -1.

code show as below:

Queue pool management class:

import Foundation

final class SGAsyncQueuePool {
    
    
    
    public static let singleton: SGAsyncQueuePool = {
    
     SGAsyncQueuePool() }()
    
    private lazy var queues: Array<SGAsyncQueue> = {
    
     Array<SGAsyncQueue>() }()
    
    private lazy var maxQueueCount: Int = {
    
    
        ProcessInfo.processInfo.activeProcessorCount > 2 ? ProcessInfo.processInfo.activeProcessorCount : 2
    }()
    
    /**
     Get a serial queue with a balanced rule by `taskCount`.
     - Note: The returned queue's  sum is under the CPU active count forever.
     */
    public func getTaskQueue() -> SGAsyncQueue {
    
    
        // If the queues is doen't exist, and create a new async queue to do.
        if queues.count < maxQueueCount {
    
    
            let asyncQueue: SGAsyncQueue = SGAsyncQueue()
            asyncQueue.taskCount = asyncQueue.taskCount + 1
            queues.append(asyncQueue)
            return asyncQueue
        }
        
        // Find the min task count in queues inside.
        let queueMinTask: Int = queues.map {
    
     $0.taskCount }.sorted {
    
     $0 > $1 }.first ?? 0
        
        // Find the queue that task count is min.
        guard let asyncQueue: SGAsyncQueue = queues.filter({
    
     $0.taskCount <= queueMinTask }).first else {
    
    
            let asyncQueue: SGAsyncQueue = SGAsyncQueue()
            asyncQueue.taskCount = asyncQueue.taskCount + 1
            queues.append(asyncQueue)
            return asyncQueue
        }
        
        asyncQueue.taskCount = asyncQueue.taskCount + 1
        queues.append(asyncQueue)
        return asyncQueue
    }
    
    /**
     Indicate a queue to stop.
     */
    public func stopTaskQueue(_ queue: SGAsyncQueue){
    
    
        queue.taskCount = queue.taskCount - 1
        if queue.taskCount <= 0 {
    
    
            queue.taskCount = 0
        }
    }  
    
}

Queue model:

final class SGAsyncQueue {
    
    
    
    public var queue: DispatchQueue = {
    
     dispatch_queue_serial_t(label: "com.sg.async_draw.queue", qos: .userInitiated) }()
    
    public var taskCount: Int = 0
    
    public var index: Int = 0
}

3. Affairs:

As mentioned above, draw every time a drawing signal comes. However, the drawing is done globally, that is to say, if the x value of the frame may be changed, the entire text content will be redrawn, which is a bit too wasteful of resources. Can these drawing signals be processed at the same time? The answer is the RunLoop cycle. This timing can be placed before the current RunLoop goes to sleep and when it exits.

Another situation is how to handle requests for the same drawing signal? That is to filter the weight, and only execute one. This can put the drawing task inside Setinstead of Arrayinside.

Plot a model of task signals:

final fileprivate class AtomicTask: NSObject {
    
    
    
    public var target: NSObject!
    public var funcPtr: Selector!
    
    init(target: NSObject!, funcPtr: Selector!) {
    
    
        self.target = target
        self.funcPtr = funcPtr
    }
    
    override var hash: Int {
    
    
        target.hash + funcPtr.hashValue
    }
    
}

You can see that the attributes are rewritten here hash, and the hash of the signal host and the hash of the signal are added together to determine whether it is a repeated task (target is the signal host, funcPtr is the signal).

Register the callback at the specified timing in RunLoop.


final class SGALTranscation {
    
    
    
    /** The task that need process in current runloop. */
    private static var tasks: Set<AtomicTask> = {
    
     Set<AtomicTask>() }()
    
    /** Create a SGAsyncLayer Transcation task. */
    public init (target: NSObject, funcPtr: Selector) {
    
    
        SGALTranscation.tasks.insert(AtomicTask(target: target, funcPtr: funcPtr))
    }
    
    /** Listen the runloop's change, and execute callback handler to process task. */
    private func initTask() {
    
    
        DispatchQueue.once(token: "sg_async_layer_transcation") {
    
    
            let runloop    = CFRunLoopGetCurrent()
            let activities = CFRunLoopActivity.beforeWaiting.rawValue | CFRunLoopActivity.exit.rawValue
            let observer   = CFRunLoopObserverCreateWithHandler(nil, activities, true, 0xFFFFFF) {
    
     (ob, ac) in
                guard SGALTranscation.tasks.count > 0 else {
    
     return }
                SGALTranscation.tasks.forEach {
    
     $0.target.perform($0.funcPtr) }
                SGALTranscation.tasks.removeAll()
            }
            CFRunLoopAddObserver(runloop, observer, .defaultMode)
        }
    }
    
    /** Commit  the draw task into runloop. */
    public func commit(){
    
    
        initTask()
    }
    
}


extension DispatchQueue {
    
    
    
    private static var _onceTokenDictionary: [String: String] = {
    
     [: ] }()
    
    /** Execute once safety. */
    static func once(token: String, _ block: (() -> Void)){
    
    
        defer {
    
     objc_sync_exit(self) }
        objc_sync_enter(self)
        
        if _onceTokenDictionary[token] != nil {
    
    
            return
        }

        _onceTokenDictionary[token] = token
        block()
    }
    
}

A little trick is used here. There is no oc dispatch_oncethread-safe method that executes only once in swift. Here, a dispatch_oncethread-safe method that is similar to only execute once is constructed with the enter and exit processing of objc_sync.

When the drawing class signals that it needs to draw, it passes to SGALTranscationcreate a transaction and then commit(). commit()The method is actually a listener that puts the drawing task in Setand then starts it RunLoop. Since it is DispatchQueue.once()a method, RunLoopcallbacks can be created and used with peace of mind.

4.Layer processing:

This is easy to understand, we handle most of the content of the underlying asynchronous drawing layer, and then just implement the drawing class.

import UIKit
import CoreGraphics
import QuartzCore

/**
 Implements this protocol and override following methods.
 */
@objc protocol SGAsyncDelgate {
    
    
    
    /**
     Override this method to custome the async view.
     - Parameter layer: A layer to present view, which is foudation of custome view.
     - Parameter context: Paint.
     - Parameter size: Layer size, type of CGSize.
     - Parameter cancel: A boolean value that tell callback method the status it experienced.
     */
    @objc func asyncDraw(layer: CALayer, in context: CGContext, size: CGSize, isCancel cancel: Bool)
    
}

class SGAsyncLayer: CALayer {
    
    
    
    /**
     A boolean value that indicate the layer ought to draw in async mode or sync mode. Sync mode is slow to draw in UI-Thread, and async mode is fast in special sub-thread to draw but the memory is bigger than sync mode. Default is `true`.
     */
    public var isEnableAsyncDraw: Bool = true
    
    /** Current status of operation in current runloop. */
    private var isCancel: Bool = false
    
    override func setNeedsDisplay() {
    
    
        self.isCancel = true
        super.setNeedsDisplay()
    }
    
    override func display() {
    
    
        self.isCancel = false
        
        // If the view could responsed the delegate, and executed async draw method.
        if let delegate = self.delegate {
    
    
            if delegate.responds(to: #selector(SGAsyncDelgate.asyncDraw(layer:in:size:isCancel:))) {
    
    
                self.setDisplay(true)
            } else {
    
    
                super.display()
            }
        } else {
    
    
            super.display()
        }
    }
    
}

extension SGAsyncLayer {
    
    
    
    private func setDisplay(_ async: Bool){
    
    
        guard let delegate = self.delegate as? SGAsyncDelgate else {
    
     return }
        // Get the task queue for async draw process.
        let taskQueue: SGAsyncQueue = SGAsyncQueuePool.singleton.getTaskQueue()

        if async {
    
    
            taskQueue.queue.async {
    
    
                
                // Decrease the queue task count.
                if self.isCancel {
    
    
                    SGAsyncQueuePool.singleton.stopTaskQueue(taskQueue)
                    return
                }
                
                let size: CGSize = self.bounds.size
                let scale: CGFloat = UIScreen.main.nativeScale
                let opaque: Bool = self.isOpaque
                
                UIGraphicsBeginImageContextWithOptions(size, opaque, scale)
                
                guard let context: CGContext = UIGraphicsGetCurrentContext() else {
    
     return }
                if opaque {
    
    
                    context.saveGState()
                    context.setFillColor(self.backgroundColor ?? UIColor.white.cgColor)
                    context.setStrokeColor(UIColor.clear.cgColor)
                    context.addRect(CGRect(x: 0, y: 0, width: size.width, height: size.height))
                    context.fillPath()
                    context.restoreGState()
                }
                
                // Provide an async draw callback method for UIView.
                delegate.asyncDraw(layer: self, in: context, size: size, isCancel: self.isCancel)
                
                // Decrease the queue task count.
                if self.isCancel {
    
    
                    SGAsyncQueuePool.singleton.stopTaskQueue(taskQueue)
                    return
                }
                
                guard let image: UIImage = UIGraphicsGetImageFromCurrentImageContext() else {
    
     return }
                UIGraphicsEndImageContext()
                
                // End this process.
                SGAsyncQueuePool.singleton.stopTaskQueue(taskQueue)
                DispatchQueue.main.async {
    
    
                    self.contents = image.cgImage
                }
                 
            }
        } else {
    
    
            
            SGAsyncQueuePool.singleton.stopTaskQueue(taskQueue)
        }
    }
    
}

The drawing agent at the top only needs to implement the drawing class, and then the drawing class will draw text and other content by itself according to the contextinformation .sizeimage

In the custom asynchronous drawing, Layerrewrite displaythe method to contextprepare the object, and then throw the proxy method out for the drawing class to implement. Finally, the asynchronous drawing Layergets the operated one and contextreturns to the main thread contents, and the content is displayed. .

5. View implementation class processing:

Using CoreText and other content to draw text will not go into details, just go to the code of Label:

import UIKit

class AsyncLabel: UIView, SGAsyncDelgate {
    
    
    
    public var text: String = "" {
    
    
        didSet {
    
     SGALTranscation(target: self, funcPtr: #selector(drawTask)).commit() }
    }

    public var textColor: UIColor = .black {
    
    
        didSet {
    
     SGALTranscation(target: self, funcPtr: #selector(drawTask)).commit() }
    }

    public var font: UIFont = .systemFont(ofSize: 14) {
    
    
        didSet {
    
     SGALTranscation(target: self, funcPtr: #selector(drawTask)).commit() }
    }
    
    public var lineSpacing: CGFloat = 3 {
    
    
        didSet {
    
     SGALTranscation(target: self, funcPtr: #selector(drawTask)).commit() }
    }
    
    public var textAlignment: NSTextAlignment = .left {
    
    
        didSet {
    
     SGALTranscation(target: self, funcPtr: #selector(drawTask)).commit() }
    }
    
    public var lineBreakMode: NSLineBreakMode = .byTruncatingTail {
    
    
        didSet {
    
     SGALTranscation(target: self, funcPtr: #selector(drawTask)).commit() }
    }

    public var attributedText: NSAttributedString? {
    
    
        didSet {
    
     SGALTranscation(target: self, funcPtr: #selector(drawTask)).commit() }
    }
    
    public var size: CGSize {
    
    
        get {
    
     getTextSize() }
    }
    
    override class var layerClass: AnyClass {
    
    
        SGAsyncLayer.self
    }
    
    @objc func drawTask(){
    
    
        self.layer.setNeedsDisplay()
    }
    
    func asyncDraw(layer: CALayer, in context: CGContext, size: CGSize, isCancel cancel: Bool) {
    
    
        if cancel {
    
    
            return
        }
        
        let size: CGSize = layer.bounds.size
        context.textMatrix = CGAffineTransformIdentity
        context.translateBy(x: 0, y: size.height)
        context.scaleBy(x: 1, y: -1)
        
        let drawPath: CGMutablePath = CGMutablePath()
        drawPath.addRect(CGRect(origin: .zero, size: size))
        
        self.attributedText = self.generateAttributedString()
        guard let attributedText = self.attributedText else {
    
     return }
        
        let ctfFrameSetter: CTFramesetter = CTFramesetterCreateWithAttributedString(attributedText)
        let ctfFrame: CTFrame = CTFramesetterCreateFrame(ctfFrameSetter, CFRange(location: 0, length: attributedText.length), drawPath, nil)
        CTFrameDraw(ctfFrame, context)
    }
    
    private func generateAttributedString() -> NSAttributedString {
    
    
        let style: NSMutableParagraphStyle = NSMutableParagraphStyle()
        style.lineSpacing = self.lineSpacing
        style.alignment = self.textAlignment
        style.lineBreakMode = self.lineBreakMode
        style.paragraphSpacing = 5
        
        let attributes: Dictionary<NSAttributedString.Key, Any> = [
            NSAttributedString.Key.font: self.font,
            NSAttributedString.Key.foregroundColor: self.textColor,
            NSAttributedString.Key.backgroundColor: UIColor.clear,
            NSAttributedString.Key.paragraphStyle: style,
        ]

        return NSAttributedString(string: self.text, attributes: attributes)
    }
    
    private func getTextSize() -> CGSize {
    
    
        guard let attributedText = self.attributedText else {
    
     return .zero }
        return attributedText.boundingRect(with: CGSize(width: self.frame.size.width, height: CGFLOAT_MAX),
                                           context: nil).size
    }
    
}

Code for ImageView:


import UIKit

class AsyncImageView: UIView, SGAsyncDelgate {
    
    
    
    public var image: UIImage? {
    
    
        didSet {
    
     SGALTranscation(target: self, funcPtr: #selector(drawTask)).commit() }
    }
    
    public var quality: CGFloat = 0.9 {
    
    
        didSet {
    
     SGALTranscation(target: self, funcPtr: #selector(drawTask)).commit() }
    }
    
    override class var layerClass: AnyClass {
    
    
        SGAsyncLayer.self
    }
    
    @objc func drawTask() {
    
    
        self.layer.setNeedsDisplay()
    }
    
    func asyncDraw(layer: CALayer, in context: CGContext, size: CGSize, isCancel cancel: Bool) {
    
    
        if cancel {
    
    
            return
        }
        
        let size: CGSize = layer.bounds.size
        context.textMatrix = CGAffineTransformIdentity
        context.translateBy(x: 0, y: size.height)
        context.scaleBy(x: 1, y: -1)
        
        guard let image = self.image else {
    
     return }
        guard let jpedData: Data = image.jpegData(compressionQuality: self.quality) else {
    
     return }
        guard let cgImage = UIImage(data: jpedData)?.cgImage else {
    
     return }
        context.draw(cgImage, in: CGRect(origin: .zero, size: size))
    }

}

How to use it? How to use UILabel usually, how to use UIIMageView how to use it here, no more details.

performance

When the drawing task is relatively small and light, using system controls such as UILabel is very fast. When the drawing tasks are many and complex, the speed of asynchronous drawing is highlighted. 500 UIImageView displaying 70k pictures is approx 120ms, while AsyncImageView is 70ms. But the price is that the memory usage is more than doubled (the memory usage drops to the normal level after the interface stops). This may be a reverse optimization on low-end devices, although the iPhone 14 Pro now has 6GB of memory. But the weak 1GB of the old iPhone 6 also needs to be considered how to deal with it. (Criticize a certain food delivery platform by name here. When I was in school, I opened the app and looked at a hotel when I was using iPhone6s Plus. WeChat was directly killed by the background. I will know why this happened below.)

Asynchronous underlying View processing:

Asynchronous underlying View processing is because I didn't think of how to accurately call this practice, so I will use this awkward name for the time being.

As mentioned above, you can customize the drawing AsyncLabel, AsyncImageView, AsyncSwitch, AsyncButtonand other content, but many asynchronous drawing also have overhead, can you put them in an asynchronous View for processing? The answer is yes, and many companies use it.

A food delivery platform previously open-sourced a project called Graver, and then deleted the library. The general principle is the detailed version of YYText. This general idea is also used here to realize it.

1. Abstract proxy:

Here, objects such as Label, ImageView, and Button are abstracted as models for processing.

import UIKit

protocol NodeLayerDelegate: NSObject {
    
    
    
    var contents: (Any & NSObject)? {
    
     set get }
    
    var backgroundColor: UIColor {
    
     set get }
    
    var frame: CGRect {
    
     set get }
    
    var hidden: Bool {
    
     set get }
    
    var alpha: CGFloat {
    
     set get }
    
    var superView: NodeLayerDelegate? {
    
     get }
    
    var paintSignal: Bool {
    
     set get }
    
    func setOnTapListener(_ listerner: (() -> Void)?)
    
    func setOnClickListener(_ listerner: (() -> Void)?)
    
    func didReceiveTapSignal()
    
    func didReceiveClickSignal()
    
    func removeFromSuperView()
    
    func willLoadToSuperView()
    
    func didLoadToSuperView()
    
    func setNeedsDisplay()
    
}

2. Abstract drawing base class:

Create a low-level drawing base class, all ImageView, Label and other controls can be placed here to perform drawing. Of course, the abstract drawing base class here is based on UIView and then implements the asynchronous drawing agent in the previous chapter.

import UIKit

class NodeRootView: UIView, SGAsyncDelgate {
    
    
    
    override class var layerClass: AnyClass {
    
    
        SGAsyncLayer.self
    }
    
    public var subNodes: Array<NodeLayerDelegate> = {
    
     Array<NodeLayerDelegate>() }()

}

// MARK: - Draw Node
extension NodeRootView {
    
    
    
    @objc private func drawTask() {
    
    
        self.layer.setNeedsDisplay()
    }
    
    public func addSubNode(_ node: NodeLayerDelegate) {
    
    
        node.willLoadToSuperView()
        self.subNodes.append(node)
        SGALTranscation(target: self, funcPtr: #selector(drawTask)).commit()
    }
    
    func asyncDraw(layer: CALayer, in context: CGContext, size: CGSize, isCancel cancel: Bool) {
    
    
        if cancel {
    
    
            return
        }
        
        drawNodeImages(layer: layer, in: context, size: size)
        drawNodeLabels(layer: layer, in: context, size: size)
        drawNodeButtons(layer: layer, in: context, size: size)
    }
    
    private func drawNodeButtons(layer: CALayer, in context: CGContext, size: CGSize) {
    
    
        let nodes: Array<NodeLayerDelegate> = self.subNodes.filter {
    
     $0.isMember(of: NodeButton.self) }
        let nodeButtons: Array<NodeButton> = nodes.map {
    
     $0 as! NodeButton }
        
        nodeButtons.forEach {
    
     button in
            let tempFrame: CGRect = CGRect(x: button.frame.minX,
                                           y: layer.bounds.height - button.frame.maxY,
                                           width: button.frame.width,
                                           height: button.frame.height)
            let drawPath: CGMutablePath = CGMutablePath()
            drawPath.addRect(tempFrame)
            
            UIColor(cgColor: button.backgroundColor.cgColor).setFill()
            let bezierPath: UIBezierPath = UIBezierPath(roundedRect: tempFrame, cornerRadius: button.cornerRadius)
            bezierPath.lineCapStyle = CGLineCap.round
            bezierPath.lineJoinStyle = CGLineJoin.round
            bezierPath.fill()
            
            let style: NSMutableParagraphStyle = NSMutableParagraphStyle()
            style.lineSpacing = 3
            style.alignment = .center
            style.lineBreakMode = .byTruncatingTail
            style.paragraphSpacing = 5
            
            let attributes: Dictionary<NSAttributedString.Key, Any> = [
                NSAttributedString.Key.font: UIFont.systemFont(ofSize: 12),
                NSAttributedString.Key.foregroundColor: button.textColor,
                NSAttributedString.Key.backgroundColor: button.backgroundColor,
                NSAttributedString.Key.paragraphStyle: style,
            ]
            let attributedText: NSAttributedString = NSAttributedString(string: button.text, attributes: attributes)
            
            let ctfFrameSetter: CTFramesetter = CTFramesetterCreateWithAttributedString(attributedText)
            let cfAttributes: CFDictionary = [
                kCTFrameProgressionAttributeName: CTFrameProgression.topToBottom.rawValue as CFNumber
            ] as CFDictionary
            let ctfFrame: CTFrame = CTFramesetterCreateFrame(ctfFrameSetter, CFRange(location: 0, length: attributedText.length), drawPath, cfAttributes)
//            let line = CTLineCreateWithAttributedString(attributedText)
//            let offset = CTLineGetPenOffsetForFlush(line, 0.5, button.frame.width)
//            var ascent: CGFloat = 0, descent: CGFloat = 0, leading: CGFloat = 0
//            CTLineGetTypographicBounds(line, &ascent, &descent, &leading)
//            let lineHeight = ascent + descent + leading
            context.textPosition = CGPoint(x: button.frame.width, y: (button.frame.height - 10)/2.0)
//            CTLineDraw(line, context)
            CTFrameDraw(ctfFrame, context)
            
            button.didLoadToSuperView()
        }
    }
    
    private func drawNodeLabels(layer: CALayer, in context: CGContext, size: CGSize) {
    
    
        let nodes: Array<NodeLayerDelegate> = self.subNodes.filter {
    
     $0.isMember(of: NodeLabel.self) }
        let nodeLabels: Array<NodeLabel> = nodes.map {
    
     $0 as! NodeLabel }
        
        nodeLabels.forEach {
    
     label in
            let tempFrame: CGRect = CGRect(x: label.frame.minX,
                                           y: layer.bounds.height - label.frame.maxY,
                                           width: label.frame.width,
                                           height: label.frame.height)
            let drawPath: CGMutablePath = CGMutablePath()
            drawPath.addRect(tempFrame)
            
            let style: NSMutableParagraphStyle = NSMutableParagraphStyle()
            style.lineSpacing = 3
            style.alignment = .left
            style.lineBreakMode = .byTruncatingTail
            style.paragraphSpacing = 5
            
            let attributes: Dictionary<NSAttributedString.Key, Any> = [
                NSAttributedString.Key.font: label.font,
                NSAttributedString.Key.foregroundColor: label.textColor,
                NSAttributedString.Key.backgroundColor: label.backgroundColor,
                NSAttributedString.Key.paragraphStyle: style,
            ]
            let attributedText: NSAttributedString = NSAttributedString(string: label.text, attributes: attributes)
            
            let ctfFrameSetter: CTFramesetter = CTFramesetterCreateWithAttributedString(attributedText)
            let ctfFrame: CTFrame = CTFramesetterCreateFrame(ctfFrameSetter, CFRange(location: 0, length: attributedText.length), drawPath, nil)
            CTFrameDraw(ctfFrame, context)
            
            label.didLoadToSuperView()
        }
        
    }
    
    private func drawNodeImages(layer: CALayer, in context: CGContext, size: CGSize) {
    
    
        let nodes: Array<NodeLayerDelegate> = self.subNodes.filter {
    
     $0.isMember(of: NodeImageView.self) }
        let nodeImageViews: Array<NodeLayerDelegate> = nodes.map {
    
     $0 as! NodeImageView }
        
        let size: CGSize = layer.bounds.size
        context.textMatrix = CGAffineTransformIdentity
        context.translateBy(x: 0, y: size.height)
        context.scaleBy(x: 1, y: -1)
        
        nodeImageViews.forEach {
    
    
            if let image = $0.contents as? UIImage, let cgImage = image.cgImage {
    
    
                context.draw(cgImage, in: $0.frame)
            }
        }
    }
    
}

// MARK: - Touch Process
extension NodeRootView {
    
    
    
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
    
    
        guard let touch: UITouch = event?.touches(for: self)?.first else {
    
     return }
        let touchPoint: CGPoint = touch.location(in: self)
        
        for node in self.subNodes {
    
    
            let isInX: Bool = touchPoint.x >= node.frame.minX && touchPoint.x <= node.frame.maxX
            let isInY: Bool = touchPoint.y >= node.frame.minY && touchPoint.y <= node.frame.maxY
            if isInX && isInY {
    
    
                node.didReceiveTapSignal()
                break
            }
        }
    }
    
    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
    
    
        guard let touch: UITouch = event?.touches(for: self)?.first else {
    
     return }
        let touchPoint: CGPoint = touch.location(in: self)
        
        for node in self.subNodes {
    
    
            let isInX: Bool = touchPoint.x >= node.frame.minX && touchPoint.x <= node.frame.maxX
            let isInY: Bool = touchPoint.y >= node.frame.minY && touchPoint.y <= node.frame.maxY
            if isInX && isInY {
    
    
                node.didReceiveClickSignal()
                break
            }
        }
    }
    
}

In drawTaskthe method, different processing is performed according to different types of drawing objects, such as ImageView drawing image, Label drawing text, Button drawing text and processing gestures.

The idea of ​​gesture processing here is: find the currently clicked point, then iterate the current subNodes to see if the point is within the frame of the node, and if so, call the gesture signal. Tap analogy UIControl.touchUp, click analogy UIControl.touchUpInside.

3. View implementation class:

Take ImageViewfor example:

import UIKit

class NodeImageView: NSObject, NodeLayerDelegate {
    
    

    var contents: (Any & NSObject)?
    
    var backgroundColor: UIColor = .white
    
    var frame: CGRect = .zero
    
    var hidden: Bool = false
    
    var alpha: CGFloat = 1.0
    
    var superView: NodeLayerDelegate?
    
    var paintSignal: Bool = false
    
    private var didReceiveTapBlock: (() -> Void)?
    
    private var didReceiveClickBlock: (() -> Void)?
    
    func setOnTapListener(_ listerner: (() -> Void)?) {
    
    
        didReceiveTapBlock = {
    
    
            listerner?()
        }
    }
    
    func setOnClickListener(_ listerner: (() -> Void)?) {
    
    
        didReceiveClickBlock = {
    
    
            listerner?()
        }
    }
    
    func didReceiveTapSignal() {
    
    
        didReceiveTapBlock?()
    }
    
    func didReceiveClickSignal() {
    
    
        didReceiveClickBlock?()
    }
    
    func removeFromSuperView() {
    
    
        
    }
    
    func willLoadToSuperView() {
    
    
        
    }
    
    func didLoadToSuperView() {
    
    
        
    }

    func setNeedsDisplay() {
    
    
        
    }
    
}

use

This time is a little different from the previous chapter.

class NodeCell: UITableViewCell {
    
    
    
    lazy var nodeView: NodeRootView = {
    
    
        let view = NodeRootView()
        view.frame = CGRect(x: 0, y: 100, width: kSCREEN_WIDTH, height: 100)
        return view
    }()
    
    lazy var nodeLabel: NodeLabel = {
    
    
        let label = NodeLabel()
        label.text = "Node Label"
        label.frame = CGRect(x: 118, y: 10, width: 100, height: 20)
        return label
    }()
    
    lazy var nodeTitle: NodeLabel = {
    
    
        let label = NodeLabel()
        label.text = "Taylor Swift - <1989> land to Music."
        label.frame = CGRect(x: 118, y: 100 - 10 - 20, width: 200, height: 20)
        return label
    }()
    
    lazy var nodeImageView: NodeImageView = {
    
    
        let imageView = NodeImageView()
        imageView.frame = CGRect(x: 10, y: 10, width: 80, height: 80)
        imageView.contents = UIImage(named: "taylor")
        imageView.setOnClickListener {
    
    
            Log.debug("click node imageView")
        }
        return imageView
    }()
    
    lazy var nodeButton: NodeButton = {
    
    
        let button = NodeButton()
        button.text = "Buy"
        button.backgroundColor = .orange
        button.textColor = .white
        button.frame = CGRect(x: kSCREEN_WIDTH - 60, y: 65, width: 40, height: 19)
        button.setOnClickListener {
    
    
            Log.debug("Buy")
        }
        return button
    }()
    
    required init?(coder aDecoder: NSCoder) {
    
    
        super.init(coder: aDecoder)
    }
    
    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
    
    
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        self.selectionStyle = .none
        
        self.contentView.addSubview(nodeView)
        self.nodeView.addSubNode(nodeLabel)
        self.nodeView.addSubNode(nodeImageView)
        self.nodeView.addSubNode(nodeTitle)
        self.nodeView.addSubNode(nodeButton)
    }
    
}

Since NodeImageViewwait is not UIViewa subclass of , so we can't addSubview(), we can only use our base class to addSubNode().

insert image description here

It can be seen that the layout level here is very shallow, which is very suitable for Caton optimization.

However, the optimization of this idea is not a panacea, and it consumes more memory than the asynchronous drawing of a single control in the previous chapter. Moreover, functions such as Label and ImageView must be consistent with the system, otherwise a more complicated business requirement will directly veto this thing. Such as animation and snapkit can't be used, and can only be processed with static content.

Github address:

https://github.com/mcry416/SGAsyncView

Guess you like

Origin blog.csdn.net/kicinio/article/details/131036581