iOS apprentice中文版 Chapter 35: 異步聯(lián)網(wǎng) Asynchronous networking

你的應(yīng)用程序可以進(jìn)行網(wǎng)絡(luò)搜索,運(yùn)行得很好。同步網(wǎng)絡(luò)調(diào)用并沒有那么糟糕,不是嗎?
是的,我來告訴你為什么!你有沒有注意到,每當(dāng)你進(jìn)行搜索時(shí),應(yīng)用程序就會(huì)變得沒有響應(yīng)?當(dāng)網(wǎng)絡(luò)請(qǐng)求發(fā)生時(shí),您不能向上或向下滾動(dòng)表視圖,也不能在搜索欄中鍵入任何新內(nèi)容。應(yīng)用程序完全凍結(jié)了幾秒鐘。

如果你的網(wǎng)絡(luò)連接非??欤憧赡軟]有見過這種情況,但如果你在野外使用iPhone,網(wǎng)絡(luò)速度會(huì)比家里或辦公室的Wi-Fi慢得多,搜索很容易就會(huì)花費(fèi)10秒甚至更多時(shí)間。

對(duì)于大多數(shù)用戶來說,沒有響應(yīng)的應(yīng)用程序就是崩潰的應(yīng)用程序。用戶可能會(huì)按下home鍵再試一次——或者更有可能的情況是,刪除你的應(yīng)用程序,在應(yīng)用程序商店給它打個(gè)差評(píng),然后切換到一個(gè)與之競(jìng)爭(zhēng)的應(yīng)用程序。
因此,在本章中,您將學(xué)習(xí)如何使用異步網(wǎng)絡(luò)來解決UI響應(yīng)問題。你可以這樣做:

  1. 極端同步網(wǎng)絡(luò): 了解同步網(wǎng)絡(luò)如何通過將同步網(wǎng)絡(luò)調(diào)到最大來影響應(yīng)用程序的性能。
  2. 活動(dòng)指示器:添加一個(gè)活動(dòng)指示器來顯示正在進(jìn)行的搜索,以便用戶知道正在發(fā)生什么。
  3. 異步:更改web服務(wù)請(qǐng)求在后臺(tái)線程上運(yùn)行的代碼,這樣它就不會(huì)鎖定應(yīng)用程序。

極端同步網(wǎng)絡(luò)

還不相信同步網(wǎng)絡(luò)的弊端嗎?”讓我們放慢網(wǎng)絡(luò)連接速度,假裝這個(gè)應(yīng)用程序運(yùn)行在iPhone上,而某人可能在公交車或火車上使用這個(gè)應(yīng)用程序,而不是在快速家庭或辦公室網(wǎng)絡(luò)的理想條件下。
首先,您將增加應(yīng)用程序接收的數(shù)據(jù)量——通過向URL添加一個(gè)“l(fā)imit”參數(shù),您可以設(shè)置web服務(wù)將返回的結(jié)果的最大數(shù)量。默認(rèn)值是50,最大值是200。

?打開SearchViewController.swift并在iTunesURL(searchText:)中,將web服務(wù)URL更改為:

let urlString = String(format: 
  "https://itunes.apple.com/search?term=%@&limit=200", 
  encodedText)

您向URL添加&limit=200。正如您所知道的,url中的參數(shù)由&符號(hào)分隔,也稱為“and”或“& and”符號(hào)。
如果你現(xiàn)在運(yùn)行這個(gè)應(yīng)用程序,搜索應(yīng)該會(huì)慢一些。

網(wǎng)絡(luò)連結(jié)調(diào)節(jié)器(The network link conditioner)

還是太快了,看不到任何應(yīng)用程序響應(yīng)問題?那就使用網(wǎng)絡(luò)鏈接調(diào)節(jié)器network link conditioner。這是蘋果提供的一個(gè)額外的開發(fā)工具,它允許你模擬不同的網(wǎng)絡(luò)條件,比如糟糕的手機(jī)網(wǎng)絡(luò),以便測(cè)試你的iOS應(yīng)用程序。
但首先,在使用它之前,您可能必須安裝Network Link Conditioner,因?yàn)檫@不是默認(rèn)安裝的東西,既不是macOS的一部分,也不是Xcode安裝的一部分。

?從Xcode菜單中選擇Open Developer Tool→More Developer Tool


