【我們都愛Paul Hegarty】斯坦福IOS8公開課個人筆記31 Multithreading多線程


在IOS中存在着許多隊列,和我們數據結構中的隊列一樣,這裏的隊列概念也是先進先出的。而每一個方法(包括閉包)都被組織在這些不同的隊列中,而每一個隊列都有自己的線程去運行這些隊列,這就造就了多線程環境。

其中有一個非常重要的隊列叫做主隊列,主隊列是一個串行隊列,所以主隊列只會一個一個地執行主隊列中的函數。所有的UI活動都必須發生在主隊列中,所以當你想要一個函數或者是閉包的時候就會執行某些代碼,這就會做任何關於UI的事,必須把它放到主隊列中,這是保護UI的好辦法,主隊列絕對不想做任何可能被阻塞的事情,比如讀取一個包含URL的NSData,上一話中的Demo當我們點擊按鈕獲取大圖的時候會卡頓,所以我們要把它從主隊列中拿出來。

通常你使用MVC中的東西都在主隊列中,比如頁面生命週期中的ViewDidLoad、viewWillAppear,我們不需要特別的留心。

IOS會在你需要的時候爲你創建其他隊列。

那麼如何在其他隊列中執行函數或者閉包呢?


首先初始化一個隊列,然後調用了函數dispatch_async,有兩個參數,第一個是隊列,第二個是一個閉包,這裏寫成了尾隨閉包的形式。那麼第一句中如何獲得主隊列呢?使用函數dispatch_get_main_queue(),就能返回主隊列了。此外還有一個面向對象的方法可以做到:NSOperationQueue.mainQueue()。上面有一個例子,在其他隊列中執行一些可能阻塞UI的操作,然後打開主隊列,執行需要呈現給UI的東西。

下面介紹一些主隊列之外的其他隊列,這些隊列通常使用不同的處理級別來表示,處理級別的高低也代表了隊列不同的處理速度。


要創建一個非主隊列,首先要創建一個服務級別,創建級別的方法有點奇怪,使用了一個Int的構造器,構造其中選擇級別的value屬性,這種做法是由歷史原因造成的。在創建好級別之後,使用dispatch_get_global_queue函數,傳入級別和一個0,這個0以後會用到。有了新的隊列,你就可以使用dispatch_asnyc函數並且在非主隊列中執行代碼了。

如果有多核處理器,隊列真的可以並行運行,但是大部分情況下他們是分時的。你可以創建自己的串行隊列,使用dispatch_queue_create來建立一個串行的隊列。

在iOS中有些API是多線程的,比如下面這個例子:


比如上例中的NSURLSession這個類,注意黃色字體的部分,它讓你從一個URL中下載一個文件,而且是異步的去做。在方法後面增加了一個閉包,這個閉包的作用是當你下載完畢時你需要在UI中打開這個文件,閉包爲你指定了一個本地URL,一個HTTP應答了一個錯誤信息,利用這些信息你會做一些更新UI的事情。那麼這樣做行得通麼?答案是“NO”。

因爲這個下載的文件不在主隊列中,解決辦法是在閉包中打開主隊列:


現在來進行實戰環節,回到我們之前的Demo中,打開圖片總是會造成阻塞,這並不好,嘗試使用多線程的知識來解決這個問題。

我們需要做的是把獲取圖片的環節放到其他線程中,方法fetchImage修改如下:

 func fetchImage(){
        if let url = imageURL {
        let qos = Int(QOS_CLASS_USER_INITIATED.value)//指定服務“可能花費時間,但是用戶需要得到,儘快完成”
        dispatch_async(dispatch_get_global_queue(qos, 0)){ () ->Void in//把要執行的代碼放到閉包中
        let imageData = NSData(contentsOfURL: url)
        if imageData != nil{
        self.image = UIImage(data: imageData!)
        } else {
        self.image = nil
        }
            }
        }
    }
