亚洲香蕉成人av网站在线观看_欧美精品成人91久久久久久久_久久久久久久久久久亚洲_热久久视久久精品18亚洲精品_国产精自产拍久久久久久_亚洲色图国产精品_91精品国产网站_中文字幕欧美日韩精品_国产精品久久久久久亚洲调教_国产精品久久一区_性夜试看影院91社区_97在线观看视频国产_68精品久久久久久欧美_欧美精品在线观看_国产精品一区二区久久精品_欧美老女人bb

首頁 > 學院 > 開發設計 > 正文

在Swift中應用GrandCentralDispatch(下)

2019-11-14 19:30:57
字體:
來源:轉載
供稿:網友

img-3-e1412704300353.jpg

本文由loveltyoic(博客翻譯自raywenderlich,原文:Grand Central Dispatch Tutorial for Swift: Part 1/2

歡迎來到本GCD教程的第二同時也是最終部分!

在第一部分中,你學到了并發,線程以及GCD的工作原理。通過使用dispatch_barrrier和dispatch_sync,你做到了讓PhotoManager單例在讀寫照片時是線程安全的。除此之外,你用到dispatch_after來提示用戶,優化了用戶體驗。還有,使用dispatch_async異步執行CPU密集型任務,從而為視圖控制器初始化過程減負。

如果你跟著教程做,現在可以從第一部分的示例工程繼續。如果你沒有完成第一部分或不想再用你的工程,可以下載第一部分的完成文件

是時候進一步探索GCD了!

糾正過早出現的彈窗

你可能注意到,當你通過 Le Internet 選項添加照片時,會有提示框在圖片下載完成之前就彈出,如下圖:

27.jpg

錯誤在于 PhotoManager 里的 downloadPhotosWithCompletion:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func downloadPhotosWithCompletion(completion: BatchPhotoDownloadingCompletionClosure?) {
  var storedError: NSError!
  for address in [OverlyAttachedGirlfriendURLString,
                  SuccessKidURLString,
                  LotsOfFacesURLString] {
    let url = NSURL(string: address)
    let photo = DownloadPhoto(url: url!) {
      image, error in
      if error != nil {
        storedError = error
      }
    }
    PhotoManager.sharedManager.addPhoto(photo)
  }
  
  if let completion = completion {
    completion(error: storedError)
  }
}

這里在方法的最后調用completion閉包——你會想當然的認為所有圖片都下載完了。但不幸的是,在此時無法保證。

DownloadPhoto類的實例方法從一個URL下載圖片并且不等下載完成就立即退出。換言之,downloadPhotosWithCompletion在最后調用completion閉包,就好像其中的所有方法都在順序執行,并且在每個方法完成后才執行下一個。

然而,DownloadPhoto(url:)是異步并且立即返回的——所以目前的方式不能正常工作。

downloadPhotosWithCompletion應該在所有圖片下載任務都完成后再調用自己的completion閉包。問題是:你怎么監視并發的異步事件呢?你不知道它們何時完成,以何種順序。

也許你可以用多個Bool值來追蹤下載情況,但那不容易擴展。而且坦白講,那是很丑陋的代碼。

幸運的是,dispatch groups就是專為監視多個異步任務的完成情況而設計的。

調度組(Dispatch Groups)

調度組在一組任務都完成后會發出通知。這些任務可以是異步或同步的,甚至可以分布在不同的隊列。調度組還可以通過同步或異步的方式來通知。因為任務在不同的隊列中,disptch_group_t實例用來追蹤隊列中的不同任務。

在組內所有事件都完成時,GCD API提供了兩種方式發送通知。

第一種是dispatch_group_wait,它會阻塞當前進程,直到所有任務都完成或是等待超時。這正是我們的例子中需要的方式。

打開 PhotoManager.swift ,替換downloadPhotosWithCompletion:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
func downloadPhotosWithCompletion(completion: BatchPhotoDownloadingCompletionClosure?) {
  dispatch_async(GlobalUserInitiatedQueue) { // 1
    var storedError: NSError!
    var downloadGroup = dispatch_group_create() // 2
  
    for address in [OverlyAttachedGirlfriendURLString,
                    SuccessKidURLString,
                    LotsOfFacesURLString]
    {
      let url = NSURL(string: address)
      dispatch_group_enter(downloadGroup) // 3
      let photo = DownloadPhoto(url: url!) {
        image, error in
        if let error = error {
          storedError = error
        }
        dispatch_group_leave(downloadGroup) // 4
      }
      PhotoManager.sharedManager.addPhoto(photo)
    }
  
    dispatch_group_wait(downloadGroup, DISPATCH_TIME_FOREVER) // 5
    dispatch_async(GlobalMainQueue) { // 6
      if let completion = completion { // 7
        completion(error: storedError)
      }
    }
  }
}

逐一來看注釋:

  • 因為使用dispatch_group_wait阻塞了當前進程,要用dispatch_async將整個方法放到后臺隊列,才能保證主線程不被阻塞。

  • 創建一個調度組,作用好比未完成任務的計數器。

  • dispatch_group_enter通知調度組一個任務已經開始。你必須保證dispatch_group_enter和dispatch_group_leave是成對調用的,否則程序會崩潰。

  • 通知任務已經完成。再一次,這里保持進和出相匹配。

  • dispatch_group_wait等待所有任務都完成直到超時。如果在任務完成前就超時了,函數會返回一個非零值??梢酝ㄟ^返回值來判斷是否等待超時;不過,這里你用DISPATCH_TIME_FOREVER來表示一直等待。這意味著,它會永遠等待!沒關系,因為圖片總是會下載完的。

  • 此時,你可以保證所有圖片任務都完成或是超時了。接下來在主隊列中加入完成閉包。閉包晚些時候會在主線程中執行。

  • 執行閉包。

運行app,下載幾張圖片,留意你的app是如何表現的。

Note:如果網速太快以至于分辨不出何時執行的閉包,你可以修改設備的設置。在 Setting 中的Developer Section 。打開 Network Link Conditioner,選擇“Very Bad Network”。

如果在模擬器上,用工具變更網速。這是你武器庫中一個很好的工具,它讓你清楚在不佳的網絡下你的app會發生什么。

這個方案目前不錯,但最好能避免阻塞進程。你下一步的工作是重寫這個方法來異步通知下載完成。

在學習下一個調度組的用法前,先看看怎樣在不同的隊列類型下使用調度組。

  • 自定義順序隊列:好選擇。當一組任務完成時用它發送通知。

  • 主隊列(順序):在當前情景下是不錯的選擇。但你要謹慎地在主隊列中使用,因為同步等待所有任務會阻塞主線程。然而,當一個需要較長時間的任務(比如網絡請求)完成時,異步更新UI是很好的選擇。

  • 并發隊列:好選擇。用于調度組和通知。

調度組,再來一次

做的不錯,但是異步調度到另一個隊列然后用 dispatch_group_wait 阻塞還是有一些笨拙。還有另一種方式…

在 PhotoManager.swift 中找到downloadPhotosWithCompletion并替換之:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
func downloadPhotosWithCompletion(completion: BatchPhotoDownloadingCompletionClosure?) {
  // 1
  var storedError: NSError!
  var downloadGroup = dispatch_group_create()
  
  for address in [OverlyAttachedGirlfriendURLString,
                  SuccessKidURLString,
                  LotsOfFacesURLString]
  {
    let url = NSURL(string: address)
    dispatch_group_enter(downloadGroup)
    let photo = DownloadPhoto(url: url!) {
      image, error in
      if let error = error {
        storedError = error
      }
      dispatch_group_leave(downloadGroup)
    }
    PhotoManager.sharedManager.addPhoto(photo)
  }
  
  dispatch_group_notify(downloadGroup, GlobalMainQueue) { // 2
    if let completion = completion {
      completion(error: storedError)
    }
  }
}

異步方法是如何工作的:

  • 新的實現不需要把方法放進dispatch_async中,因為你并沒有阻塞主線程。

  • dispatch_group_notify異步執行閉包。當調度組內沒有剩余任務的時候閉包才執行。同樣要指明在哪個隊列中執行閉包。當下,你需要在主隊列中執行閉包。

這是更優雅的方法,并且不會阻塞任何進程。

并發過多帶來的危險

通過支配這些新工具,你應該將每件事都線程化,對嗎?

Thread_All_The_Code_Meme.jpg

看看PhotoManager中的downloadPhotosWithCompletion。你會發現通過for循環下載了三張圖片?,F在來看看能否通過并發執行for循環來提速。

是時候請出dispatch_apply了。

dispatch_apply像for循環一樣,只不過它會并發地執行循環過程。這個函數是同步的,所以像普通的for循環一樣,dispatch_apply在所有工作都完成后才返回。

要注意循環的最佳次數,如果有太多循環但每個循環內只有很小的工作量,那么額外的開銷會抹殺掉并發帶來的好處。 步進 (striding)可以幫助到你。它讓你在每次循環中做多件工作。

什么時候用dispatch_apply合適?

  • 自定義順序隊列:在順序隊列中使用dispatch_apply完全無意義;它的效果和for循環一樣。

  • 主隊列(順序):理由同上,用for循環就可以了。

  • 并發隊列:明智之選,尤其是你需要追蹤任務進度時。

替換downloadPhotosWithCompletion如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
func downloadPhotosWithCompletion(completion: BatchPhotoDownloadingCompletionClosure?) {
  var storedError: NSError!
  var downloadGroup = dispatch_group_create()
  let addresses = [OverlyAttachedGirlfriendURLString,
                   SuccessKidURLString,
                   LotsOfFacesURLString]
  
  dispatch_apply(UInt(addresses.count), GlobalUserInitiatedQueue) {
    in
    let index = Int(i)
    let address = addresses[index]
    let url = NSURL(string: address)
    dispatch_group_enter(downloadGroup)
    let photo = DownloadPhoto(url: url!) {
      image, error in
      if let error = error {
        storedError = error
      }
      dispatch_group_leave(downloadGroup)
    }
    PhotoManager.sharedManager.addPhoto(photo)
  }
  
  dispatch_group_notify(downloadGroup, GlobalMainQueue) {
    if let completion = completion {
      completion(error: storedError)
    }
  }
}

現在你的循環可以并發執行了;調用 dispatch_apply 時,第一個參數是循環的次數,第二個參數是執行任務的隊列,第三個參數是閉包。

盡管你的代碼在添加圖片時是線程安全的,但是圖片的順序取決于線程完成的順序。

運行app,用 Le Internet 添加一些圖片,發現不同了嗎?

在真機上運行新的代碼會發現 些許 的速度提升。但是這值得嗎?

實際上,在這里并不值得這么做。原因如下:

  • 你很可能因為并行而花費了比for循環更多的開銷。你應該結合合適的步長對 非常大 的集合使用dispatch_apply。

  • 開發app的時間有限——不要花時間過早優化。如果你想優化,那么就優化那些值得優化的東西。用Instruments測試app以找到最耗時間的方法。如何使用Instruments。

  • 一般說來,代碼優化會讓你的代碼變得更復雜。你要確定帶來的好處值得你增加復雜性。

記住,不要癡迷于優化。否則只會讓你自己為難,也讓看你代碼的人抓狂。

取消調度塊

iOS 8 和 OS X Yosemite引入了 調度對象塊 (dispatch block object)。它們實現起來就像對閉包再包裝一層。調度對象塊可以做到很多事情,比如為隊列中的對象設置QoS等級來決定優先級,但最顯著的能力是可以取消塊的執行。要明白對象塊只有在輪到它執行之前才可以取消(一旦開始執行就不能取消了)。

為了說明這個問題,首先用 Le Internet 下載一些圖片,然后取消它們。替換 PhotoManager.swift 中的downloadPhotosWithCompletion:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
func downloadPhotosWithCompletion(completion: BatchPhotoDownloadingCompletionClosure?) {
  var storedError: NSError!
  let downloadGroup = dispatch_group_create()
  var addresses = [OverlyAttachedGirlfriendURLString,
                   SuccessKidURLString,
                   LotsOfFacesURLString]
  addresses += addresses + addresses // 1
  var blocks: [dispatch_block_t] = [] // 2
  
  for in 0 ..< addresses.count {
    dispatch_group_enter(downloadGroup)
    let block = dispatch_block_create(DISPATCH_BLOCK_INHERIT_QOS_CLASS) { // 3
      let index = Int(i)
      let address = addresses[index]
      let url = NSURL(string: address)
      let photo = DownloadPhoto(url: url!) {
        image, error in
        if let error = error {
          storedError = error
        }
        dispatch_group_leave(downloadGroup)
      }
      PhotoManager.sharedManager.addPhoto(photo)
    }
    blocks.append(block)
    dispatch_async(GlobalMainQueue, block) // 4
  }
  
  for block in blocks[3 ..< blocks.count] { // 5
    let cancel = arc4random_uniform(2) // 6
    if cancel == 1 {
      dispatch_block_cancel(block) // 7
      dispatch_group_leave(downloadGroup) // 8
    }
  }
  
  dispatch_group_notify(downloadGroup, GlobalMainQueue) {
    if let completion = completion {
      completion(error: storedError)
    }
  }
}
  • 擴展addresses數組,將每個地址復制3份。

  • 這個數組用來保存接下來創建的對象塊。

  • dispatch_block_create創建一個對象塊。第一個參數是一個表明了塊特征的標志。此處的標志讓塊從它進入的隊列那里繼承QoS等級。第二個參數是閉包形式的塊定義。

  • 塊被異步的調度到全局主隊列。這里用全局主隊列是因為它是一個順序隊列,可以方便我們取消對象塊。當前代碼已經在主線程中執行著,所以你可以保證下載任務將在此之后才執行(也就是這個downloadPhotosWithCompletion返回后才輪到下載任務執行)。

  • 取數組中第三個到結尾的部分。

  • arc4random_uniform會隨機返回一個0到上界之間(不含上界)的整數。以2為上界會得到0或1,像投硬幣一樣。

  • 如果隨機數是1,則取消塊。前提是,塊還在隊列中并且沒開始。塊在執行的過程中是不可以取消的。

  • 因為所有塊都加入調度組了,不要忘記移除被取消的那些塊。

運行,從 Le Internet 添加圖片。你會看到app下載3張圖片,以及隨機數量的額外圖片。那些沒下載的圖片是因為在加入隊列 后 被取消了。這是一個刻意設計的例子,但是很好的演示了怎樣使用調度對象塊以及如何取消它。

調度對象塊能做更多事情,別忘了查看文檔。

五花八門的GCD趣用

等等!還有更多!下面展示一些常規用途之外的功能。盡管你不會經常使用這些工具,但他們可能在特定情況下非常有用。

測試異步代碼

這聽起來很瘋狂,但是你知道Xcode擁有測試功能嗎?:]我知道,有時我喜歡假裝它不存在,但是編寫和運行測試對構建復雜的代碼很重要。