這應(yīng)該會(huì)在你的默認(rèn)瀏覽器中打開蘋果開發(fā)者的下載網(wǎng)頁——你可能會(huì)被要求先登錄蘋果開發(fā)者門戶,因?yàn)檫@是一個(gè)只有注冊(cè)的蘋果開發(fā)者才能使用的資源。


正如截圖所示,搜索“additional tools”。您應(yīng)該得到一個(gè)不同下載的列表。根據(jù)發(fā)布日期選擇最新版本,下載,打開DMG文件,切換到DMG上的硬件文件夾,然后雙擊Network Link Conditioner.prefPane來安裝它。
現(xiàn)在可以使用Network Link Conditioner作為系統(tǒng)首選項(xiàng)面板選項(xiàng)。



Let’s simulate a really slow connection.
? Click on Manage Profiles and create a new profile by clicking the plus button on the bottom left. Add the following settings:
Name: Very slow connection
Downlink Bandwidth: 48 Kbps
Downlink Packets Dropped: 0 %
Downlink Delay: 5000 ms, i.e. 5 seconds

Press OK to add this profile and return to the main screen. Make sure this new profile is selected and flick the switch to ON to start the Network Link Conditioner.
? Now run the app and search for something. The Network Link Conditioner tool will delay the HTTP request by 5 seconds in order to simulate a slow connection, and then downloads the data very slowly.

Tip: If the download still appears very fast, then try searching for some term you haven’t used before; the system may be caching the results from a previous search.

Notice how the app totally doesn’t respond during this time? It feels like something is wrong. Did the app crash or is it still doing something? It’s impossible to tell and very confusing to your users when this happens.
Even worse, if your program is unresponsive for too long, iOS may actually force kill it, in which case it really does crash. You don’t want that to happen!
“Ah,” you say, “l(fā)et’s show some type of animation to let the user know that the app is communicating with a server. Then at least they will know that the app is busy.”
That sounds like a decent thing to do, so let’s get to it.

Tip: Even better than pretending to have a lousy connection on the Simulator is to use Network Link Conditioner on your device, so you can also test bad network connections on your actual iPhone. You can find it under Settings → Developer → Network Link Conditioner. Using these tools to test whether your app can deal with real-world network conditions is a must! Not every user has the luxury of broadband…
Also, if you do not see the Developer option in the iOS Settings app, you might need to connect your iPhone to your computer via a USB cable, and launch the Xcode Devices and Simulators window so that your device is recognized by Xcode as a developer device.

活動(dòng)指標(biāo)(The activity indicator)

你之前在MyLocations中使用過旋轉(zhuǎn)活動(dòng)指示器,向用戶顯示應(yīng)用程序正在忙。讓我們創(chuàng)建一個(gè)新的表格視圖單元格,在應(yīng)用程序查詢iTunes商店時(shí)顯示。它會(huì)是這樣的:


活動(dòng)指示表視圖單元格
創(chuàng)建一個(gè)新的空nib文件。稱之為L(zhǎng)oadingCell.xib。
?將一個(gè)新的TableViewCell拖放到畫布上。設(shè)置寬度為320點(diǎn),高度為80點(diǎn)。
?將單元格的重用標(biāo)識(shí)符設(shè)置為L(zhǎng)oadingCell,并將Selection屬性設(shè)置為None。
?拖拽一個(gè)新的Label到單元格中。將標(biāo)題設(shè)置為L(zhǎng)oading…并將字體更改為System 15。標(biāo)簽的文本顏色應(yīng)該是50%不透明的黑色。
?將一個(gè)新的Activity Indicator View拖拽到單元格中,并將其放在標(biāo)簽旁邊。把它的Style設(shè)為Gray,給它打上100的tag。


要使此單元格在較大的屏幕上正常工作,您將添加一些約束,以保持標(biāo)簽和活動(dòng)微調(diào)器位于單元格的中心。最簡(jiǎn)單的方法是將這兩項(xiàng)放到容器視圖中并居中

?選擇Label和Activity Indicator View——按住?選擇多個(gè)項(xiàng)目。從Xcode菜單欄中,選擇Editor→Embed In→View Without Inset。這將在選定的視圖后面放置一個(gè)白色視圖。


