在公司项目开发中有碰到这样一个问题,请求图片数据参数以POST方式提交,返回的json中带有图片数据格式如下:
其中data
中的数据是一个类似[-1,-40,-1,-32,0,16,74]
的数组.data
数据即为图片的数据。由于图片很多,原有逻辑是采用Alamofire
一张一张图片进行下载,现在有个想法就是做成多线程下载的方式。于是想到了GCD
和Operation
,下面来看看他们的优缺点:
GCD
GCD
有一个问题无法控制最大并发数,而且对队列的管理也并不完善,比如我们要下载100
个文件,如果同时下载的话开辟100
个线程,那肯定是不行的,先不说移动设备是否支持(最多70
个左右),即使支持了那这个开销太大。虽说GCD
的话可以使用信号量进行线程控制,但是每个线程的暂停启动之类的又是问题,而且毕竟是曲线救国的方法。
OperationQueue
Operation
及OperationQueue
是基于GCD
封装的对象,作为对象可以提供更多操作选择,可以用方法或Block
实现多线程任务,同时也可以利用继承、类别等进行一些其他操作;但同时实现代码相对复杂一些。但是他毕竟不像GCD
那样使用C
语言实现,所以效率会相比GCD
低一些。但是对线程的控制的灵活性要远高于GCD
,对于下载线程来说可以优先选择这个。
自定义Operation实现思路
在Swift
里面我们可以使用BlockOperation
(InvocationOperation
已不存在了)他的闭包则是需要执行的下载任务,然后我们把他添加进OperationQueue
中便开始执行了任务。但是这里,我选择自定义Operation
来实现。我们把每一个下载任务封装成一个Operation
。注意Operation
不能直接使用,我们需要使用他的子类。Operation
中有两个方法,我们来了解下:
open func start()
open func main()
start
和main
。按照官方文档所说,如果是非并发就使用main
,并发就使用start
。那现在并发和非并发已经没有区别了,start
和main
的区别在哪里呢?
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的使用(下载队列,断点续传)