Xcode中的測試運行在XCTestCase的子類之下,它會運行所有以test開頭的方法。測試跑在主線程下,所以你可以認為測試是順序執行的。

一旦給定的測試方法返回了,XCTest 會認為這個測試完成了而去做下一個測試。這就是說,在下一個測試執行過程中,前一個測試中的異步代碼也在繼續執行。

網路請求通常是異步的,因為你不想阻塞主線程。一旦測試方法返回,測試也就結束了,因此很難對網絡請求做測試。

我們簡單看一下兩種普遍的測試異步代碼的方法:信號量(semaphores)和 期望(expectations)。

信號量

信號量是一個古老學院派的線程概念,它是由謙遜的Edsger W. Dijkstra提出的。信號量是很復雜的話題,因為它建立在錯綜復雜的操作系統函數之上。

如果你想了解更多信號量的知識,查閱細說信號量原理。如果你是學院派,有一個用到了信號量的經典軟件開發問題叫做哲學家進餐問題。

信號量讓你控制多個消費者對有限資源的獲取。例如,如果你創建一個信號量來控制擁有2個資源的資源池,那么同一時刻最多有兩個線程可以進入臨界區。其它也想使用資源的線程必須在FIFO隊列中等待。

打開 GooglyPuffTests.swift 并替換掉 downloadImageURLWithString:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func downloadImageURLWithString(urlString: String) {
  let url = NSURL(string: urlString)
  let semaphore = dispatch_semaphore_create(0) // 1
  let photo = DownloadPhoto(url: url!) {
    image, error in
    if let error = error {
      XCTFail("/(urlString) failed. /(error.localizedDescription)")
    }
    dispatch_semaphore_signal(semaphore) // 2
  }
  
  let timeout = dispatch_time(DISPATCH_TIME_NOW, DefaultTimeoutLengthInNanoSeconds)
  if dispatch_semaphore_wait(semaphore, timeout) != 0 { // 3
    XCTFail("/(urlString) timed out")
  }
}

