探索 Swift 中的 MVC-N 模式

AudCadman 8年前發布 | 24K 次閱讀 Swift Apple Swift開發

 

Marcus 將會為大家介紹一種設計模式,他曾經在那些需要從互聯網進行大量頻繁數據請求的 iOS 應用當中使用此設計模式。這個設計采用了著名的 MVC (Model View Controller) 模式,并且在此基礎之上對其進行了擴展,從而允許使用異步網絡調用并與用戶界面控制器相互隔離。

See the discussion on Hacker News .

Transcription below provided by Realm: a replacement for SQLite & Core Data that makes building iOS apps a joy. Check out the docs!

Get new videos & tutorials — we won’t email you for any other reason, ever.

About the Speaker: Marcus Zarra

Marcus S.Zarra 自從 2003 年開始就開始開發 Cocoa 應用了,自 1996 年開始開發 Java 軟件,并且自 1985 年開始進入到這個行業當中了。目前,Marcus 正在為 iOS 和 OS X 開發軟件。除了編寫軟件之外,他還在 Cocoa Is My Girlfriend 博客上發表關于開發的相關博文,并提供相應的代碼示例,以幫助其他開發者。Marcus 同樣還是 Core Data (3rd edition): Data Storage and Management for iOS, OS X, and iCloud 這本書的著者,此外還是 Core Animation: Simplified Animation Techniques for Mac and iPhone Development 的共同著者之一。

@mzarra

本篇文章并不會為大家介紹一種新的設計模式。相反,我們會探索某些『亙古不變』的內容:也就是關于網絡層方面的東西。

在我的職業生涯當中,我曾不止一次地見到過擁有相同反面模式 (Anti-pattern,譯者注:也就是所謂的『不完善的設計模式』)的項目及代碼庫。它總是伴隨著新項目的出現而出現。

我們開始編程:我們已經構建了設計、UI,以及用戶體驗。我們會基于我們的 UI 或者 UX 設計稿來構建視圖控制器,我們樂此不疲。然而,視圖控制器需要數據( 在這個世界上不存在那種不需要使用數據的 iOS 應用 ),通常這個數據是從網絡獲取的。我們在視圖控制器中通過一個網絡請求來抓取數據(這是一個很常見的模式了),然后從服務器得到返回的數據。這完全沒有問題:我們的 UI 將會展示出來,我們為之感到非常高興。 第一步已經邁出

接下來跳轉到 UI 上的下一個頁面,現在我們就必須要構建另一個視圖控制器。這個控制器需要不同的數據,甚至可能是從另一個服務器獲取的數據。您需要再次做一邊同樣的事情,因為我們在第一個視圖控制器中所做的運行得很成功。我們構建界面,或許會使用故事板來構建。然后我們連接網絡,獲取數據,最后在屏幕上將其展示出來。 我們再一次成功了

然后我們會構建第三個頁面。在第三、第四,甚至第五個視圖控制器當中,我們可能會意識到我們需要來自第一或者第二個視圖控制器當中的數據,而這些數據可能已經從內存中清除掉了(或者您可能會做一些 奇怪的事情 ,比如說在導航控制器當中抓取數據)。我們將會遍歷包含視圖控制器的數組,以找出所需的視圖控制器的索引值,并在旁邊提供注釋(『不要移動這個!』),從而從其中拿出相關的數據。 這個也是沒有問題的

然后,UX 設計師過來告訴你:『我們需要對 UI 當中的一些界面重新進行排序,不過會給用戶帶來更好的用戶體驗』。他們建議將第四個視圖控制器移動到第二個位置上。 哦,這出問題了 ,因為我們需要的是在序列當中第二個視圖控制器中獲取的數據。現在咋辦?

