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

news/2024/11/24 7:09:39/

前言:

基于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 Foundationfinal 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 + 1queues.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 + 1queues.append(asyncQueue)return asyncQueue}asyncQueue.taskCount = asyncQueue.taskCount + 1queues.append(asyncQueue)return asyncQueue}/**Indicate a queue to stop.*/public func stopTaskQueue(_ queue: SGAsyncQueue){queue.taskCount = queue.taskCount - 1if 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 = 0public 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 = targetself.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.rawValuelet observer   = CFRunLoopObserverCreateWithHandler(nil, activities, true, 0xFFFFFF) { (ob, ac) inguard 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] = tokenblock()}}

这里用到了一个小技巧,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 = falseoverride func setNeedsDisplay() {self.isCancel = truesuper.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.sizelet scale: CGFloat = UIScreen.main.nativeScalelet opaque: Bool = self.isOpaqueUIGraphicsBeginImageContextWithOptions(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 UIKitclass 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.sizecontext.textMatrix = CGAffineTransformIdentitycontext.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.lineSpacingstyle.alignment = self.textAlignmentstyle.lineBreakMode = self.lineBreakModestyle.paragraphSpacing = 5let 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 UIKitclass 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.sizecontext.textMatrix = CGAffineTransformIdentitycontext.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 UIKitprotocol 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 UIKitclass 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 inlet 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.roundbezierPath.lineJoinStyle = CGLineJoin.roundbezierPath.fill()let style: NSMutableParagraphStyle = NSMutableParagraphStyle()style.lineSpacing = 3style.alignment = .centerstyle.lineBreakMode = .byTruncatingTailstyle.paragraphSpacing = 5let 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 CFDictionarylet 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 + leadingcontext.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 inlet 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 = 3style.alignment = .leftstyle.lineBreakMode = .byTruncatingTailstyle.paragraphSpacing = 5let 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.sizecontext.textMatrix = CGAffineTransformIdentitycontext.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.maxXlet isInY: Bool = touchPoint.y >= node.frame.minY && touchPoint.y <= node.frame.maxYif 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.maxXlet isInY: Bool = touchPoint.y >= node.frame.minY && touchPoint.y <= node.frame.maxYif 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 UIKitclass NodeImageView: NSObject, NodeLayerDelegate {var contents: (Any & NSObject)?var backgroundColor: UIColor = .whitevar frame: CGRect = .zerovar hidden: Bool = falsevar alpha: CGFloat = 1.0var superView: NodeLayerDelegate?var paintSignal: Bool = falseprivate 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 = .orangebutton.textColor = .whitebutton.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 = .noneself.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.ppmy.cn/news/211421.html

相关文章

家用计算机的内存容量大约是多少升,家用旧电脑最佳升级方案:8G内存、混合硬盘足够了!...

在讲加内存条、换硬盘、加固态硬盘之前&#xff0c;内存、硬盘、固态硬盘在广义上属于存储器的一种&#xff0c;它们的主要作用就是存储数据。不过他们之间爷爷很多不同之处&#xff0c;下面先来了解一下他们的基本工作原理。 台式机内存条 在问题中提到的内存&#xff0c;它是…

8g内存一般占用多少_8g内存开机占用一半|Windows操作系统内存使用率多少正常?...

Windows操作系统内存使用率多少正常?内存使用率根据不同用户的使用习惯和软件安装,笔者总结并模拟了一下资源占用情况,可以根据数据预测XP、Win7、Win8、Win8.1、Win10的开机资源占用率上下浮动,以供参考。 Windows操作系统内存使用率多少正常? 如果使用是2G内存的情况下,…

8g内存一般占用多少_你到底需要多大内存?4G、8G还是16G

1你到底需要多大内存&#xff1f; 很多老DIY玩家或许还依稀记得&#xff0c;在DDR2时代(大概2007年左右)&#xff0c;2GB和4GB内存的游戏性能相差并不大&#xff0c;所以在当时很长一段时间内&#xff0c;看上去很美的4G容量往往会被扣上华而不实的帽子。如今&#xff0c;内存已…

果推断17--基于反事实因果推断的度小满额度模型学习笔记

目录 一、原文地址 二、一些问题 2.1如何从RCT随机样本过渡到观测样本因果建模&#xff1f; 2.2反事实学习的核心思想 2.3度小满的连续反事实额度模型 Mono-CFR 2.4Mono-CFR代码实现&#xff08;待补充&#xff09; 2.5CFR学习 2.5.1CFR 2.5.2DR-CFR 参考 一、原文地…

极速版手机蓝牙APP开发

极速版手机蓝牙APP开发 零、效果展示一、环境介绍二、开发过程控件布局代码逻辑蓝牙部分摇杆部分其他部分 三、整体优化四、结束语 零、效果展示 “这是一个充满科技风的手机蓝牙APP” 一、环境介绍 App Inventor是一款谷歌公司开发的手机编程软件&#xff0c;主要支持各种…

python环境安装

测试电脑环境有无安装python&#xff1a; winR&#xff0c;输入cmd&#xff0c;打开窗口&#xff0c;输入pyhton&#xff0c;查看是否有版本号&#xff0c;没有则是没有安装python环境 找到python-3.7.0-amd64的安装包&#xff0c;直接双击启动。上面是快速安装&#xff0c;我…

python文本注释数学表达式设置|python绘图中的数学表达式设置

本篇文章将介绍如何在Matplotlib中设置文本、注释和数学表达式&#xff0c;以便更好地呈现数据&#xff0c;提高可视化效果。 文章目录 一、Matplotlib中的文本设置1.1 纯文本设置1.2 含箭头的文本设置 二、Matplotlib中的数学表达式设置三、Matplotlib中的字体设置 一、Matplo…

联想C320一体机升级CPU 失败,求高手指点!

联想C320一体机升级CPU 失败&#xff0c;求高手指点&#xff01; 联想C320一体机CPU:G630&#xff0c;内存4G&#xff0c;独立显卡&#xff0c;原机械硬盘已升级为固态&#xff0c;开机速度还可以。近期闲来无事想升级下CPU&#xff0c;百度了下&#xff0c;先是闲鱼购买了I5 3…