以上代碼中信號量的工作原理: 
1. 創建信號量。參數表明信號量起始值。這個值代表了起始階段可以獲取信號量的線程數目(增加信號量就是發信號,用0做初始值代表當前沒有線程可以獲取信號量)。 2. 在完成閉包中,你告訴信號量不再需要資源。這會使信號量增加,同時給其他等待資源的任務發信號,通知當前信號量可用。 
3. 等待信號量并設置超時時間。這個調用會阻塞當前進程直到收到信號。非0返回表示等待已超時。在這種情況下,測試失敗,因為網絡請求不應該超過10秒——相當合理的假設! 
(譯者注:說下我的理解:首先創建了信號量,但此時因為信號量是0,沒有線程可以獲取它,注釋3中對信號量的等待會阻塞。只有在圖片下載好了以后,才會發送一個信號量,那么注釋3對信號量的獲取就成功了,并退出等待。但如果圖片下載失敗呢?就不會調用注釋2這句觸發信號的語句,那么注釋3就會等待超時,從而測試失敗。)

PRoduct/Test 或 cmd+U 運行測試。測試應該成功。

斷掉網絡連接并再次測試;如果在真機測試,請開啟飛行模式。如果在模擬器上,直接斷網就好了。測試在10秒后會返回失敗的結果。很好,起作用了!