注意:如果你想知道在 Editor菜單中Embed In → View 和Embed In → View Without Inset之間有什么不同,試試它,你應(yīng)該會(huì)看到發(fā)生了什么。第一個(gè)選項(xiàng)添加了一個(gè)比它所包含的項(xiàng)稍大一些的視圖,因?yàn)樗呀?jīng)嵌入了新視圖,以便在所包含的項(xiàng)周圍添加一些填充。第二個(gè)選項(xiàng),就是你用的那個(gè),簡(jiǎn)單地把所有的東西都包起來,不加任何標(biāo)簽。

選中這個(gè)新的容器視圖后,單擊Align按鈕,復(fù)選標(biāo)記Horizontally in Container與Vertically in Container以創(chuàng)建新的約束。


最終會(huì)出現(xiàn)一些紅色約束。這是沒有好;我們想看藍(lán)色的。你的新約束是紅色的原因是自動(dòng)布局不知道這個(gè)容器視圖應(yīng)該有多大;您只是為視圖的位置添加了約束,而不是它的大小。
要解決這個(gè)問題,您還需要向標(biāo)簽和活動(dòng)指示器添加約束,這樣容器視圖的寬度和高度就由其中兩個(gè)元素的大小決定。

這對(duì)于以后你要把應(yīng)用翻譯成另一種語言時(shí)尤其重要。如果Loading……文本變大或變小,那么容器視圖也應(yīng)該變大或變小,以便保持單元格內(nèi)的居中。

?選擇標(biāo)簽并點(diǎn)擊Add New Constraints按鈕。只需將它釘在所有四個(gè)邊,并按下添加4約束。

?在活動(dòng)指示器視圖中重復(fù)這個(gè)動(dòng)作。您不需要將它固定在左邊,因?yàn)檫@個(gè)約束已經(jīng)存在——將標(biāo)簽固定在左邊添加了它。
現(xiàn)在標(biāo)簽和活動(dòng)指示器的約束應(yīng)該都是藍(lán)色的。
此時(shí),容器視圖可能仍然有橙色的線,表示約束沒有問題,但是視圖的框架沒有處于適當(dāng)?shù)奈恢?。如果是,選擇它并選擇“Editor → Resolve Auto Layout Issues → Update Frames - under Selected Views。這將把容器視圖移動(dòng)到它的約束所指定的位置。
酷,你現(xiàn)在有了一個(gè)可以自動(dòng)調(diào)整自身大小的屏幕。

使用活動(dòng)指示Cell

要使這個(gè)特殊的表視圖單元格出現(xiàn),您將按照與“Nothing Found”單元格相同的步驟。
?將下面的一行添加到SearchViewController.swift的TableView.Cellidentifier結(jié)構(gòu)中。

static let loadingCell = "LoadingCell

?并在viewDidLoad()中注冊(cè)nib:

cellNib = UINib(nibName: TableView.CellIdentifiers.loadingCell, 
                bundle: nil)
tableView.register(cellNib, forCellReuseIdentifier: 
                   TableView.CellIdentifiers.loadingCell)

現(xiàn)在,您必須想辦法讓table view的數(shù)據(jù)源知道應(yīng)用程序目前正處于從服務(wù)器下載數(shù)據(jù)的狀態(tài)。最簡(jiǎn)單的方法是添加另一個(gè)布爾標(biāo)志。如果該變量為真,則應(yīng)用程序正在下載內(nèi)容并顯示新的load . cell;如果變量為false,則顯示table視圖的常規(guī)內(nèi)容。

?添加一個(gè)新的實(shí)例變量:

var isLoading = false

? 將tableView(_:numberOfRowsInSection:) 修改為:

func tableView(_ tableView: UITableView, 
               numberOfRowsInSection section: Int) -> Int {
  if isLoading {
    return 1
  } else if !hasSearched {
    . . . 
  } else if . . . 

if isLoading條件返回1,因?yàn)樾枰恍衼盹@示單元格。

?將tableView(_:cellForRowAt:)更新如下:

