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: <NSThread: 0x60400006e100>{number = 1, name = main}
数据请求 current thread: <NSThread: 0x604000464c40>{number = 3, name = (null)}
这里先执行,因为上面异步开辟线程需要消耗时间
操作执行完了
我睡醒了

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

current thread: <NSThread: 0x60400046da00>{number = 3, name = (null)}
这里先执行,因为上面异步开辟线程需要消耗时间
数据请求 current thread: <NSThread: 0x600000470640>{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: <NSThread: 0x60400027b840>{number = 3, name = (null)}
数据请求 current thread: <NSThread: 0x60400027bd00>{number = 4, name = (null)}
数据请求回来了
操作执行完了

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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,036评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,046评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,411评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,622评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,661评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,521评论 1 304
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,288评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,200评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,644评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,837评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,953评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,673评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,281评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,889评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,011评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,119评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,901评论 2 355

推荐阅读更多精彩内容