您或許可以選擇復制代碼,但是我們知道這是一個糟糕的主意。您可能會開始做一些『有意思』的工作。或許您可能會在屏幕之外去加載視圖控制器,或者在初始化視圖控制器但是卻不把它放到堆棧當中( 將其提交后,我們就完成了獲取數據的所有操作,因為您想要這段代碼可以運行 )。接著您可能會想了『我需要做點什么,比如說緩存數據什么的』。說實話,您可以使用任何您希望使用的持久化引擎。

我會選擇 Core Data(我也很:heart:它)。Realm 也是不錯的選擇,JSON 文件也是可用得的,只要可以用來緩存數據就可以了。

因此,您緩存了數據,改變了圖層,然后每個視圖控制器仍會留在緩存當中,以便能夠獲取數據,當任何一個視圖控制器出現之后,都可以在緩存之外獲取數據。 不過,這仍然還是一個壞主意

這就是我所說的『反面模式』。這種類似的東西我見到過的已經數不勝數了,我自己也寫過類似的東西。我們討厭這樣的模式。我在此公開呼吁大家:別在這么做了。 我們想要寫出好的代碼,我們想要看到正確的結果,然而我們卻寫出這樣糟糕的代碼。這著實是一個嚴肅的問題

我們現在都普遍應用著 MVC 架構,但是我們需要在 MVC 的基礎上多增加點東西。

我想介紹另一種控制器,不過不是所謂的視圖控制器。

注意:如果您不喜歡『控制器』一詞的話,那么可以使用『管理器 (manager)』、『處理器 (handler)』或者其他您覺得可以的詞。關于是這是一個控制器對象,因為它既不是視圖,也不是模型

這就是我們應該在 設計時 所應該做的事,而不是在第二天早晨發布版本的關鍵時候,說:『我需要對所有這些東西進行重構』。當我們擁有 UI/UX,我們直到應用是需要如何出現的,這也是我們需要查看的時候所需要了解的東西。

『我需要從網絡獲取數據。我應該把這個從網絡獲取出數據的代碼放到何處呢?』 這就是 MVC-N 大顯身手的時刻了。

我們將擁有一個網絡控制器:它會與互聯網、您的本地網絡,甚至與純文本文件 (Flat file) 系統建立連接。接下來它會將數據放到您的緩存、或者持久化存儲引擎當中。這里最精彩的部分在于:您的整個視圖層都只與緩存進行溝通交流。它不會直接從網絡中獲取任何數據。 視圖控制器的任務就是展示或者更新數據就可以了 ,但是不會執行任何的網絡操作。這個設計可以伴隨您的一生,并且它擁有許多好處,以至于您就不會再去使用反面模式的代碼了。

class NetworkController: NSObject {
    let queue = NSOperationQueue()
    var mainContext: NSManagedObjectContext?

    func requestMyData()
    func requestMyData() -> NSFetchedResultsController

    func requestMyData(completion: (Void) -> Bool)
}

這里沒有太多的內容:這是一個對象。我讓它繼承了一個對象(即便是在 Swift 當中)因為我會在我的網絡控制器當中使用 KVO。您或許不必跟著我這樣做;如果您使用的是另一種不同的緩存系統的話,您或許就不需要 KVO。

只是 一個對象而已,我們不必要根據視圖控制器(或者其他類似的東西)來決定它是什么樣的。它是一個底層對象。我們在其中設立了兩個屬性:一個變量和一個常量。我們持有一條隊列和我們的緩存機制(Core Data, 因為我很喜歡 Core Data )。

暴露給您的視圖控制器的將是相關的便利方法和語法糖。我們可以用多種方式來構建這些內容,這取決于您有多信任那些制作 UI 的人。如果您信任他們的話,您可以使用一個函數,然后說『去獲取我的數據吧』,您可以信任他們,給予他們直接訪問緩存層的權利,將數據取出,然后在他們各自的視圖控制器當中展示出來。然而,我并不是非常信任他們,因此我會給他們第二種版本的方法,也就是用返回 NSfetchedResultsController 來替代。