func tableView(_ tableView: UITableView, 
    cellForRowAt indexPath: IndexPath) -> UITableViewCell {
  // New code 
  if isLoading {
    let cell = tableView.dequeueReusableCell(withIdentifier: 
        TableView.CellIdentifiers.loadingCell, for: indexPath)
        
    let spinner = cell.viewWithTag(100) as! 
                  UIActivityIndicatorView
    spinner.startAnimating()
    return cell
  } else 
  // End of new code
  if searchResults.count == 0 {
    . . .

您添加了一個(gè)if條件來返回新Loading…單元格的實(shí)例。它還通過tag查找UIActivityIndicatorView,然后告訴轉(zhuǎn)輪開始動(dòng)畫。方法的其余部分保持不變。

?將tableView(_:willSelectRowAt:)改為:

func tableView(_ tableView: UITableView, 
     willSelectRowAt indexPath: IndexPath) -> IndexPath? {
  if searchResults.count == 0 || isLoading {    // Changed
    return nil
  } else {
    return indexPath
  }
}

你在if語句中添加了||。就像您不希望用戶選擇“Nothing Found”單元格一樣,您也不希望他們選擇“Loading…”單元格,因此在這兩種情況下都返回nil。
現(xiàn)在只剩下一件事:在向iTunes服務(wù)器發(fā)出HTTP請(qǐng)求之前,應(yīng)該將isLoading設(shè)置為true,并重新加載table視圖,使Loading…cell出現(xiàn)。

?將searchBarSearchButtonClicked(_:)更改為:

func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
  if !searchBar.text!.isEmpty {
    searchBar.resignFirstResponder()
    // New code
    isLoading = true                    
    tableView.reloadData()
    // End of new code
    . . .
    isLoading = false                     // New code
    tableView.reloadData()
  }
}

在執(zhí)行網(wǎng)絡(luò)請(qǐng)求之前,將isLoading設(shè)置為true并重新加載表以顯示活動(dòng)指示器。
在請(qǐng)求完成并得到搜索結(jié)果之后,將isLoading設(shè)置回false并重新加載表以顯示SearchResult對(duì)象。
很有道理,對(duì)吧?讓我們啟動(dòng)這個(gè)應(yīng)用程序,看看它是如何運(yùn)行的!

測(cè)試新的加載單元

運(yùn)行應(yīng)用程序并執(zhí)行搜索。當(dāng)搜索正在進(jìn)行時(shí),帶有旋轉(zhuǎn)活動(dòng)指示器的單元格應(yīng)該出現(xiàn)…
……還是應(yīng)該? !
可悲的事實(shí)是沒有一個(gè)轉(zhuǎn)輪可以看到。在不太可能的情況下它才會(huì)出現(xiàn)在你面前,它現(xiàn)在不會(huì)旋轉(zhuǎn)——嘗試啟用Network Link Conditioner。

為了說明原因,首先改變searchBarSearchButtonClicked(_:)如下:

func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
  if !searchBar.text!.isEmpty {
    searchBar.resignFirstResponder()
    isLoading = true
    tableView.reloadData()
    /*
       . . . the networking code (commented out) . . . 
     */
  }
}

注意,您不需要從代碼中刪除任何內(nèi)容——只需在第一次調(diào)用tableView.reloadData()之后注釋掉所有內(nèi)容。

運(yùn)行應(yīng)用程序并進(jìn)行搜索。現(xiàn)在活動(dòng)轉(zhuǎn)輪出現(xiàn)了!
所以至少你知道這部分代碼運(yùn)行良好。但啟用網(wǎng)絡(luò)代碼后,應(yīng)用程序不僅對(duì)用戶的任何輸入完全沒有響應(yīng),而且也不想重繪屏幕。這是怎么回事?

主線程

舊款iPhone和iPad的CPU(中央處理器)只有一個(gè)核心,這意味著它一次只能做一件事。最近的模型有一個(gè)CPU有兩個(gè)核心,這允許同時(shí)進(jìn)行兩個(gè)驚人的計(jì)算。你的Mac可能有4個(gè)內(nèi)核。
由于可用的內(nèi)核如此之少,為什么現(xiàn)代計(jì)算機(jī)可以同時(shí)運(yùn)行更多的應(yīng)用程序和其他進(jìn)程(我的Mac上現(xiàn)在有287個(gè)活動(dòng)進(jìn)程)?

為了克服只有一兩個(gè)CPU核心的硬件限制,包括iPhone和iPad在內(nèi)的大多數(shù)電腦都采用搶占式的多任務(wù)處理和多線程技術(shù),給人一種可以同時(shí)做很多事情的錯(cuò)覺。