這是相當微不足道的測試,但是如果你和服務端團隊一起工作,這些基礎測試可以避免一些涉及網絡問題的無端指責。

期望(expectations)

XCTest框架提供了另一種使用 期望 來測試異步代碼的方法。這種特性讓你首先設置你的期望——你希望發生的事——然后再開始異步任務。接下來測試會一直等待,直到異步任務將期望標記為 已完成 。

替換 GooglyPuffTests.swift 中的downloadImageURLWithString:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func downloadImageURLWithString(urlString: String) {
  let url = NSURL(string: urlString)
  let downloadExpectation = expectationWithDescription("Image downloaded from /(urlString)"// 1
  let photo = DownloadPhoto(url: url!) {
    image, error in
    if let error = error {
      XCTFail("/(urlString) failed. /(error.localizedDescription)")
    }
    downloadExpectation.fulfill() // 2
  }
  
  waitForExpectationsWithTimeout(10) { // 3
    error in
    if let error = error {
      XCTFail(error.localizedDescription)
    }
  }
}

工作原理: 
1. 用expectationWithDescription生成期望。測試會在日志上顯示其中的字符串參數,所以請描述你期望發生的事。 
2. 在異步執行的閉包中調用fulfill來標記期望已達成。 
3. 調用線程用waitForExpectationsWithTimeout等待期望達成。如果等待超時會視為出錯。

運行測試。結果和使用信號量沒什么不同,但使用XCTest框架是更清晰易讀的方案。

調度源(Dispatch Sources)

GCD中存在一個特別有趣的特性叫調度源,它是一個包含底層功能的百寶囊,幫助你響應或監控Unix信號,文件描述符(file descriptors),Mach端口,VFS Nodes,以及其他復雜的東西。所有這些都超出了本教程的范圍,但是你可以嘗試著使用一下調度源對象。

第一次使用調度源的用戶可能會迷失其中,所以你首先要理解dispatch_source_create的工作原理。下面是創建它的函數原型:

1
2
3
4
5
func dispatch_source_create(
  type: dispatch_source_type_t,
  handle: UInt,
  mask: UInt,
  queue: dispatch_queue_t!) -> dispatch_source_t!

第一個參數type: dispatch_source_type_t是最重要的參數,因為它描述了句柄(handle)和掩碼(mask)參數。你需要查看Xcode文檔來弄清楚dispatch_source_type_t的參數有哪些可選項。

這里你會監視DISPATCH_SOURCE_TYPE_SIGNAL。如文檔所述:

調度源監控當前進程的信號。句柄(handle)是信號數字(int)。掩碼(mask)沒用到(傳0)。

Unix信號列表可以從signal.h找到。在頂部有一串#define。在這些信號列表中,你將要監控SIGSTOP信號。這個信號會在進程接收到不可抗拒的掛起指令時被發送。這個信號與你用LLDB debugger調試程序時發送的信號相同。

進入 PhotoCollectionViewController.swift ,在viewDidLoad附近添加下面的代碼。你需要為類添加兩個私有屬性,并在viewDidLoad的開始處添加段代碼,在調用superclass和ALAssetLibrary之間:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#if DEBUG
private var signalSource: dispatch_source_t!
private var signalOnceToken = dispatch_once_t()
#endif
  
override func viewDidLoad() {
  super.viewDidLoad()
  
  #if DEBUG // 1
  dispatch_once(&signalOnceToken) { // 2
    let queue = dispatch_get_main_queue()
    self.signalSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_SIGNAL,
                                               UInt(SIGSTOP), 0, queue) // 3
    if let source = self.signalSource { // 4
      dispatch_source_set_event_handler(source) { // 5
        NSLog("Hi, I am: /(self.description)")
      }
      dispatch_resume(source) // 6
    }
  }
  #endif
  
  // The other stuff
}

