iOS性能优化-异步绘制与异步底层View处理

news2024/12/24 8:21:55

前言:

基于UIKit的性能优化似乎已经到了瓶颈,无论是使用frame代理snpakit,缓存高度,减少布局层次,diff刷新,压缩图片,选择合适队列,选择高性能锁,也不能满足当前庞大而又复杂的项目优化。每次加载刷新的时候过长时间的VC加载,或者collectionView刷新的时候卡顿,真是有点不知所措。那么,有没有比上述内容更高级的优化方法呢?答案是有的,那就是异步绘制。

(有关异步绘制内容,更好的处理是选择YYText或者AsyncKit这些成熟的作品,本文仅限于介绍入门,请不要将示例直接用于生产环境!)

异步绘制:

UIButtonUILabelUITableView等空间是同步绘制在主线程上的,也就是说,如果这些控件在业务上表现很复杂,那么有可能就会导致界面卡顿。好在系统开了一个口子可以让我们自己去绘制,当然是在子线程上处理然后回到主线程上显示。

大致原理就是UIView作为一个显示类,它实际上是负责事件与触摸传递的,真正负责显示的类是CALayer。只要能控制layer在子线程上绘制,就完成了异步绘制的操作。

1.原理:

按照以下顺序操作:

  • 继承于CALayer创建一个异步Layer。由于Label的表征是text文本,ImageView的表征是image图像,那可以利用Context去绘制文本、图片等几何信息。
  • 创建并管理一些线程专用于异步绘制。
  • 每次针对某个控件的绘制接受到了绘制信号(如设置text,color等属性)就绘制。

粗略步骤就是以上内容。然而实际情况会更复杂,下面是每个操作的介绍。

2.队列池:

关于子线程的处理,这里选择GCD而不是其它多线程类。选择的原理如下:过多子线程不断切换上下文会明显带来性能损耗,那可以选择异步串行队列将绘制任务串行方式去执行,避免频繁切换上下文带来的开销。

而队列的个数,这可以根据处理器工作的核数来(小核不算)。每个队列又可以粗略地设置一个属性当前任务数来方便找出当前绘制任务最轻的队列去处理。

这样每次有绘制任务来的时候,就从队列池里面取一个,没有就创建。绘制任务取消的时候就把当前队列的当前任务数给-1

代码如下:

队列池管理类:

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
        }
    }  
    
}

队列模型:

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.事务:

上文提到,每次有绘制信号来临的时候就绘制。然而绘制是全局进行的,也就是说,可能改了一下frame的x值整个文本内容就要重新绘制,这未免有点太浪费资源了。那能不能把这些绘制信号统一放个时机去处理呢?答案就是RunLoop的循环。这一时机可以放在当前RunLoop的在休眠之前与退出的时候。

还有一种情况就是相同绘制信号的请求如何处理?那就是滤重了,只执行一个。这一点可以把绘制任务放在Set而不是Array里面。

绘制任务信号的模型:

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
    }
    
}

可以看到这里重写了hash属性,拿信号宿主的hash与信号的hash加在一起来判断是否为重复任务(target为信号宿主,funcPtr为信号)。

在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()
    }
    
}

这里用到了一个小技巧,swift中没有oc的dispatch_one仅执行一次的线程安全方法,这里以objc_sync的enter与exit处理构造了一个类似dispatch_one仅执行一次的线程安全方法。

当绘制类发出信号需要绘制时,就通过SGALTranscation来创建一个事务然后commit()。commit()方法实际上是将绘制任务放入Set中然后开启RunLoop的监听。由于是DispatchQueue.once()方法,所以RunLoop回调可以安心创建使用。

4.Layer处理:

这就很好理解了,我们把底层异步绘制layer的大部分内容处理好,然后绘制类去实现就好了。

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)
        }
    }
    
}

最顶部的绘制代理只需要绘制类实现就行,然后绘制类根据context,size等信息自己去绘制文本,image等内容。

