在公司項目開發(fā)中有碰到這樣一個問題,請求圖片數(shù)據(jù)參數(shù)以POST方式提交缰猴,返回的json中帶有圖片數(shù)據(jù)格式如下:
其中data
中的數(shù)據(jù)是一個類似[-1,-40,-1,-32,0,16,74]
的數(shù)組.data
數(shù)據(jù)即為圖片的數(shù)據(jù)。由于圖片很多,原有邏輯是采用Alamofire
一張一張圖片進行下載,現(xiàn)在有個想法就是做成多線程下載的方式嘀韧。于是想到了GCD
和Operation
,下面來看看他們的優(yōu)缺點:
GCD
GCD
有一個問題無法控制最大并發(fā)數(shù)缠捌,而且對隊列的管理也并不完善,比如我們要下載100
個文件译蒂,如果同時下載的話開辟100
個線程曼月,那肯定是不行的,先不說移動設(shè)備是否支持(最多70
個左右)柔昼,即使支持了那這個開銷太大哑芹。雖說GCD
的話可以使用信號量進行線程控制,但是每個線程的暫停啟動之類的又是問題捕透,而且畢竟是曲線救國的方法聪姿。
OperationQueue
Operation
及OperationQueue
是基于GCD
封裝的對象,作為對象可以提供更多操作選擇乙嘀,可以用方法或Block
實現(xiàn)多線程任務(wù)末购,同時也可以利用繼承、類別等進行一些其他操作虎谢;但同時實現(xiàn)代碼相對復雜一些盟榴。但是他畢竟不像GCD
那樣使用C
語言實現(xiàn),所以效率會相比GCD
低一些婴噩。但是對線程的控制的靈活性要遠高于GCD
擎场,對于下載線程來說可以優(yōu)先選擇這個。
自定義Operation實現(xiàn)思路
在Swift
里面我們可以使用BlockOperation
(InvocationOperation
已不存在了)他的閉包則是需要執(zhí)行的下載任務(wù)几莽,然后我們把他添加進OperationQueue
中便開始執(zhí)行了任務(wù)迅办。但是這里,我選擇自定義Operation
來實現(xiàn)章蚣。我們把每一個下載任務(wù)封裝成一個Operation
站欺。注意Operation
不能直接使用,我們需要使用他的子類。Operation
中有兩個方法镊绪,我們來了解下:
open func start()
open func main()
start
和main
匀伏。按照官方文檔所說,如果是非并發(fā)就使用main
蝴韭,并發(fā)就使用start
够颠。那現(xiàn)在并發(fā)和非并發(fā)已經(jīng)沒有區(qū)別了,start
和main
的區(qū)別在哪里呢榄鉴?
main
方法的話履磨,如果main
方法執(zhí)行完畢,那么整個Operation
就會從隊列中被移除庆尘。如果你是一個自定義的operation
并且它是某些類的代理剃诅,這些類恰好有異步方法,這是就會找不到代理導致程序出錯了驶忌。
然而start
方法就算執(zhí)行完畢矛辕,它的finish
屬性也不會變,因此你可以控制這個Operation
的生命周期了付魔。
然后在任務(wù)完成之后手動cancel
掉這個Operation
即可聊品。
方式一、實現(xiàn)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("這里先執(zhí)行几苍,因為上面異步開辟線程需要消耗時間")
}
}
//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("操作執(zhí)行完了")
}
op.start()
// let queue = OperationQueue()
// queue.addOperation(op)
}
}
current thread: <NSThread: 0x60400006e100>{number = 1, name = main}
數(shù)據(jù)請求 current thread: <NSThread: 0x604000464c40>{number = 3, name = (null)}
這里先執(zhí)行翻屈,因為上面異步開辟線程需要消耗時間
操作執(zhí)行完了
我睡醒了
采用op.start()
來執(zhí)行的話,main
里面是在當前線程執(zhí)行的妻坝,也就是說不會重新開辟新的線程伸眶,然后采用加入到OperationQueue
的方式來做的話,打印如下:
current thread: <NSThread: 0x60400046da00>{number = 3, name = (null)}
這里先執(zhí)行刽宪,因為上面異步開辟線程需要消耗時間
數(shù)據(jù)請求 current thread: <NSThread: 0x600000470640>{number = 4, name = (null)}
操作執(zhí)行完了
我睡醒了
好了到這里厘贼,不知道有沒有工友看出來有問題。在main
中有異步操作的時候纠屋。并不會等待異步操作執(zhí)行完(打印我睡醒了)涂臣,才會執(zhí)行CompletionBlock
.而是執(zhí)行完操作,不去管異步操作是否完成售担,就執(zhí)行完回調(diào)的block
赁遗。這顯然不是我們想要的結(jié)果。我們想要的結(jié)果是等我打印睡醒了再去執(zhí)行操作的回調(diào)族铆。那么如何解決這個問題呢岩四?信號量機制
。代碼如下:
//TKMainOperation.swift
class TKMainOperation: Operation {
override func main() {
print("current thread: \(Thread.current)")
let queue = DispatchQueue.global()
//創(chuàng)建一個新的信號量哥攘,參數(shù)value代表信號量資源池的初始數(shù)量剖煌。
// value < 0材鹦, 返回NULL
// value = 0, 多線程在等待某個特定線程的結(jié)束。
// value > 0, 資源數(shù)量耕姊,可以由多個線程使用桶唐。
let semaphore = DispatchSemaphore(value: 0)
queue.async {
print("sleep current thread: \(Thread.current)")
sleep(3)
print("我睡醒了")
// 釋放一個資源。返回值為0表示沒有線程等待這個信號量茉兰;返回值非0表示喚醒一個等待這個信號量的線程尤泽。如果線程有優(yōu)先級,則按照優(yōu)先級順序喚醒線程规脸,否則隨機選擇線程喚醒坯约。
semaphore.signal()
}
semaphore.wait()
print("這里先執(zhí)行,因為上面異步開辟線程需要消耗時間")
}
}
方式二莫鸭、實現(xiàn)start()
代碼如下:
//TKStartOperation.swift
let FINISHED = "isFinished"
let CANCELLED = "isCancelled"
let EXECUTING = "isExecuting"
protocol TKStartOperationDelegate: NSObjectProtocol {
/// 下載完數(shù)據(jù)回調(diào)
///
/// - Parameters:
/// - taskId: 記錄唯一值
/// - success: 是否成功
/// - data: 數(shù)據(jù)
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)
}
}
/// 操作是否正在執(zhí)行
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 { // 如果取消了 將狀態(tài)更改
operationFinished = true
operationExecuting = false
return
}
operationExecuting = true
// 開始請求
requestData()
}
private func requestData() {
let queue = DispatchQueue.global()
queue.async {
print("數(shù)據(jù)請求 current thread: \(Thread.current)")
sleep(3)// 假裝網(wǎng)絡(luò)請求數(shù)據(jù)
DispatchQueue.main.async {
self.operationDelegate?.dataDownloadFinished(self.taskId, success: true, data: nil)
self.finish() //標記操作完成 狀態(tài)更改
}
}
}
override func cancel() {
// 當前任務(wù)未完成才執(zhí)行取消(完成了就沒必要取消了)
guard !operationFinished else {
return
}
// 如果有請求任務(wù) 需要在這進行取消
if operationExecuting {
operationExecuting = false
}
if !operationFinished {
operationFinished = true
}
operationCancelled = true
super.cancel()
}
private func finish() {
operationFinished = true
operationExecuting = false
}
// MARK: - 以下四個方法是必須實現(xiàn)的
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("操作執(zhí)行完了")
}
// op.start()
let queue = OperationQueue()
queue.addOperation(op)
}
func testStartOperation() {
let op = TKStartOperation("1111")
op.operationDelegate = self
op.completionBlock = {
print("操作執(zhí)行完了")
}
// op.start()
let queue = OperationQueue()
queue.addOperation(op)
}
}
// MARK: - 下載數(shù)據(jù)回調(diào)
extension ViewController: TKStartOperationDelegate {
func dataDownloadFinished(_ taskId: String, success: Bool, data: Data?) {
print("數(shù)據(jù)請求回來了")
}
}
current thread: <NSThread: 0x60400027b840>{number = 3, name = (null)}
數(shù)據(jù)請求 current thread: <NSThread: 0x60400027bd00>{number = 4, name = (null)}
數(shù)據(jù)請求回來了
操作執(zhí)行完了
到這一步闹丐,基本上基礎(chǔ)部分已經(jīng)講完了。
接下來需要將請求塞進去被因。這塊有用到Alamofire進行請求卿拴,應(yīng)該是有緩存的原因,導致下載下來的圖片數(shù)據(jù)(一般有4-5M)會導致內(nèi)存暴增(測試過有兩百張圖氏身,目測會開辟500-600M的內(nèi)存巍棱,而一張一張采用Alamofire下載的話只有20-50M左右)。后來采用的是URLSession來進行處理(并發(fā)數(shù)3,內(nèi)存消耗在100M左右)蛋欣,此處還有待研究。不知道有沒有工友知道更好的解決方案如贷。歡迎賜教陷虎,不甚感謝!
參考:
NSOperation中start與main的區(qū)別
AlamoFire的使用(下載隊列杠袱,斷點續(xù)傳)