NSfetchedResultsController 對于 Core Data 來說是一個很好的用于連接緩存和視圖控制器的膠水代碼 (Glue Code)。它會將所有數據以所需的結構,按照所需的過濾模式,并依據所需的次序進行排列,最后傳遞給視圖控制器。如果您擁有一個地址列表的話,它會按照字母順序將這個列表進行重新排列,提供給您以進行填充。

NSfetchedResultsController 的另一方面就是,它會在數據發生變化的時候通知視圖控制器。視圖控制器無需與緩存進行交互,它所需要的,只是 NSfetchedResultsController 就可以了,從中可以拿出相應的數據,它會提供相關的委托回調來通知何時數據發生了改變。這極大地簡化了視圖控制器。 您的視圖控制器只需要等待被通知刷新就可以了

如果您的應用在遵循某種常見的 UX 模式的話(例如下拉刷新),您的視圖控制器或許會想要更多的信息,例如數據發生了更新。它可能需要知曉數據請求何時結束。如果您擁有一個下拉刷新的話,然后用戶不停地下拉、下拉、下拉,這時候您就進入了一個未知的隧道當中——用戶可能會發出多個請求。

您或許會想要用某種方式提示他們說:『請求正在運行,暫未結束』。這就是一個所謂的承諾系統 (Promise System)。您可以將它們傳遞到一個閉包中,然后說『讓我知道您結束的時刻。我可以更新刷新控件,然后不再用這個消息纏著您』。

回到網絡控制器這里來,這就是所有它應該擁有的外部接口。這些就是您想要公開的東西,甚至可能沒有這個隊列或者上下文,這取決于您有多信任制作 UI 的那幫人。

那么我們用這個網絡控制器做些什么呢? 我們需要深入視圖控制器內部。有以下幾種方式:

這只是我個人的建議。當您的應用啟動之后,就會調用 applicationDidFinishLaunching 方法。您可以在這里構建您的根視圖控制器,您也需要在這里構建網絡控制器,這樣您就可以將網絡控制器傳遞到根視圖控制器當中。接著,當下一個場景或者下一個視圖控制器出現的時候,根就會將網絡控制器傳遞給下一個,依此類推。這就是所謂的『依賴注入 (Dependency Injection)』。

這是從 Objective-C 那里得來的一個簡單的概念,這樣就不需要任何第三方框架了:我們只需要傳遞這個對象,然后設置為下一個對象的屬性就可以了。

第二種方法是我很討厭的方式:您可以將網絡控制器轉變為單例的形式。這是一個完全安全的單例,因為單例的缺點之一就是不能有會導致單例失效的情況發生。您無法對單例進行重置,因為這會違背這個要求。不過,因為網絡控制器本身完全是一個空的架構,它很容易重置其內部的要素,因此您可以放心大膽地使用單例。

不過,我還是建議您不要使用單例。單例是萬惡之源。如果您的系統和應用需要使用單例,并且也只能使用單例的話,那么我也不會(太)反對您。繼續用就是了。

第三種方式,同時也是最糟糕的一種方式,就是直接訪問 AppDelegate。您的每個視圖控制器直接訪問 AppDelegate,然后調用 UIApplication.ApplicationDelegate 然后將其進行轉換。我同樣也強烈反對您使用這種方式,因為它會將元件的耦合度增加太多。

總體來看,請使用依賴注入這種方式。另外兩個方法都可能在生產環境中等著您踩雷。

網絡控制器 - NSOperation

class MyNetworkRequest: NSOperation { var context: NSManagedObjectContext? init(myParam1: String, myParam2: String) }

網絡控制器是 基于網絡操作 (Network Operation) 進行封裝的 。網絡操作執行后會生成一個對網絡的請求操作;數據回傳并處理之后,這樣您接下來就可以將其保存到緩存當中,這樣所有操作就結束了。這些都是操作的 任務最小離散單元 (small discrete unit of work) 。這是很優雅的做法,當您編寫代碼的時候,您可以在屏幕上看到任何您想要的東西。您可以輕易理解它,它只做一件事情:這很贊,所生成的代碼也很好維護。這就是我們想要在網絡控制器當中所構建的東西。