多任務(wù)處理是發(fā)生在不同應(yīng)用程序之間的事情。每個(gè)應(yīng)用程序都有自己的進(jìn)程,每個(gè)進(jìn)程每秒鐘都有一小部分CPU時(shí)間來執(zhí)行其任務(wù)。然后它被暫時(shí)停止,或被搶占,控制權(quán)被交給下一個(gè)進(jìn)程。
每個(gè)進(jìn)程包含一個(gè)或多個(gè)線程。我剛才提到每個(gè)進(jìn)程都有一點(diǎn)CPU時(shí)間來完成它的工作。進(jìn)程在它的線程之間分割時(shí)間。每個(gè)線程通常執(zhí)行自己的工作,并且盡可能獨(dú)立于該進(jìn)程中的其他線程。
一個(gè)應(yīng)用程序可以有多個(gè)線程,CPU在它們之間切換:


如果你進(jìn)入Xcode調(diào)試器并暫停應(yīng)用程序,調(diào)試器會(huì)顯示當(dāng)前哪些線程處于活動(dòng)狀態(tài),以及在你停止它們之前它們?cè)谧鍪裁础?br> 對(duì)于StoreSearch應(yīng)用程序,顯然在截屏?xí)r有六個(gè)線程:


大多數(shù)線程都是由iOS自己管理的,你不必?fù)?dān)心。此外,您可能會(huì)看到少于或多于6個(gè)線程。然而,有一個(gè)線程需要特別注意:主線程。在上面的圖像中,這是線程1。

主線程是應(yīng)用程序的初始線程,所有其他線程都從這里派生出來。主線程負(fù)責(zé)處理用戶界面事件,并繪制UI。應(yīng)用程序的大多數(shù)活動(dòng)都是在主線程上進(jìn)行的。每當(dāng)用戶點(diǎn)擊應(yīng)用程序中的按鈕時(shí),執(zhí)行操作方法的線程就是主線程。

因?yàn)樗浅V匾?,所以您?yīng)該小心不要阻塞主線程。如果action方法的運(yùn)行時(shí)間超過幾分之一秒,那么在主線程上執(zhí)行所有這些計(jì)算就不是一個(gè)好主意,因?yàn)檫@會(huì)鎖定主線程。

當(dāng)你讓主線程忙于做其他事情時(shí),它無法處理任何UI事件,如果操作時(shí)間太長(zhǎng),應(yīng)用程序甚至可能被系統(tǒng)殺死,因此應(yīng)用程序變得無響應(yīng)。

在StoreSearch中,您要在主線程上執(zhí)行冗長(zhǎng)的網(wǎng)絡(luò)操作。它可能需要許多秒,甚至幾分鐘來完成。

將isLoading標(biāo)志設(shè)置為true后,告訴tableView重新加載它的數(shù)據(jù),這樣用戶就可以看到旋轉(zhuǎn)動(dòng)畫。但這從未實(shí)現(xiàn)。告訴表視圖重新加載調(diào)度了一個(gè)“重繪”事件,但是主線程沒有機(jī)會(huì)處理該事件,因?yàn)槟⒓磫?dòng)網(wǎng)絡(luò)操作,使主線程長(zhǎng)時(shí)間處于繁忙狀態(tài)。

這就是為什么當(dāng)前的同步網(wǎng)絡(luò)方法是不好的:永遠(yuǎn)不要阻塞主線程。這是iOS編程的主要原罪之一!

使其異步

為了防止阻塞主線程,任何可能需要一段時(shí)間才能完成的操作都應(yīng)該是異步的。這意味著操作發(fā)生在后臺(tái)線程中,同時(shí)主線程可以自由地處理新事件。
這并不是說您應(yīng)該創(chuàng)建自己的線程。如果你以前在其他平臺(tái)上編程過,你可能會(huì)毫無猶豫的創(chuàng)建新線程,但在iOS上這通常不是最好的解決方案。

你看,線程是很棘手的。不是線程本身,而是并行地做事情。我不會(huì)在這里詳細(xì)介紹,但是一般來說,您希望避免兩個(gè)線程同時(shí)修改同一塊數(shù)據(jù)的情況。這可能會(huì)導(dǎo)致非常令人驚訝但不是非常令人愉快的結(jié)果。

iOS有幾種更方便的方式啟動(dòng)后臺(tái)進(jìn)程,而不是自己創(chuàng)建線程。對(duì)于這個(gè)應(yīng)用程序,您將使用隊(duì)列和Grand Central Dispatch (GCD)。GCD大大簡(jiǎn)化了需要并行編程的任務(wù)。您已經(jīng)在MyLocations中簡(jiǎn)單地使用了GCD,但是現(xiàn)在您將更好地使用它。

