Swift-Operation使用笔记

在公司项目开发中有碰到这样一个问题,请求图片数据参数以POST方式提交,返回的json中带有图片数据格式如下:


POST方式返回图片json.png

其中data中的数据是一个类似[-1,-40,-1,-32,0,16,74]的数组.data数据即为图片的数据。由于图片很多,原有逻辑是采用Alamofire一张一张图片进行下载,现在有个想法就是做成多线程下载的方式。于是想到了GCDOperation,下面来看看他们的优缺点:

GCD

GCD有一个问题无法控制最大并发数,而且对队列的管理也并不完善,比如我们要下载100个文件,如果同时下载的话开辟100个线程,那肯定是不行的,先不说移动设备是否支持(最多70个左右),即使支持了那这个开销太大。虽说GCD的话可以使用信号量进行线程控制,但是每个线程的暂停启动之类的又是问题,而且毕竟是曲线救国的方法。

OperationQueue

OperationOperationQueue是基于GCD封装的对象,作为对象可以提供更多操作选择,可以用方法或Block实现多线程任务,同时也可以利用继承、类别等进行一些其他操作;但同时实现代码相对复杂一些。但是他毕竟不像GCD那样使用C语言实现,所以效率会相比GCD低一些。但是对线程的控制的灵活性要远高于GCD,对于下载线程来说可以优先选择这个。

自定义Operation实现思路

Swift里面我们可以使用BlockOperation(InvocationOperation已不存在了)他的闭包则是需要执行的下载任务,然后我们把他添加进OperationQueue中便开始执行了任务。但是这里,我选择自定义Operation来实现。我们把每一个下载任务封装成一个Operation。注意Operation不能直接使用,我们需要使用他的子类。Operation中有两个方法,我们来了解下:

    open func start()
    open func main()

startmain。按照官方文档所说,如果是非并发就使用main,并发就使用start。那现在并发和非并发已经没有区别了,startmain的区别在哪里呢?
main方法的话,如果main方法执行完毕,那么整个Operation就会从队列中被移除。如果你是一个自定义的operation并且它是某些类的代理,这些类恰好有异步方法,这是就会找不到代理导致程序出错了。
然而start方法就算执行完毕,它的finish属性也不会变,因此你可以控制这个Operation的生命周期了。
然后在任务完成之后手动cancel掉这个Operation即可。

方式一、实现main()

//TKMainOperation.swift
class TKMainOperation: Operation { 
    override func main() {
        print("current thread: \(Thread.current)")
        let queue = DispatchQueue.global()
        queue.async {
            print("sleep current thread: \(Thread.current)")
            sleep(3)
            print("我睡醒了")
        }
        print("这里先执行,因为上面异步开辟线程需要消耗时间")
    }
}
//ViewController.swift
class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        
        testMainOperation()
        // Do any additional setup after loading the view, typically from a nib.
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }

    
    func testMainOperation() {
        let op = TKMainOperation()
        op.completionBlock = {
            print("操作执行完了")
        }
        op.start()
//        let queue = OperationQueue()
//        queue.addOperation(op)
        
    }

}
current thread: {number = 1, name = main}
数据请求 current thread: {number = 3, name = (null)}
这里先执行,因为上面异步开辟线程需要消耗时间
操作执行完了
我睡醒了

采用op.start()来执行的话,main里面是在当前线程执行的,也就是说不会重新开辟新的线程,然后采用加入到OperationQueue的方式来做的话,打印如下:

current thread: {number = 3, name = (null)}
这里先执行,因为上面异步开辟线程需要消耗时间
数据请求 current thread: {number = 4, name = (null)}
操作执行完了
我睡醒了

好了到这里,不知道有没有工友看出来有问题。在main中有异步操作的时候。并不会等待异步操作执行完(打印我睡醒了),才会执行CompletionBlock.而是执行完操作,不去管异步操作是否完成,就执行完回调的block。这显然不是我们想要的结果。我们想要的结果是等我打印睡醒了再去执行操作的回调。那么如何解决这个问题呢?信号量机制。代码如下:

//TKMainOperation.swift
class TKMainOperation: Operation {
    override func main() {
        print("current thread: \(Thread.current)")
        let queue = DispatchQueue.global()
        //创建一个新的信号量,参数value代表信号量资源池的初始数量。
        //    value < 0, 返回NULL
        //    value = 0, 多线程在等待某个特定线程的结束。
        //    value > 0, 资源数量,可以由多个线程使用。
        let semaphore = DispatchSemaphore(value: 0)
        
        queue.async {
            print("sleep current thread: \(Thread.current)")
            sleep(3)
            print("我睡醒了")
            // 释放一个资源。返回值为0表示没有线程等待这个信号量;返回值非0表示唤醒一个等待这个信号量的线程。如果线程有优先级,则按照优先级顺序唤醒线程,否则随机选择线程唤醒。
            semaphore.signal()
            
        }
        semaphore.wait()
        print("这里先执行,因为上面异步开辟线程需要消耗时间")
    }
}