在自定义的异步绘制Layer里面重写display方法用以把context对象准备好,然后把代理方法抛出去让绘制类去实现,最后异步绘制Layer拿到被操作的context回到主线程赋给contents,内容就展示出来了。

5.View实现类处理:

利用CoreText等内容绘制文本就不再赘述了,直接上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
    }
    
}

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))
    }

}

怎么使用呢?平常UILabel怎么使用的,UIIMageView怎么使用的这里就怎么使用的,不再赘述。

性能

当绘制任务比较小很轻的时候,使用UILabel等系统控件速度很快。当绘制任务较多很复杂的时候就凸显了异步绘制的速度了。500个UIImageView显示70k的图片大约为120ms,而AsyncImageView为70ms。但是代价是使用内存提高了不止一倍(界面停止以后内存使用下降到正常水平)。这在低端设备上可能反而是个反向优化,虽然现在iPhone14 Pro都6GB内存了。但老旧的iPhone6孱弱的1GB也要考虑如何处理。(这里点名批评某外卖平台,当年上学的时候我用iPhone6s Plus的时候打开app再看个酒店,微信直接被后台杀了。下文就知道为什么会出现这个情况)

异步底层View处理:

异步底层View处理,是因为我没想到如何准确称呼这种做法为好,暂时用这种拗口的名字称呼。

上文提到,可以自定义绘制AsyncLabel、AsyncImageView、AsyncSwitch、AsyncButton等内容,然而诸多的异步绘制也是有开销的,能不能把它们统一放到一个异步View里面去处理呢?答案是可以,也有不少公司落地使用。

某外卖平台以前开源过一个项目为Graver,后来删库了。大致原理就是细致化版本的YYText。这里也用这种大概思路去实现一下。

2.抽象代理:

这里其实是把Label、ImageView、Button等对象抽象为模型去处理。

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.抽象绘制基类:

创建一个底层的绘制基类,所有的ImageView、Label等控件可以放到这里去执行绘制。当然这里抽象绘制基类是基于UIView的然后实现上一章的异步绘制代理。

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
            }
        }
    }
    
}

在drawTask方法里面根据不同类型的绘制对象去做不同的处理,如ImageView绘制image,Label绘制文本,Button绘制文本与处理手势。

这里手势处理思路为:找到当前点击的point,然后迭代当前subNodes,看point是否在node的frame内,如果是则调用手势信号。tap类比UIControl.touchUp,click类比UIControl.touchUpInside。

3.View实现类:

以ImageView为例:

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() {
        
    }
    
}

使用

这里的时候就与上一章有点不同了。

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)
    }
    
}

由于NodeImageView等不是UIView的子类,所以不能addSubview(),只能用我们的基类去addSubNode()。

在这里插入图片描述

可以看到这里布局层次很浅,很适合卡顿优化。

但是,这种思路的优化也不是万能的,它比上一章的单个控件的异步绘制还要耗内存。而且像Label、ImageView等的功能要做到与系统一致,不然一个复杂一点的业务需求直接把这玩意给否决了。诸如动画、snapkit就用不了了,只能用静态内容去处理。

Github地址:

https://github.com/mcry416/SGAsyncView

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/609433.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

SpringBoot 整合 MongoDB 实现数据的增删改查功能

1、介绍说明 在 MongoDB 中有三个比较重要的名词&#xff1a;数据库、集合、文档 数据库&#xff08;Database&#xff09;&#xff1a;和关系型数据库一样&#xff0c;每个数据库中有自己的用户权限&#xff0c;不同的项目组可以使用不同的数据库 集合&#xff08;Collectio…

汽车新能源 - 单体电压值为什么通常是5V以内