簡(jiǎn)而言之,GCD有許多具有不同優(yōu)先級(jí)的隊(duì)列。要在后臺(tái)執(zhí)行作業(yè),可以將作業(yè)放入閉包中,然后將該閉包傳遞給隊(duì)列,然后忘記它。就這么簡(jiǎn)單。

GCD將一個(gè)接一個(gè)地從隊(duì)列中獲取閉包(或它所調(diào)用的“塊”),并在后臺(tái)執(zhí)行它們的代碼。它是如何做到的并不重要,你只能保證它發(fā)生在某個(gè)后臺(tái)線程上。隊(duì)列與線程并不完全相同,但是它們使用線程來完成它們的工作。


將web請(qǐng)求放在后臺(tái)線程中

要使web服務(wù)請(qǐng)求異步,您需要將searchBarSearchButtonClicked(_:)中的網(wǎng)絡(luò)部分放到一個(gè)閉包中,然后將該閉包放在一個(gè)中等優(yōu)先級(jí)隊(duì)列中。

?將searchBarSearchButtonClicked(_:)修改如下:

func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
  if !searchBar.text!.isEmpty {
    . . .
    searchResults = []
    // Replace all code after this with new code be“l(fā)ow
    // 1
    let queue = DispatchQueue.global()
    let url = self.iTunesURL(searchText: searchBar.text!)
    // 2
    queue.async {
      
      if let data = self.performStoreRequest(with: url) {
        self.searchResults = self.parse(data: data)
        self.searchResults.sort(by: <)
        // 3
        print("DONE!")
        return
      }
    }
  }
}
  • 這將獲取對(duì)隊(duì)列的引用。您正在使用一個(gè)“全局”隊(duì)列,這是一個(gè)由系統(tǒng)提供的隊(duì)列。你也可以創(chuàng)建你自己的隊(duì)列,但是使用一個(gè)標(biāo)準(zhǔn)的隊(duì)列對(duì)于這個(gè)應(yīng)用來說是可以的。

  • 一旦有了隊(duì)列,就可以對(duì)它分派閉包——隊(duì)列之間的所有內(nèi)容。async{和close}是閉包。閉包中的任何代碼都將放在隊(duì)列上,并在后臺(tái)異步執(zhí)行。在調(diào)度此閉包之后,主線程立即可以繼續(xù)。它不再被阻塞。

  • 在閉包中,我刪除了在搜索完成后重新加載表視圖的代碼,以及錯(cuò)誤處理代碼。目前,這已經(jīng)被print()語句所取代。這是有原因的,我們馬上就會(huì)講到。首先,讓我們?cè)俅螄L試應(yīng)用程序。

運(yùn)行應(yīng)用程序并進(jìn)行搜索?!癓oading…”單元格應(yīng)該是可見的,完成動(dòng)畫微調(diào)!過了一會(huì)兒,你應(yīng)該看到“完成!”消息出現(xiàn)在控制臺(tái)中。
當(dāng)然,裝載過程……Cell會(huì)一直存在,因?yàn)槟氵€沒有讓它離開。

將UI更新放在主線程上

我從閉包中刪除所有用戶界面代碼——并將搜索URL移到閉包之外——的原因是UIKit有一條規(guī)則,UI代碼應(yīng)該總是在主線程上執(zhí)行。這是很重要的!

從多個(gè)線程訪問相同的數(shù)據(jù)會(huì)造成各種各樣的痛苦,因此UIKit的設(shè)計(jì)者決定不允許從其他線程更改UI。這意味著您不能從這個(gè)閉包中重新加載表視圖,因?yàn)樗\(yùn)行在后臺(tái)線程(而不是主線程)上的隊(duì)列上。

碰巧,還有一個(gè)與主線程相關(guān)聯(lián)的“主隊(duì)列”。如果您需要從后臺(tái)隊(duì)列對(duì)主線程執(zhí)行任何操作,您可以簡(jiǎn)單地創(chuàng)建一個(gè)新的閉包并在主線程上調(diào)度主線程操作。

?將searchBarSearchButtonClicked(_:)中顯示print(“完成!”)的一行替換為:

DispatchQueue.main.async {
  self.isLoading = false
  self.tableView.reloadData()
}

