iOS(Swift) TaskProtocol异步任务队列

需求前提

昨天做需求时,一个简单的需求,一个动画展示,在动画展示几秒后,移除动画,然后我在 动画完成时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 优先级应该都是要比这个任务队列要高,省性能一定也要考虑

你可能感兴趣的:(iOS(Swift) TaskProtocol异步任务队列)