方式二、实现start()

代码如下:

//TKStartOperation.swift
let FINISHED  = "isFinished"
let CANCELLED = "isCancelled"
let EXECUTING = "isExecuting"


protocol TKStartOperationDelegate: NSObjectProtocol {
    
    /// 下载完数据回调
    ///
    /// - Parameters:
    ///   - taskId: 记录唯一值
    ///   - success: 是否成功
    ///   - data: 数据
    func dataDownloadFinished(_ taskId: String,success: Bool, data: Data?)
}

class TKStartOperation: Operation {
    // 标记当前Operation
    var taskId: String = ""
    
    weak var operationDelegate: TKStartOperationDelegate?
    
    /// 操作是否完成
    private var operationFinished: Bool = false {
        willSet {
            willChangeValue(forKey: FINISHED)
        }
        
        didSet {
            didChangeValue(forKey: FINISHED)
        }
    }
    
    /// 操作是否取消
    var operationCancelled: Bool = false {
        willSet {
            willChangeValue(forKey: CANCELLED)
        }
        didSet {
            didChangeValue(forKey: CANCELLED)
        }
    }
    
    /// 操作是否正在执行
    var operationExecuting: Bool = false {
        willSet {
            willChangeValue(forKey: EXECUTING)
        }
        didSet {
            didChangeValue(forKey: EXECUTING)
        }
    }
    
    
    init(_ taskIdentifier:String) {
        taskId = taskIdentifier
    }
    
    
    override func start() {
        print("current thread: \(Thread.current)")
        if isCancelled { // 如果取消了 将状态更改
            operationFinished = true
            operationExecuting = false
            return
        }
        operationExecuting = true
        // 开始请求
        requestData()
    }
    
    
    private func requestData() {
        let queue = DispatchQueue.global()
        queue.async {
            print("数据请求 current thread: \(Thread.current)")
            sleep(3)// 假装网络请求数据
            DispatchQueue.main.async {
                self.operationDelegate?.dataDownloadFinished(self.taskId, success: true, data: nil)
                self.finish() //标记操作完成 状态更改
            }
            
        }
    }
    
    
    override func cancel() {
        // 当前任务未完成才执行取消(完成了就没必要取消了)
        guard !operationFinished else {
            return
        }
    
        // 如果有请求任务 需要在这进行取消
        
        if operationExecuting {
            operationExecuting = false
        }
        
        if !operationFinished {
            operationFinished = true
        }
        operationCancelled = true

        super.cancel()
    }
    
    
    private func finish() {
        operationFinished = true
        operationExecuting = false
    }
    
    // MARK: -  以下四个方法是必须实现的
    override var isExecuting: Bool {
        return operationExecuting
    }
    
    override var isFinished: Bool {
        return operationFinished
    }
    
    override var isCancelled: Bool {
        return operationCancelled
    }
    
    override var isAsynchronous: Bool {
        return true
    }    
}
//ViewController.swift
class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        
//        testMainOperation()
        
        testStartOperation()
        // Do any additional setup after loading the view, typically from a nib.
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }

    
    func testMainOperation() {
        let op = TKMainOperation()
        op.completionBlock = {
            print("操作执行完了")
        }
//        op.start()
        let queue = OperationQueue()
        queue.addOperation(op)
        
    }
    
    
    func testStartOperation() {
        let op = TKStartOperation("1111")
        op.operationDelegate = self
        op.completionBlock = {
            print("操作执行完了")
        }
        //        op.start()
        let queue = OperationQueue()
        queue.addOperation(op)
    }

}


// MARK: -  下载数据回调
extension ViewController: TKStartOperationDelegate {
    func dataDownloadFinished(_ taskId: String, success: Bool, data: Data?) {
        print("数据请求回来了")
    }
}
current thread: {number = 3, name = (null)}
数据请求 current thread: {number = 4, name = (null)}
数据请求回来了
操作执行完了

到这一步,基本上基础部分已经讲完了。
接下来需要将请求塞进去。这块有用到Alamofire进行请求,应该是有缓存的原因,导致下载下来的图片数据(一般有4-5M)会导致内存暴增(测试过有两百张图,目测会开辟500-600M的内存,而一张一张采用Alamofire下载的话只有20-50M左右)。后来采用的是URLSession来进行处理(并发数3,内存消耗在100M左右),此处还有待研究。不知道有没有工友知道更好的解决方案。欢迎赐教,不甚感谢!

参考:
NSOperation中start与main的区别
AlamoFire的使用(下载队列,断点续传)

你可能感兴趣的:(Swift-Operation使用笔记)