在DispatchQueue.main.async您可以在主隊(duì)列上調(diào)度一個(gè)新的閉包。這個(gè)新的閉包將isLoading設(shè)置回false并重新加載表視圖。注意self是必需的,因?yàn)檫@段代碼位于閉包中。
試一試。有了這些變化,您的網(wǎng)絡(luò)代碼就不再占據(jù)主線程,應(yīng)用程序就會(huì)突然變得響應(yīng)性更強(qiáng)!

各種各樣的隊(duì)列

當(dāng)處理GCD隊(duì)列時(shí),您經(jīng)常會(huì)看到這樣的模式:

let queue = DispatchQueue.global()
queue.async {
  // code that needs to run in the background
  
  DispatchQueue.main.async {
    // update the user interface
  }
}

基本上,當(dāng)你在后臺(tái)線程中工作時(shí),你仍然必須切換到主線程來進(jìn)行任何用戶界面更新。事情就是這樣。

還有排隊(duì)。同步,沒有“a”,它從隊(duì)列中獲取下一個(gè)閉包并在后臺(tái)執(zhí)行它,但是會(huì)讓您等待閉包完成。在某些情況下,這可能很有用,但大多數(shù)情況下,您希望使用queue.async。沒有人喜歡等待!

The main thread checker

我之前提到過,不應(yīng)該在后臺(tái)線程上運(yùn)行UI代碼。然而,在Xcode 9之前,沒有一種簡(jiǎn)單的方法可以發(fā)現(xiàn)在后臺(tái)線程上運(yùn)行的UI代碼,只能通過費(fèi)力地逐行搜索源代碼來確定哪些代碼在主線程上運(yùn)行,哪些在后臺(tái)線程上運(yùn)行。

在Xcode 9中,蘋果引入了一個(gè)新的診斷設(shè)置,名為主線程檢查器,如果后臺(tái)線程上運(yùn)行了任何UI代碼,它會(huì)發(fā)出警告。默認(rèn)情況下,這個(gè)設(shè)置應(yīng)該是啟用的,但如果沒有啟用,您可以很容易地啟用它——我建議您隨時(shí)啟用它,因?yàn)樗赡芊浅S袃r(jià)值。

?點(diǎn)擊Xcode工具欄中的scheme下拉菜單,選擇Edit scheme……



?在左側(cè)面板中選擇Run,切換到Diagnostics選項(xiàng)卡,并確保主線程檢查器在運(yùn)行時(shí)API檢查下被選中。

現(xiàn)在,把下面這句話從結(jié)束語outside the closure中移開:

let url = self.iTunesURL(searchText: searchBar.text!)

在封閉的內(nèi)部就像這樣:

queue.async {
    let url = self.iTunesURL(searchText: searchBar.text!)
    ...
} 

運(yùn)行StoreSearch并對(duì)一個(gè)項(xiàng)目進(jìn)行搜索,你應(yīng)該會(huì)在Xcode控制臺(tái)看到如下內(nèi)容