這段代碼有點難懂,因此逐個注釋來講解: 
1. 最好只在DEBUG模式下編譯這段代碼,因為這可能讓不懷好意者洞見很多信息。:] 在 Project Settings –> Build Settings –> Swift Compiler – Custom Flags –> Other Swift Flags –> Debug 下添加 -D DEBUG 。 
2. 用dispatch_once一次性初始化調度源。 
3. 初始化signalSource變量。你指明對信號感興趣并且提供SIGSTOP做第二個參數。除此之外,你用主隊列處理接收到的事件——稍后你會發現為什么。 
4. 如果參數錯誤,調度源對象不會被創建。因此,你應該在使用它之前確保調度源是有效的。 
5. dispatch_source_set_event_handler注冊了一個事件處理閉包,當你接收到監控的信號時會調用這個閉包。 
6. 默認情況下,所有調度源在開始都處于掛起狀態。當你想監視事件時,必須讓源對象繼續執行。

運行app;暫停調試器然后立即恢復。檢查控制臺(console),你會看到類似下面的信息:

1
2014-08-12 12:24:00.514 GooglyPuff[24985:5481978] Hi, I am:

你的app現在可以感知到調試(debugging-aware)了!這真棒,但在現實中怎樣用它呢?

你可以用它調試一個對象并在恢復app時展示數據;你也可以自定義一些安全邏輯來保護app,當惡意攻擊者在你的程序上附著調試器的時候。

