需求前提
昨天做需求时,一个简单的需求,一个动画展示,在动画展示几秒后,移除动画,然后我在 动画完成时completion写了个 prepareForReuse 方法去重置数据.但是冲突点来了,这个动画是 View 动画,不能提前取消,所以导致 Completion会在下次的 show 动画中被执行,如果还没来的及prepareForReuse就调用 show,这样数据展示就会出问题.
设计
利用 OperationQueue 维护任务队列,设置并发数为1,所有任务都由信号量控制并发
实现
设计协议
protocol TaskProtocol: NSObject {
var taskQueue: OperationQueue { get }
func addTask(_ task:@escaping (DispatchSemaphore?, CancellableBlockOperation) -> ())
func cancelAllTask()
}
队列属性实现
extension TaskProtocol {
private var taskQueue: OperationQueue {
get {
objc_sync_enter(self); defer { objc_sync_exit(self)}
if let obj = objc_getAssociatedObject(self, &taskQueueKey) as? OperationQueue {
return obj
} else {
let queue: OperationQueue = {
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 1
queue.underlyingQueue = .global(qos: .utility)
return queue
}()
objc_setAssociatedObject(self, &taskQueueKey, queue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
return queue
}
}
set {
objc_setAssociatedObject(self, &taskQueueKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
}
这个操作队列的实现队列qos 采用utility,这个 UI 任务有可能持续一段时间.
QOS_CLASS_USER_INTERACTIVE
最高优先级,即使在争用情况下也可以运行几乎所有可用的系统CPU和I / O带宽。所以使用时应限于与用户的关键交互,例如处理主事件循环上的事件,视图绘制,动画等。
QOS_CLASS_USER_INITIATED
低于用户的关键交互,但相对高于系统上的其他工作,使用于持续时间短的操作
QOS_CLASS_UTILITY
用于一些可能需要花点时间的任务,这些任务不需要马上返回结果,比如下载任务等
QOS_CLASS_BACKGROUND
用于完全不紧急的任务,磁盘 I/O后台备份等用这个,使用此QOS类表明工作应以最节能和最有效的方式运行
QOS_CLASS_DEFAULT
优先级介于user-initiated 和 utility,由pthread_create()创建的线程没有指定QOS的属性将默认为QOS_CLASS_DEFAULT, 此值不应用作工作分类,只应在传播或恢复系统提供的QOS类值时进行设置
核心实现
extension TaskProtocol {
/// 添加异步 UI任务
/// - Parameters:
/// - task: UI 结束时调用 semaphore.signal()
func addTask(_ task:@escaping (DispatchSemaphore?, CancellableBlockOperation) -> ()) {
self.addTask(task, finishBlock: nil, delay: 0, timeout: 0, taskBehavier: .queue(.normal))
}
/// 添加异步 UI任务
/// - Parameters:
/// - task: UI 结束时调用 semaphore.signal()
/// - finishBlock: 完成回调大部分情况不需要
/// - delay: 任务延时
/// - timeout: 超时
/// - taskBehavier: 行为,绝大部分不需要传
func addTask(_ task:@escaping (DispatchSemaphore?, CancellableBlockOperation) -> (), finishBlock: ((Bool, CancellableBlockOperation) -> ())? = nil, delay: TimeInterval = 0, timeout: TimeInterval = 0, taskBehavier: AsyncTaskBehavior = .queue(.normal)) {
let operation = CancellableBlockOperation(block: { (sem, op) in
if delay > 0 {
_ = DispatchSemaphore(value: 0).wait(timeout: .now() + delay)
}
DispatchQueue.main.async {
task(sem, op)
}
}, completionBlock: { (isSuccess, op) in
DispatchQueue.main.async {
finishBlock?(isSuccess, op)
}
}, timeout: timeout)
switch taskBehavier {
case let .queue(priority):
operation.queuePriority = priority
taskQueue.addOperation(operation)
case .replaceAll:
// let deferredOperations = deferredOperations(queue: queue, filterRunning: false)
taskQueue.cancelAllOperations()
operation.queuePriority = .veryHigh
operation.immediatelyStarted = true
taskQueue.addOperation(operation)
// 暂时不处理 ops
// for op in deferredOperations {
// op.queuePriority = .high
// }
case .replaceCurrent:
let deferredOperations = deferredOperations(queue: taskQueue, filterRunning: true)
taskQueue.cancelRunningOperations()
operation.queuePriority = .veryHigh
operation.immediatelyStarted = true
taskQueue.addOperation(operation)
for op in deferredOperations {
taskQueue.addOperation(op)
}
}
}
/// 取消所有未执行任务
func cancelAllTask() {
taskQueue.cancelAllOperations()
}
private func deferredOperations(queue: OperationQueue, filterRunning: Bool) -> [CancellableBlockOperation] {
var deferredOps = [CancellableBlockOperation]()
for op in queue.operations {
guard let op = op as? CancellableBlockOperation, !op.isFinished, !op.isCancelled,
filterRunning ? (op.isExecuting || op.immediatelyStarted) : true
else {
continue
}
deferredOps.append(op)
}
return deferredOps
}
}
核心逻辑即为用CancellableBlockOperation包装一个operation, 内部用DispatchSemaphore
控制该任务的结束时机,利用 operationQueue 并发1的机制,达到异步UI 任务顺序执行的逻辑.
CancellableBlockOperation核心逻辑
override func main() {
autoreleasepool {
guard self.block != nil else { return }
if self.isCancelled {
self.block = nil
self.finishHandler = nil
return
}
var backgroundIdentifier = UIBackgroundTaskIdentifier.invalid
if self.keepRunningInBackground {
backgroundIdentifier = UIApplication.shared.beginBackgroundTask {
// UIApplication.shared.endBackgroundTask(backgroundIdentifier)
}
}
self.semaphore = DispatchSemaphore(value: 0)
self.block?(self.semaphore, self)
let _ = self.semaphore?.wait(timeout: self.timeout > 0 ? .now() + self.timeout : .distantFuture)
self.block = nil
self.finishHandler?(!self._cancelled, self)
self.finishHandler = nil
if backgroundIdentifier != .invalid {
UIApplication.shared.endBackgroundTask(backgroundIdentifier)
}
}
}
因为这个 信号wait ,所以我们需要之前的 undeylingQueue 需要利用.global()
协议设计完成,可以看看实现
从现在开始,我们就可以摆脱动画的嵌套地狱了.只要在你想要实现的对象上实现TaskProtocol
,即可使用
class DiscoverHeaderCell: CollectionViewCell, TaskProtocol {
let box = UIView(.red)
override func commonInit() {
super.commonInit()
flexRootContainer.backgroundColor = .gray
box.add(to: contentView)
}
override func layoutSubviews() {
superview?.layoutSubviews()
box.frame = MakeRect(10, 10, 100, 100)
// 变蓝, 变圆
addTask {[weak self] sem, op in
guard let self = self else { return }
UIView.animate(withDuration: 2) {
self.box.backgroundColor = .blue
self.box.cornerRadius = self.box.height * 0.5
} completion: { _ in
sem?.signal()
}
}
// 变宽
addTask {[weak self] sem, op in
guard let self = self else { return }
UIView.animate(withDuration: 1.5) {
self.box.width = 200
} completion: { _ in
sem?.signal()
}
}
// 复原
addTask {[weak self] sem, op in
guard let self = self else { return }
let right = self.box.frame.maxX
UIView.animate(withDuration: 1) {
self.box.width = 100
self.box.right = right
} completion: { _ in
sem?.signal()
}
}
// 变色,变方
addTask {[weak self] sem, op in
guard let self = self else { return }
UIView.animate(withDuration: 1) {
self.box.backgroundColor = .gray
self.box.cornerRadius = 0
} completion: { _ in
sem?.signal()
}
}
}
}
我设定4个动画逻辑,展示
如果用UIView 实现,逻辑嵌套会十分的麻烦,而且无法取消,所以,搞起来吧,兄dei~
异步任务基本也是每个 APP 必备的,以前都是手写 queue,昨天思考了下用协议实现,省去中间商赚差价.
不过,如非必要,还是不要大规模实现,平时思考下,真的需要异步时再考虑,CAAnimation, GroupAnimation 优先级应该都是要比这个任务队列要高,省性能一定也要考虑