常见蓄电池单体电压的值&#xff08;25℃&#xff09;&#xff0c;如下表&#xff1a; 蓄电池类型单体电压&#xff08;V&#xff09;铅酸 蓄电池2.08镍金属氢 蓄电池&#xff08;NIMH&#xff09;1.32锂离子 蓄电池2.5~4.2&#xff08;典型3.6&#xff09; 单体电压为什么不…

安卓学习笔记(一)

从今天开始我们开始学习安卓的知识&#xff1a; 1.0 Android基础入门教程 1.Android背景与当前的状况 Android系统是由Andy Rubin创建的&#xff0c;后来被Google收购了&#xff1b;最早的版本是:Android 1.1版本 而现在最新的版本是今年5.28&#xff0c;Google I/O大会上推…

Logstash部署与使用

ElasticSearch 1、ElasticSearch学习随笔之基础介绍 2、ElasticSearch学习随笔之简单操作 3、ElasticSearch学习随笔之java api 操作 4、ElasticSearch学习随笔之SpringBoot Starter 操作 5、ElasticSearch学习随笔之嵌套操作 6、ElasticSearch学习随笔之分词算法 7、ElasticS…

123网盘在线解析PHP版源码

123网盘解析PHP版本源码是一种非常实用的工具&#xff0c;可以方便地帮助用户在网页上直接解析出其它网站中的资源&#xff0c;并提供下载链接。当用户需要获取某些资源时&#xff0c;往往需要通过各种搜索引擎或者专业的资源网站进行查找&#xff0c;而且很多时候找到了资源链…

Android NDK集成OpenCV使用C++的.h和.so库函数

Android NDK集成OpenCV使用C的.h和.so库函数 opencv可以作为一个单独的Android module库&#xff0c;被工程下的其他模块使用&#xff0c;但是这样就没法在Android NDK项目的c代码直接使用opencv的.h文件和.so文件。要在Android NDK项目C代码文件中使用&#xff0c;则需要以An…

Nautilus Chain上首个DEX PoseiSwap即将开启IDO

据悉&#xff0c;Nautilus Chain 上的首个 DEX PoseiSwap 即将开启 IDO &#xff0c;根据官方的最新公告显示&#xff0c;PoseiSwap 即将于 6 月 13 日至 6 月 14 日期间&#xff0c;在 Bounce 平台开启其治理通证 $POSE 的 IDO&#xff08;Initial DEX Offering&#xff09;&a…

数据分析--Numpy初级(一)

Numpy初级 Ndarray对象dtype对象 Numpy是数据分析的基础库&#xff0c;它支持大量的维度计算与矩阵运算。同时他也是一个运行速度非常快的数学库&#xff0c;主要用于数组计算&#xff0c;具有线性代数、傅里叶变换、随机数生成等功能。 Ndarray对象 Numpy最重要的一个特点就…

【内部类】

目录 1.什么是内部类2.内部类分类2.1静态内部类2.非静态内部类2.3 局部内部类&#xff08;几乎不用&#xff0c;大家了解&#xff09; 3.匿名内部类 1.什么是内部类 在Java中&#xff0c;可以将一个类定义在一个类中或者在一个方法中&#xff0c;前者称为内部类&#xff0c;后…

JAVA打印Hello World的底层实现

任何一个学过JAVA的人应该都对这段代码非常熟悉。空闲时间翻了下代码&#xff0c;看看它的底层是怎么实现的 public class HelloWorld {public static void main(String[] args) {System.out.print("Hello, World!");} }首先点开out&#xff0c;发现它是System类中的…

JAVA微服务_网关

服务网关 什么是服务网关/API网关 API Gateway&#xff08;APIGW / API 网关&#xff09;&#xff0c;顾名思义&#xff0c;是系统对外的唯一入口。API网关封装了系统内部架构&#xff0c;为每个客户端提供定制的API。 近几年来移动应用与企业间互联需求的兴起。从以前单一的…

数据结构学习记录——图-最短路径问题(无权图单源最短路径算法、有权图单源最短路径算法、多源最短路径算法、Dijkstra(迪杰斯特拉)算法、Floyd算法)