Main Thread Checker: UI API called on a background thread: -[UISearchBar text]
PID: 12986, TID: 11267540, Thread name: (none), Queue name: com.apple.root.default-qos, QoS: 0
Backtrace:
4   StoreSearch                         0x000000010bccfa75 $S11StoreSearch0B14ViewControllerC09searchBarB13ButtonClickedyySo08UISearchF0CFyycfU_ + 469
5   StoreSearch                         0x000000010bcd0101 $S11StoreSearch0B14ViewControllerC09searchBarB13ButtonClickedyySo08UISearchF0CFyycfU_TA + 17
6   StoreSearch                         0x000000010bcd02bd $SIeg_IeyB_TR + 45
7   libdispatch.dylib                   0x000000010f3a1225 _dispatch_call_block_and_release + 12
8   libdispatch.dylib                   0x000000010f3a22e0 _dispatch_client_callout + 8
9   libdispatch.dylib                   0x000000010f3a4d8a _dispatch_queue_override_invoke + 1028
10  libdispatch.dylib                   0x000000010f3b2daa _dispatch_root_queue_drain + 351
11  libdispatch.dylib                   0x000000010f3b375b _dispatch_worker_thread2 + 130
12  libsystem_pthread.dylib             0x000000010f791169 _pthread_wqthread + 1387
13  libsystem_pthread.dylib             0x000000010f790be9 start_wqthread + 13
2018-07-28 11:39:02.726132+0200 StoreSearch[12986:11267540] [reports] Main Thread Checker: UI API called on a background thread: -[UISearchBar text]
PID: 12986, TID: 11267540, Thread name: (none), Queue name: com.apple.root.default-qos, QoS: 0
Backtrace:
4   StoreSearch                         0x000000010bccfa75 $S11StoreSearch0B14ViewControllerC09searchBarB13ButtonClickedyySo08UISearchF0CFyycfU_ + 469
5   StoreSearch                         0x000000010bcd0101 $S11StoreSearch0B14ViewControllerC09searchBarB13ButtonClickedyySo08UISearchF0CFyycfU_TA + 17
6   StoreSearch                         0x000000010bcd02bd $SIeg_IeyB_TR +bcd02bd $SIeg_IeyB_TR + 45
7   libdispatch.dylib                   0x000000010f3a1225 _dispatch_call_block_and_release + 12
8   libdispatch.dylib                   0x000000010f3a22e0 _dispatch_client_callout + 8
9   libdispatch.dylib                   0x000000010f3a4d8a _dispatch_queue_override_invoke + 1028
10  libdispatch.dylib                   0x000000010f3b2daa _dispatch_root_queue_drain + 351
11  libdispatch.dylib                   0x000000010f3b375b _dispatch_worker_thread2 + 130
12  libsystem_pthread.dylib             0x000000010f791169 _pthread_wqthread + 1387
13  libsystem_pthread.dylib             0x000000010f790be9 start_wqthread + 13

你可能還注意到Xcode工具欄的activity視圖現(xiàn)在有一個(gè)紫色的圖標(biāo),在跳轉(zhuǎn)欄的右下角有一個(gè)紫色的圖標(biāo),通常會(huì)顯示錯(cuò)誤。


如果你點(diǎn)擊activity視圖中的圖標(biāo),你將被帶到問題導(dǎo)航器的Runtime選項(xiàng)卡,在那里你可以點(diǎn)擊列出的問題,然后被帶到源代碼中有問題的那一行



你終于看到問題出在哪里了——你從后臺(tái)線程的UI控件搜索欄訪問數(shù)據(jù)。在主線程中這樣做可能更好。由于我們創(chuàng)建這個(gè)問題是為了演示后臺(tái)線程檢查器,所以修復(fù)很簡(jiǎn)單,只需將代碼行移回原來的位置:]

提交代碼

我認(rèn)為有了這個(gè)重要的改進(jìn),應(yīng)用程序應(yīng)該有一個(gè)新的版本號(hào)。因此,提交更改并為v0.2創(chuàng)建一個(gè)tag。您必須按照兩個(gè)單獨(dú)的步驟來完成此操作——首先創(chuàng)建一個(gè)包含適當(dāng)消息的提交,然后為您的最新提交創(chuàng)建一個(gè)tag。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 1.設(shè)計(jì)模式是什么? 你知道哪些設(shè)計(jì)模式,并簡(jiǎn)要敘述?設(shè)計(jì)模式是一種編碼經(jīng)驗(yàn),就是用比較成熟的邏輯去處理某一種類型...
    龍飝閱讀 2,302評(píng)論 0 12
  • Swift1> Swift和OC的區(qū)別1.1> Swift沒有地址/指針的概念1.2> 泛型1.3> 類型嚴(yán)謹(jǐn) 對(duì)...
    cosWriter閱讀 11,673評(píng)論 1 32
  • ORA-00001: 違反唯一約束條件 (.) 錯(cuò)誤說明:當(dāng)在唯一索引所對(duì)應(yīng)的列上鍵入重復(fù)值時(shí),會(huì)觸發(fā)此異常。 O...
    我想起個(gè)好名字閱讀 5,981評(píng)論 0 9
  • 翻譯自“Collection View Programming Guide for iOS” 0 關(guān)于iOS集合視...
    lakerszhy閱讀 4,076評(píng)論 1 22
  • 難過,其實(shí)并沒有那么堅(jiān)不可摧,沒有那么偉大,一次一次麻木感被擊敗,看著身邊的朋友一個(gè)一個(gè)脫單,朋友之間的親密感一點(diǎn)...
    mynbx閱讀 179評(píng)論 0 0

友情鏈接更多精彩內(nèi)容