有趣的想法是把這個方法當做堆棧追蹤工具,來找到你想要在調試器中修改的對象。

16.jpg

設想一下這樣的場景。當你意外地停掉調試器時,你很難處在期望的棧幀上。而現在你可以在任意時刻停止調試器并讓代碼執行到你期望的位置。這很有用,當你想執行一段從調試器很難達到的代碼。試一試!

在viewDidLoad中的NSLog語句處設置斷點。暫停調試器,然后再開始;app會命中你剛剛設置的斷點。現在你已經深入到PhotoCollectionViewController方法中了?,F在你可以隨心所欲地使用PhotoCollectionViewController實例了。多么便捷!

注意:如果在調試器中你不知道哪個線程是哪個,來看一下。主線程總是第一個,libdispatch,GCD的協調器是第二個。剩下的線程要看硬件當時在做什么樣的工作。

在調試器中,輸入:

1
po self.navigationItem.prompt = "WOOT!"

然后繼續執行app。你會看到如下所示:

 

18.png

通過這個方法,你可以更新UI,探查類的屬性,甚至執行方法——無需重啟app來進入特定的工作流狀態。很巧妙。

下一步?

下載最終的工程。

我不想重提,但是你真的應該看一下怎樣使用Instruments。如果你想優化app,絕對需要這個。Instruments可以概述程序中哪些代碼相對其它代碼執行更久。如果你想知道代碼實際的執行時間,很可能需要一些自制的解決方案。