目录 问题分类 无权图单源最短路径算法 思路 伪代码 时间复杂度 代码实现&#xff08;C语言&#xff09; 有权图单源最短路径算法 Dijkstra&#xff08;迪杰斯特拉&#xff09;算法 伪代码 时间复杂度 代码实现&#xff08;C语言&#xff09; 多源最短路径算法 …

《Apollo 智能驾驶进阶课程》四、感知

1. 感知概貌 2. 传感器和标定 激光雷达&#xff1a;主动式&#xff0c;发射功率限制 Camera: 被动式&#xff0c;受到光照影响大 Radar : 多普勒效率 相对速度 超声波: 感知距离有限&#xff0c;倒车时使用。 … 最后设备还在研发过程中。 PnP问题&#xff0c;解决标定。 IC…

chatgpt赋能python:Python实现字符串匹配的SEO优化

Python实现字符串匹配的SEO优化 在现代网络中&#xff0c;SEO&#xff08;搜索引擎优化&#xff09;已成为一项必不可少的技能。它涉及到网站的排名、用户的流量和营销策略等方面。关键字匹配是一种常见的SEO技术&#xff0c;它可以帮助你的网站在搜索引擎中排名更高。 本篇文…

Java 实现判定顺序表中是否包含某个元素的方法

一、思路 1.定义一个toFind变量来传入要查找的元素 2.遍历整个顺序表并判定当前下标的元素等不等于toFind 3.如果等于就返回一个true&#xff0c;否则返回false。 二、图解 首先调用以下的方法求出顺序表的长度&#xff0c;再使用 for 循环遍历每一个元素。 // 求顺序表的长…

《嵌入式系统》知识总结9:使用STM32固件库操纵GPIO

STM32固件库&#xff08;函数库&#xff09; “STM32 标准函数库”它是由 ST 公司针对 STM32 提供的函数接口&#xff0c;即 API (Application Program Interface)&#xff0c;开发者可调用这些函数接口来配置 STM32的寄存器&#xff0c;使开发人员得以脱离最底层的寄存器操作&…

《阿里大数据之路》研读笔记(1)

首先先看到OLAP和OLTP的区别&#xff1a; OLTP(Online transaction processing):在线/联机事务处理。典型的OLTP类操作都比较简单&#xff0c;主要是对数据库中的数据进行增删改查&#xff0c;操作主体一般是产品的用户或者是操作人员。 OLAP(Online analytical processing):…

libVLC 抓取视频帧并渲染(QGraphicsView)

作者: 一去、二三里 个人微信号: iwaleon 微信公众号: 高效程序员 在《libVLC 抓取视频帧并渲染(QWidget)》介绍完 QWidget 对视频帧的渲染之后,是时候介绍第二种方式了 - QGraphicsView/QGraphicsScene/QGraphicsItem 图形视图框架。 基本步骤:自定义一个 QGraphicsIte…

RK3588平台开发系列讲解(驱动基础篇)等待队列

平台内核版本安卓版本RK3588Linux 5.10Android 12文章目录 一、等待队列二、等待队列头三、等待队列项四、添加/删除队列五、等待唤醒六、等待事件沉淀、分享、成长,让自己和他人都能有所收获!😄 📢 Linux 内核的等待队列是以双循环链表为基础数据结构,与进程调度机制紧…

KeepChatGPT: chatGPT增强插件,解决报错、保持活跃,让AI更丝滑

KeepChatGPT&#xff1a; chatGPT增强插件&#xff0c;解决报错、保持活跃&#xff0c;让AI更丝滑 这是一个ChatGPT的畅聊与增强插件。开源免费。不仅能解决所有报错不再刷新&#xff0c;还有保持活跃、取消审计、克隆对话、净化首页、展示大屏、展示全屏、言无不尽、拦截跟踪…