注意現在還不夠,設置image的動作會修改UI,所以這個動作應該在主隊列中,我們把設置image的動作放到主隊列中:

 func fetchImage(){
        if let url = imageURL {
        let qos = Int(QOS_CLASS_USER_INITIATED.value)//指定服務“可能花費時間,但是用戶需要得到,儘快完成”
        dispatch_async(dispatch_get_global_queue(qos, 0)){ () ->Void in//把要執行的代碼放到閉包中
        let imageData = NSData(contentsOfURL: url)//加載圖片應該放到其他線程中
            dispatch_async(dispatch_get_main_queue()){//把加載到的圖片顯示在頁面中的時候使用主線程
        if imageData != nil{
        self.image = UIImage(data: imageData!)
        } else {
        self.image = nil
        }
            }
            }
        }
    }
現在加載圖片的時候系統不會卡頓,你會跳轉到下一個頁面中,在當前頁面中等待即可看到加載完的圖片:

現在的問題是每一次點擊一個按鈕都會生成一個全新的MVC,那麼如何避免重複加載呢,我們需要判斷一下點擊按鈕時獲取的URL是否是當前頁面上圖片的URL,方法修改如下:

  func fetchImage(){
        if let url = imageURL {
        let qos = Int(QOS_CLASS_USER_INITIATED.value)//指定服務“可能花費時間,但是用戶需要得到,儘快完成”
        dispatch_async(dispatch_get_global_queue(qos, 0)){ () ->Void in//把要執行的代碼放到閉包中
        let imageData = NSData(contentsOfURL: url)//加載圖片應該放到其他線程中
            dispatch_async(dispatch_get_main_queue()){//把加載到的圖片顯示在頁面中的時候使用主線程
                if url == self.imageURL{//新增判斷語句
        if imageData != nil{
        self.image = UIImage(data: imageData!)
        } else {
        self.image = nil
        }
            }
            }
            }
        }
    }

最後需要做的是在其他隊列加載圖片的時候,主頁面上運行一個齒輪來表示這個加載過程。

現在去對象庫中拖出一個齒輪控件到場景中,打開文檔大綱你會發現這個新增的齒輪控件被加到了scrollview中,這是因爲我們之前用這個scrollview幾乎鋪滿了我們的view:

這裏文檔大綱的優勢就體現出來了,你可以在文檔大綱中拖動控件來組合它們的次序,下面是正確地順序:

另外在文檔大綱中可以增加約束,和在IB中一樣,按住control拖動即可,我們讓齒輪控件居中


然後設置齒輪控件的屬性,勾選當它停止時消失的選項:


和控制器建立連接:

@IBOutlet weak var spinner: UIActivityIndicatorView!
當我建立一個HTTP請求的時候讓這個齒輪轉動,當我結束這個請求的時候,讓齒輪停止轉動。

所以很明顯我們需要在下面的位置增加齒輪的轉動:

 func fetchImage(){
        if let url = imageURL {
            spinner?.startAnimating()//新增齒輪轉動
        let qos = Int(QOS_CLASS_USER_INITIATED.value)//指定服務“可能花費時間,但是用戶需要得到,儘快完成”
        dispatch_async(dispatch_get_global_queue(qos, 0)){ () ->Void in//把要執行的代碼放到閉包中
        let imageData = NSData(contentsOfURL: url)//加載圖片應該放到其他線程中
            dispatch_async(dispatch_get_main_queue()){//把加載到的圖片顯示在頁面中的時候使用主線程
                if url == self.imageURL{//新增判斷語句
        if imageData != nil{
        self.image = UIImage(data: imageData!)
        } else {
        self.image = nil
        }
            }
            }
            }
        }
    }

注意在調用spinner的時候把它當做可選型,因爲頁面可能先於spinner控件生成,在調用控件的時候這是個常用的辦法。但是這個方法中加載完畢有不同的處理情況,共同點是都要處理image,所以我們選擇在計算屬性image的set方法中停止齒輪:

private var image:UIImage? {
        get {return imageView.image}
        set {
        imageView.image = newValue
        imageView.sizeToFit()
        scrollview?.contentSize = imageView.frame.size
        spinner?.stopAnimating()//加載完成了停止齒輪
        }
    }

現在來運行時候,當圖片沒有加載完成的時候,之前我們看到的是一個白色的頁面,現在頁面中間有齒輪在轉動了:


加載完畢後齒輪消失:



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章