我喜歡使用一個繼承自 NSOperation 的 MyNetworkRequests 。每個 NSOperation 都是專門只執行任務最小離散單元的,比如說獲取 推ter 的時間軸信息。當您讓操作執行任務最小離散單元的時候,您可以將這些離散單元串聯在一起,然后對代碼進行重用。

關于重用代碼有以下兩種思路:

  1. 『我會將其發布在網上,這樣每個人都可以通過 CocoaPod 來進行重用。』:-1:

  2. 『我會在應用的 15 處不同的地方使用相同的代碼。我需要做的就是在一處地方進行改變,我的整個應用都會進行更新。』:+1:

這些方法看起來怎樣?

這些都是 NSOperation 的子類。在它們當中,有兩個我們可以設置的屬性。再說一次,我們使用的是依賴注入:我們將某項對緩存的訪問操作注入到當前操作當中,然后再傳遞進一些參數(或許是 NSURLRequest 、 URL 或者某些和搜索參數一樣簡單的內容),然后讓這個操作自行構建完整的 URL。

警告 :大段代碼即將來襲!不要被嚇跑了哦~:runner:

class MyNetworkRequest: NSOperation { var context: NSManagedObjectContext? private var innerContext: NSManagedObjectContext? private var task: NSURLSessionTask? private let incomingData = NSMutableData() init(myParam1: String, myParam2: String) { super.init() //在這里設置參數 } var internalFinished: Bool = false override var finished: Bool { get { return internalFinished } set (newAnswer) { willChangeValueForKey("isFinished") internalFinished = newAnswer didChangeValueForKey("isFinished") } } }

開始的時候,我們繼承 NSOperation 。我們有好幾個屬性設置為私有的,它們對于 NSOperation 來說是可內部訪問的,因此我們把它們標記為私有,這樣可以提醒我們,不要在外部碰這些玩意兒。

第一個屬性是 innerContext (為了可以在多線程上使用 Core Data,我們可以持有多個上下文變量)。如果您打算使用不同的緩存機制的話,請遵循它們對于多線程訪問的設計指導。

對于 NSURLSession 來說,我是又愛又恨(不過更傾向于喜歡),但是對于在 iOS 9 已經廢除的 NSURLConnection 來說,已經是一個極大地提升了。我討厭的部分是,如果您想要使用它的閉包實現 (block implementation) 的話,這將是一個棘手的問題。因為它會讓您折回到 UIViewController 里面去,然后突然一下子您讓當前的這個視圖控制器離開了屏幕,結果發生了內存問題。不過,您同樣可以在 NSOperation 設計當中使用它,我覺得這是最好的方式。

我們不能夠再使用 NSoperation 當中的主要方法了。現在,無論您再怎么努力, NSOperation 都不會在您執行網絡連接的時候暫停了。您可以使用私有 API( 不建議這么做 ),您也可以做一些同步的操作,或者操作鎖定的操作( 不建議 )。在 iOS 6 或者 7 當中,它們會在不再執行并發的時候,改變 NSOperation 。您無法在已經調用過操作的線程上再讓另一個操作運行起來了。無論您將 isConcurrent 屬性設置為何值,它都會忽略您的操作。它始終保證能夠在另一個線程,或者是另一個隊列上運行。

我們可以使用 NSOperation 的并發設計來執行我們的網絡請求。在 Swift 當中,由于我們不能夠直接訪問 finished 屬性,我們必須要做一點黑科技(關于這個我已經提交了一個 radar,希望他們能夠修復這個問題)。重載這個 變量 的目的在于,我們需要告訴隊列我們已經結束了操作。為了實現這個效果,隊列將會監聽 “isFinished” 這個屬性。在 Objective-C 當中, isFinished 翻譯為變量 Finished 是完全沒有問題的;在 Swift 中,它只能夠監聽 “isFinished”,它實際上并不能夠監聽實際的 Finish 屬性,您無法對此進行改變。

這就是為什么我們的 setter 方法非常有趣,因為其中增加了 KVO 這一模塊。不管我們有沒有結束任務,我們都會讓自己的內部狀態變量保持掛起狀態。無論何時我們對其進行了改變,我們都會讓 isFinished 訪問器觸發 KVO。

func URLSession(session: NSURLSession, dataTask: NSURLSessionDataTask, didReceiveResponse response: NSURLResponse, completionHandler: (NSURLSessionResponseDisposition) -> Void) { if cancelled { finished = true sessionTask?.cancel() return } //檢查回調代碼,并根據結果做出相應的回應 completionHandler(.Allow) }
func URLSession(session: NSURLSession, dataTask: NSURLSessionDataTask, didReceiveData data: NSData) { if cancelled { finished = true sessionTask?.cancel() return } incomingData.appendData(data) }

您可以將下面這兩個方法放到一個抽象父類當中。通常情況下,您可以獲取回調結果,然后離開網絡控制器,說『去連接此服務器,然后請求該數據』。服務器會立即給您相應的回調結果。

注釋所在的地方是您放置邏輯和查看回調結果的地方:『狀態返回碼是什么?是不是正常的 200?』好的,萬事大吉。『如果不是呢?』好吧,應用崩潰了。但是我們想要對這種情況進行處理。一旦我們知道我們成功的獲取到了來自服務器的數據、狀態,我們就向完成閉包 (completion block) 當中傳遞一個參數,這個閉包會分析我們傳遞進去的數據,然后告知我們 (.Allow) 。隨后告知 URLConnection 繼續執行,開始將獲取的數據返回到我們的 NSURLSession 當中。 如果您隨意測試一下便會發現,這個方法的實現運行良好。

另一個方法可能會被多次調用,也可能不會被調用。調用的次數并不完全取決于數據的大小。有許多變量可以決定它,例如說蜂窩數據和 Wi-Fi 狀態下調用次數會不同,或者是否發生丟包現象。我們要做的唯一一件事情就是將它添加到我們內部的變量當中,因為我們不知道我們會獲取多少次數據。我們需要保持調用,保證數據的持續輸入,然后將這些比特流持續添加到 NSData 數組當中,然后我們要持續等待,直到我們得到另一個回調為之,也就是我們是否被告知網絡訪問已經結束。

func URLSession(session: NSURLSession, task: NSURLSessionTask, didCompleteWithError error: NSError?) { if cancelled { finished = true sessionTask?.cancel() return } if error != nil { self.error = error log("Failed to receive response: \(error)") finished = true return } //向 Core Data 中寫入數據 finished = true }

關于上面這兩個另外的部分是最后的部分了,但是這也是最有趣的部分。

這個方法是我們能得到的最后一個部分,它將要表述『我成功地獲取了數據』或者『我沒有成功獲取數據』。不管從服務器獲取數據成功與否,它們都將會調用此方法。如果獲取數據失敗的話,我們會將錯誤對象進行賦值,隨后我們會對這個錯誤進行處理。我們將會檢查 cancelled: 以確定我們是否取消了網絡操作,我們對發生的錯誤并不關心。

不過我們需要檢查錯誤。您可以使用 if let ,但是這貨給您帶來的幫助并不大。我一般會將這個錯誤掛起。外面如果有人使用這個操作,并且還監聽這個操作是否已經完成的話,那么他便可以查看這個錯誤,然后對其做出回應。從這里,我不想要告訴 UI 層哪里出問題了,因為這些人的任務只是將數據提取出來,然后根據數據做出相應的處理就夠了。假設我們一切順利(例如我們沒有取消網絡操作,并且我們也沒有遇到任何錯誤), 這時候我們便已經有了從網絡上下載下來的數據了 。我們已經結束了網絡的訪問,這樣我們便可以開始下一步操作。我們或許會在這里處理這個數據。

在同樣的操作當中,我們將其保存到緩存當中;或者,我們可能會決定鏈接到另外的操作當中。這兩個都是解決此問題的有效解決方案。

但是 我會在這里對數據進行處理 ,然后再存放到緩存當中。我不會通知我的 UI:我只是將數據放到緩存當中,只有緩存有責任去通知 UI。我們必須在底部設置 finished = true 。

因此,這就是我所謂的 MVC-N 了,我希望大家都可以使用這個設計實現。首先,就如我所討論過的,任務最小離散單元讓代碼更容易理解、使用。當下一個開發者接手并需要修復網絡控制器中的某個問題的時候,他們不必看完一大段長長的代碼;他們只需要查看相的獨立代碼了。這是一場巨大的勝利。

有一次,我在舊金山這里加入到了一個項目當中。他們的主程去度假了,因此他們讓我加入進來去解決主程離開之后出現的那些 BUG。他們發現了一個 BUG,然后跟我說『如果您能夠在兩周內解決這個問題,我們就可以按時上線了』。

這個 BUG 是:『在啟動的時候,數據可能會出現,也可能不會出現』。這就是這個 BUG 的表現形式。

我拿到了源代碼,然后我啟動了應用,但是我沒發現這個問題。我等了會兒。一些繪制的很漂亮的 TableView 單元格出現了,然后它們消失了。接著另一個不同的單元格又出現了,然后又消失了。接著,數據終于趨于穩定正確了。

我一遍又一遍地啟動,嘗試了不同的次序以獲取不同的數據,但是這仍然導致相同的結果。我必須一遍又一遍地嘗試,嘗試了大概半個小時,還沒有看任何代碼,只是不停地重啟而已。隨后我就開始看代碼了:

我發現他們使用的是 Core Data,因此我可以立刻上手。我試圖去在視圖控制器當中尋找網絡代碼,但是實際上他們使用了一個網絡控制器。他們在一個文件中對其進行了本地化;我覺得這應該要更簡單、更好用。

我打開了這個網絡控制器(他們叫它『DataHandler』),里面有 12,000 行代碼。

這玩意兒有點蠢蠢的。開發者通過寫了一個方法,告訴它『快去獲取數據吧!』。當數據回傳之后它們調用了一個閉包。在其中,它使用了另一個閉包用以將數據轉換為 JSON 格式。在這里,它使用了另一個閉包將數據存放到 Core Data 當中。閉包中嵌入了閉包,閉包中又嵌入了閉包。不停地異步、異步、異步。

如果這貨只執行一次的話,那么它絕對不會有問題。他重復使用了這個模式,最終同時調用了將近 15 個網絡調用;15 個異步的網絡調用返回了 15 個異步數據處理調用,然后又返回 15 個異步緩存調用,然后又引起了 15 次以上的 UI 更新。這些全都是異!步!的!哪個數據首先出現在屏幕上完全取決于哪個調用首先抵達服務器,哪個調用首先返回,以及哪個調用首先同步到主線程上面。這絕對是一個爛攤子。

這就是閉包調用的陷阱之一。單次運行,沒有任何問題。您覺得『這是一個相當簡單、精簡、獨立的代碼單元了:我要重復使用它』。

我們可以通過執行操作來修復這個問題。我們將這些操作放到一個隊列當中。隊列控制調用的頻率如何,以及我們對這些操作如何觸發進行更多的控制。通過這玩意兒,我們可以讓單元測試變得輕松,因為現在我們有了任務最小離散單元了。我們可以初始化操作;我們可以手動觸發它。我們可以調用 OperationStart ,然后它就會觸發掉,之后我們就可以自己監聽結束方法了。我們可以構建單元測試,然后在主線程運行,然后等待結束,最后根據數據做出回應。我們可以對每一個操作進行獨立測試。

我們可以更進一步。如果我們向操作當中注入 URL,我們甚至可以控制操作發生的地方。我們可以使用在硬盤上的純文本文件,然后現在我們就有了一致的輸入,這樣就可以測試輸出了。

甚至我們還可以更進一步,我們可以使用一個已知的 SQLite 文件作為緩存來進行測試。現在,我們有了已知的輸入,已知的原始數據,我們就可以測試輸出了。我們的單元測試是端對端的,這樣就可以測試每個網絡調用了。

在閉包代碼中,這就比較難做了;您最終不得不選擇進行數據模擬。我們要做的就是說『您不再是與服務器進行數據交流,而是和這個文件進行數據交流』,隨后您就會去使用這個 SQLite 文件,而后現在『我打算去測試您的結果,以確保您得到了正確的回調結果』。單元測試變得更容易、輕松了。

您從前可能沒有聽說過 Steve Jobs 的這條格言,這是我最喜歡的一句話之一: 『當我的代碼準確無誤的時候,人們都視之為常。我并沒有得到任何的贊譽,也沒有得到任何的鼓勵。而當我的代碼出現問題的時候,所有的責備蜂擁而至』。

這就是我所在的處境。當這個網絡控制器工作良好的時候,沒有人在意這個問題。當我的網絡操作沒法工作的時候,突然間他們就說我的應用跟:hankey:一樣。無論我們有多少 UI 工程師,無論我們在 UX 上面花了多少錢,這個應用就是個渣,因為我沒法在我坐地鐵的時候獲取數據。

這就是網絡層的責任所在。存儲層應該告知用戶『我無法連接網絡,這里是最近一次我得到的數據』。如果您無法理解這個概念的話,退出您手機上的 非死book,然后到一個電梯里面去,把電梯門關掉,然后再啟動 非死book。啊哦,沒法用了。我們可以通過對應用進行恰當的設計,添加緩存,從而修復這個問題。

操作可以探知網速

最重要的是,我們也可以關心 網絡的當前狀態 。當我們開始一個操作的時候,我們知道它會以調用 NSState 開始,以再次調用 NSState 結束,這樣我們就可以將兩者互減,就可以知道這個操作花費了多長時間。我們同樣也知道返回了多少數據。 NSData 有一個名為 length 的屬性,這告訴了我們從服務器獲取回來的這條數據當中有多少位的數據。位長 / 時間 = 帶寬。如果我將帶寬告知給我的網絡控制器,并對這些帶寬聚合求平均值的話,我就可以根據我的網絡狀態添加一個 動態運行時的解決方案 了。我知道我的網絡的速度是多少,并且精確到秒。

通過隊列來控制性能

我可以對此做出回應,因為 NSOperationQueue 有一個概念叫作『并發 (Concurrency)』。這樣我就可以說『網絡狀態良好,那么我們就啟動并發,盡可能多地一次性運行完全部操作』。或者,我也可以返回來說『網絡很糟糕。我們要把速度放慢下來,一次只執行一件事情,然后在我所有的 NSOperations 中設置優先級,這樣高優先級的操作會優先觸發』。 而不是在 推ter 上等待貓咪的圖片刷出來,文字內容會優先加載,而喵咪的圖片:smiley_cat: 會在后面加載出來

網絡控制器可以對網絡狀態做出回應

現在我的應用反應更靈敏了,因為時間線加載請求比其他請求具有更高的優先級。

我可以更進一步,『網絡狀態不好,我會取消所有的低優先級操作』。我可以暫停所有對圖片的請求,這樣我就可以離開時間線請求,直到網絡狀態改善,再去發表東西。

取消操作的關鍵位置

同樣,對于取消操作而言,我們有一個很關鍵的位置。當應用退出的時候,我可以殺死所有的操作。我可以這么說『應用準備退出了,取消所有的頭像加載請求,取消所有的時間線加載請求。如果您試圖發表東西的話,我就會去向系統申請更多的時間,然后讓那個操作結束』。即使我已經退出了應用,您的發表狀態仍然還是在進行當中。

我們不必要這么做。我們沒必要侮辱我們的用戶,然后迫使他們等待我們這些蹩腳的代碼。

后臺處理的單決策點

您知道當您試圖退出應用的時候,您按下了 Home 按鈕,但是這時候應用卡死了!用戶就會想『啊,這個手機跟:hankey:一樣。速度太慢了』。手機:這個鍋我不背。這個鍋應該歸咎于寫代碼的人,他告訴應用『準備進入后臺』,因此應用給系統回應『好吧,我要先來執行這個操作,再進入后臺』。這樣操作系統會停止一切操作,直到應用的這個方法返回。因此這會帶來幾秒中的延遲,然后才給應用傳遞 kill -9 。

這會導致手機的主線程被阻塞,直到所有的工作完成為止,因為后臺處理的操作遠比前往其他應用的操作更重要。我們沒必要這么做。我們可以做得更好一些,即使別人不會注意到,不過我們的應用會運行得更好,操作系統也會很樂意看到這樣的。

  • 自行完成網絡代碼

    您注意到,我沒有使用 AFNetworking ,我也沒有使用 ASIHTTPRequest 。請自行完成。我保證您自行完成的代碼遠比任何通用的代碼要快得多,這是一項常理了。因為您寫的東西是非常具體到您的業務的,它將會比任何通用的東西快得多。

  • 保證所有的網絡代碼都歸類到一起。不要把網絡代碼放到視圖控制器當中。

    不然的話您所做的就是將您的活動范圍限制在一個角落當中,出了問題之后您就不得不選擇重構。一旦您在視圖控制器當中寫了網絡代碼的話,在它后面放上一個 TODO 標記:『我需要對其進行重構』。避免那樣做,您應該:執行網絡代碼,把它放到一個地方,這樣您會更輕松。

  • 所有的 UI 都應該根據 Core Data 來表現

    您的所有 UI 都應該根據緩存來顯示。您的 UI 不應該直接與網絡直接交互。 在一個理想狀態下,UI 并不知道您有沒有對網絡進行訪問。它所知道的就是緩存當中有它想要的數據

  • 分離數據采集和數據顯示

    保證數據顯示和數據收集彼此分離:這樣您的應用就更容易維護,更容易修改,更容易添加新的功能,并且更容易重用。

我現在工作的項目當中,我們已經重寫了 4 次代碼了,因為我們不知道 UI 應該是何種樣子。我們仍舊在開發當中。然而,無論我們怎么折騰,底層的網絡層仍舊工作良好。我們已經重構了許多次 UI 層了,但是我們不必去重寫網絡層,因為它是完全抽象的,我們已經有三個月沒去碰它了。

這就是我希望大家所做的。 請停止將網絡訪問代碼放到您的視圖控制器當中。 將您的網絡視作應用的一等公民。如果您的網絡訪問代碼受影響了,那么您的整個應用也會受影響。每次您坐在 ail 應用面前,然后瘋狂的下拉刷新,您或許會想:這個網絡訪問寫得真糟糕。我們很容易讓它變得更容易一些,一旦您開始這么做,您就不會使用別的方式了。

如果您有什么想問我的問題的話,請在 推ter [email protected]

,或者給我發送 郵件

。我很高興回答諸位的問題。

 

See the discussion on Hacker News .

 

Get new videos & tutorials — we won’t email you for any other reason, ever.

來自: https://realm.io/cn/news/slug-marcus-zarra-exploring-mvcn-swift/

 

 本文由用戶 AudCadman 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!