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

前言:

基于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_once仅执行一次的线程安全方法,这里以objc_sync的enter与exit处理构造了一个类似dispatch_once仅执行一次的线程安全方法。

当绘制类发出信号需要绘制时,就通过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)
        }
    }
    
}

最顶部的绘制代理只需要绘制类实现就行,然后绘制类根据contextsize等信息自己去绘制文本,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处理,是因为我没想到如何准确称呼这种做法为好,暂时用这种拗口的名字称呼。

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

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

1.抽象代理:

这里其实是把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

猜你喜欢

转载自blog.csdn.net/kicinio/article/details/131036581