前言:
基于UIKit的性能优化似乎已经到了瓶颈,无论是使用frame代理snpakit,缓存高度,减少布局层次,diff刷新,压缩图片,选择合适队列,选择高性能锁,也不能满足当前庞大而又复杂的项目优化。每次加载刷新的时候过长时间的VC加载,或者collectionView刷新的时候卡顿,真是有点不知所措。那么,有没有比上述内容更高级的优化方法呢?答案是有的,那就是异步绘制。
(有关异步绘制内容,更好的处理是选择YYText或者AsyncKit这些成熟的作品,本文仅限于介绍入门,请不要将示例直接用于生产环境!)
异步绘制:
UIButton
、UILabel
、UITableView
等空间是同步绘制在主线程上的,也就是说,如果这些控件在业务上表现很复杂,那么有可能就会导致界面卡顿。好在系统开了一个口子可以让我们自己去绘制,当然是在子线程上处理然后回到主线程上显示。
大致原理就是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() {