同時學習如何在Swift中使用NSOperations和NSOperationQueue,一種基于GCD的并發技術。實際上,這是使用GCD的最佳實踐。NSOperations提供更好的控制,處理最多的并發操作,在犧牲一定速度的情況下更加面向對象。

記住,除非你有特別的理由深入底層,你應該始終嘗試并堅持使用更高層的API。只在你想學習更多或做一些非常非常“有趣”的事時才進入到Apple的“暗黑藝術”(dark art)中探險。:]

祝你好運,盡情歡樂!


發表評論 共有條評論
用戶名: 密碼:
驗證碼: 匿名發表
亚洲香蕉成人av网站在线观看_欧美精品成人91久久久久久久_久久久久久久久久久亚洲_热久久视久久精品18亚洲精品_国产精自产拍久久久久久_亚洲色图国产精品_91精品国产网站_中文字幕欧美日韩精品_国产精品久久久久久亚洲调教_国产精品久久一区_性夜试看影院91社区_97在线观看视频国产_68精品久久久久久欧美_欧美精品在线观看_国产精品一区二区久久精品_欧美老女人bb
欧美日韩精品在线视频| 97久久精品在线| 亚洲成**性毛茸茸| 欧美日韩国产va另类| 亚洲а∨天堂久久精品喷水| 欧美一区第一页| 97视频免费观看| 久久久久久91香蕉国产| 亚洲第一网站男人都懂| 日韩电影中文字幕在线| 亚洲国产精品久久精品怡红院| 欧美日韩国产页| 69av在线播放| 51精品在线观看| 精品国产一区二区三区久久| www.午夜精品| 国产亚洲综合久久| 91国产美女视频| 日韩精品在线免费观看视频| 91亚洲精品在线观看| 亚洲一区二区久久久| 欧美激情亚洲精品| 成人免费大片黄在线播放| 久久网福利资源网站| 久久久精品国产亚洲| 日日骚久久av| 成人国产精品免费视频| 欧美激情videoshd| 久久久久久免费精品| 国产日本欧美一区二区三区在线| 欧美激情视频在线免费观看 欧美视频免费一| 亚洲精品自拍第一页| 欧亚精品在线观看| 亚洲激情免费观看| 亚洲石原莉奈一区二区在线观看| 俺去亚洲欧洲欧美日韩| 久久国产精品首页| 最近2019年手机中文字幕| 日韩国产在线播放| 欧美日韩午夜激情| 久久久av亚洲男天堂| 精品国内自产拍在线观看| 久久视频在线免费观看| 欧美香蕉大胸在线视频观看| 欧美最猛黑人xxxx黑人猛叫黄| 亚洲国产女人aaa毛片在线| 亚洲最大福利视频| 日韩国产欧美精品一区二区三区| 欧美日韩国产va另类| 伊人久久免费视频| 日韩在线视频中文字幕| 成人福利视频在线观看| 国产精品一区二区三区毛片淫片| 91亚洲精品在线| 视频在线一区二区| 日韩美女写真福利在线观看| 欧美激情在线观看视频| 亚洲最大成人在线| 一区二区三区美女xx视频| 日韩欧美在线一区| 欧美日韩亚洲激情| 国产精品一区二区av影院萌芽| 亚洲视频电影图片偷拍一区| 91tv亚洲精品香蕉国产一区7ujn| 国产精品伦子伦免费视频| 欧美日韩免费区域视频在线观看| 欧美重口另类videos人妖| 久久久久久久久综合| 欧美最猛性xxxxx免费| 欧美在线激情视频| 国产精品午夜视频| 亚洲精品www久久久久久广东| 日韩一区二区av| 成人国内精品久久久久一区| 欧美—级高清免费播放| 亚洲一区二区少妇| 色小说视频一区| 欧美视频国产精品| 8090理伦午夜在线电影| 亚洲成人激情图| 亚洲国产精彩中文乱码av| 成人免费淫片aa视频免费| 日韩精品高清在线观看| 精品国内自产拍在线观看| 亚洲欧美在线第一页| 日本成人免费在线| 人九九综合九九宗合| 国产成人免费91av在线| 精品国产依人香蕉在线精品| 国产成人精品免高潮费视频| 日韩女优人人人人射在线视频| 欧美一区深夜视频| 色在人av网站天堂精品| 亚洲人成伊人成综合网久久久| 亚洲专区在线视频| 91国自产精品中文字幕亚洲| 美女性感视频久久久| 欧美亚州一区二区三区| 欧美日韩国产色| 午夜欧美大片免费观看| 俺去亚洲欧洲欧美日韩| 欧美激情视频免费观看| 岛国av在线不卡| 久久久久久久一区二区| 日产精品久久久一区二区福利| 92国产精品视频| 欧美国产在线视频| 久久精品国产一区二区三区| 亚洲欧美日韩中文在线| 国产精彩精品视频| 欧美在线视频播放| 国产精品美女www爽爽爽视频| 国产一区二区三区视频| 午夜精品一区二区三区在线播放| 日韩中文字幕精品视频| 最新国产成人av网站网址麻豆| 色偷偷噜噜噜亚洲男人的天堂| 免费不卡在线观看av| 96pao国产成视频永久免费| 亚洲一区二区福利| 欧美精品videossex88| 国产精品高精视频免费| 久久99热这里只有精品国产| 美女av一区二区三区| 国产精品一二三在线| 在线播放日韩专区| 精品中文字幕久久久久久| 国内精品美女av在线播放| 国产精品中文字幕久久久| 色综合色综合久久综合频道88| 亚洲精品动漫久久久久| 热久久美女精品天天吊色| 色偷偷av一区二区三区| 久久精品国产亚洲精品2020| 粗暴蹂躏中文一区二区三区| 亚洲a成v人在线观看| 日韩在线精品视频| 久久久久久久一区二区三区| 最近2019中文免费高清视频观看www99| 久久青草精品视频免费观看| 成人网在线免费观看| 久久99国产精品久久久久久久久| 日韩av在线免费看| 91久久国产婷婷一区二区| 亚洲精品成人网| 国产91精品黑色丝袜高跟鞋| 欧美精品videosex牲欧美| 欧美性高潮床叫视频| 欧美激情亚洲一区| 亚洲午夜未满十八勿入免费观看全集| 国产91精品久| 国产一区二区av| 97视频在线观看视频免费视频| 国产欧美日韩91| 欧美性少妇18aaaa视频| 中文字幕在线观看亚洲| 久久亚洲成人精品| 久久久久久久久久久av| 在线播放亚洲激情| 精品电影在线观看| 久久精品最新地址| 国产精品第七十二页| 中文字幕成人精品久久不卡| 中文欧美日本在线资源|