為避免撕逼嗽交,提前聲明:本文純屬翻譯花颗,僅僅是為了學(xué)習(xí)卖哎,加上水平有限,見(jiàn)諒录淡!
【原文】https://www.teehanlax.com/blog/krush-ios-architecture/
Krush iOS應(yīng)用架構(gòu)
在Teehan+Lax公司捌木,到目前為止,我們已經(jīng)著手開(kāi)發(fā)Krush
項(xiàng)目好幾個(gè)月了赁咙。從iOS
架構(gòu)上講Krush
是一個(gè)非常有趣的應(yīng)用钮莲,因
為它會(huì)涉及到很多iOS新手都會(huì)遇見(jiàn)的常見(jiàn)誤區(qū)。特別是彼水,需要訪問(wèn)API
崔拥,擁有磁盤緩存,展示感興趣內(nèi)容的聯(lián)網(wǎng)應(yīng)用凤覆。在這篇文章中链瓦,我將會(huì)探討一些關(guān)于應(yīng)用方面的案例研究:為什么我們選擇特定的方法,如何在實(shí)踐中應(yīng)用盯桦,以及事后我們應(yīng)該怎么做慈俯。
在90天內(nèi),我們把Krush
作為最小化可行產(chǎn)品(MVP
:minimum viable product)推出拥峦,所以贴膘,“為什么”我們選擇特定方法的動(dòng)機(jī)主要是基于速度:如何快速的獲取能向市場(chǎng)推出的特性和功能最小的集合所組成的可測(cè)試版本,并且在此后能多快的進(jìn)行迭代略号?這些動(dòng)機(jī)影響了我們做的決定刑峡,所以,如果你的動(dòng)機(jī)不同玄柠,你可以通過(guò)這個(gè)鏡頭(lens)看待我們的決定突梦。
案例研究 1:網(wǎng)絡(luò)層
網(wǎng)絡(luò)層主要由我的天才同事Brendan Lynch
構(gòu)建。網(wǎng)絡(luò)層主要負(fù)責(zé)處理Krush
發(fā)送的所有連接羽利,他們調(diào)用服務(wù)器API
或者CDN進(jìn)行資源傳輸宫患。所有的東西都使用一個(gè)公共接口。
我們選擇使用更熟悉了網(wǎng)絡(luò)操作技術(shù)这弧,而不是使用像NSURLSession這樣的新API娃闲。更具體就是,使用屬于我們應(yīng)用委托的請(qǐng)求客戶端管理所有的網(wǎng)絡(luò)活動(dòng)匾浪。這個(gè)請(qǐng)求客戶端持有一個(gè)NSOperationQueue
畜吊,這是管理你網(wǎng)絡(luò)請(qǐng)求的隊(duì)列。
網(wǎng)絡(luò)請(qǐng)求包含URL
户矢、參數(shù)和認(rèn)證編碼規(guī)范。請(qǐng)求對(duì)象知道如何構(gòu)造認(rèn)證NSURLRequests
殉疼,在連接請(qǐng)求失敗的情況下重新建立請(qǐng)求梯浪。網(wǎng)絡(luò)請(qǐng)會(huì)求子類化NSOperation
并遵守NSURLConnectionDataDelegate 協(xié)議捌年。
如果網(wǎng)絡(luò)請(qǐng)求失敗或者超時(shí),請(qǐng)求客戶端將自動(dòng)重新排隊(duì)挂洛,這樣會(huì)嘗試數(shù)次礼预,如果還是無(wú)法連接,那就徹底失敗了虏劲。
每個(gè)操作都有回調(diào)塊(block
)托酸。當(dāng)一個(gè)操作完成或失敗后,塊(block
)會(huì)被調(diào)用柒巫,同時(shí)傳遞網(wǎng)絡(luò)返回的數(shù)據(jù)或操作的結(jié)果励堡。下節(jié)我們會(huì)提及在請(qǐng)求客戶端中定義的回調(diào)塊(block
),它會(huì)把數(shù)據(jù)轉(zhuǎn)存到磁盤緩存中堡掏。
在實(shí)際中应结,這個(gè)網(wǎng)絡(luò)架構(gòu)可以正常工作。當(dāng)一個(gè)請(qǐng)求失敗后泉唁,它會(huì)自動(dòng)重啟鹅龄,所以,我們的應(yīng)用非常健壯亭畜。而不是使用新的iOS7 API
扮休,通過(guò)使用熟知的方法,我們可以快速的把產(chǎn)品推出來(lái)拴鸵。
如果我們必須從頭來(lái)過(guò)玷坠,為了減少代碼量并利用iOS 7的后臺(tái)fetch API
,研究一下NSURLSession
還是很值得的宝踪。我還想探討一下把來(lái)自視圖控制器的命令通過(guò)響應(yīng)者鏈發(fā)送到應(yīng)用委托中的想法侨糟,然后把他們發(fā)送到請(qǐng)求客戶端中。通過(guò)這種方式我們可以完全解耦視圖控制器和請(qǐng)求客戶端瘩燥。
案例研究 2:磁盤上的緩存
Krush
是一個(gè)視覺(jué)化的應(yīng)用秕重,他下載并展示很多圖片。一旦這些圖片從JPEG格式解壓成展示用的位圖厉膀,將會(huì)占用大量的內(nèi)存溶耘。在內(nèi)存中保存應(yīng)用的所有內(nèi)容不是一個(gè)好的選擇,但是在每次展示時(shí)都去下載可能會(huì)占用用戶大量的網(wǎng)絡(luò)資源服鹅。這個(gè)問(wèn)題的解決辦法是使用磁盤緩存凳兵。
對(duì)于可讀性,Brendan
使用他熟悉的SQLite
構(gòu)建了磁盤存儲(chǔ)系統(tǒng)企软。然而庐扫,在我構(gòu)建磁盤緩存的時(shí)候,他正在忙于構(gòu)建網(wǎng)絡(luò)層,而且我的SQLite
功底很弱形庭。所以铅辞,我使用了我熟悉的Core Data
。
Core Data
不是一個(gè)對(duì)象持久化庫(kù)萨醒,而是一個(gè)能數(shù)據(jù)持久化到磁盤存儲(chǔ)器中的對(duì)象圖管理框架斟珊。我們把它當(dāng)做一個(gè)緩存;應(yīng)用的每次啟動(dòng)都會(huì)刪除存儲(chǔ)的數(shù)據(jù)富纸。
應(yīng)用啟動(dòng)時(shí)應(yīng)用最重要的方面之一囤踩。如果應(yīng)用沒(méi)有在一個(gè)合理的時(shí)間內(nèi)啟動(dòng)的話,用戶可能會(huì)放棄這個(gè)應(yīng)用的晓褪。至于Krush
堵漱,我們正在收集來(lái)自用戶的反饋,并且客戶端應(yīng)用程序啟動(dòng)很慢辞州。(⊙o⊙)…
我打開(kāi)Instruments
并在設(shè)備上測(cè)試了一下應(yīng)用的啟動(dòng)時(shí)間怔锌。
哦~,應(yīng)用創(chuàng)建了很多網(wǎng)絡(luò)連接啊变过。在一次跟蹤應(yīng)用第一次啟動(dòng)的時(shí)候埃元,我測(cè)出了170個(gè)網(wǎng)絡(luò)請(qǐng)求。這表明我們提前創(chuàng)建了很多請(qǐng)求而不是按照需要?jiǎng)?chuàng)建媚狰。我增加了按需請(qǐng)求網(wǎng)絡(luò)情況使得網(wǎng)絡(luò)請(qǐng)求不那么樂(lè)觀岛杀,這很容易改變。然而崭孤,這個(gè)改變導(dǎo)致了大量的界面卡頓类嗤。之后,我又測(cè)試了一遍辨宠。
我們發(fā)布的Krush使用了很簡(jiǎn)單的Core Data
緩存遗锣,因?yàn)槲覀儧](méi)有過(guò)多的時(shí)間投入到更復(fù)雜的東西上。堆棧有主線程上的單個(gè)管理對(duì)象上下文組成嗤形。不管怎樣精偿,我從來(lái)都不喜歡過(guò)早的進(jìn)行優(yōu)化;相比與此赋兵,我更喜歡測(cè)試——調(diào)整——測(cè)試這樣的開(kāi)發(fā)節(jié)奏笔咽。當(dāng)我測(cè)試界面卡頓的時(shí)候,我立即就發(fā)現(xiàn)了問(wèn)題:Core Data
在主線程上阻塞了霹期。
我做了些研究后最終決定采取不同的方式叶组。請(qǐng)求客戶端實(shí)例持有一個(gè)在自己隊(duì)列中運(yùn)行的后臺(tái)上下文;后臺(tái)隊(duì)列和主線程隊(duì)列共用一個(gè)持久化存儲(chǔ)協(xié)調(diào)器(Persistent Store Coordinator
)历造。
我們來(lái)看一個(gè)獲取用戶詳情的網(wǎng)絡(luò)請(qǐng)求示例甩十。
用戶對(duì)象已經(jīng)存在于主管理對(duì)象上下文中船庇,但是不需要后臺(tái)上下文。為了保證對(duì)象已經(jīng)存在于持久存儲(chǔ)器中枣氧,我們必須保存主上下文溢十。然后,從用戶信息中獲取對(duì)象id(objectId)达吞,并在網(wǎng)絡(luò)請(qǐng)求的回調(diào)塊(block)中從后臺(tái)上下文獲取相應(yīng)的的用戶對(duì)象。在后臺(tái)線程荒典,我們執(zhí)行JSON解析酪劫,并在后臺(tái)上下文中形成用戶和其他對(duì)象之間的關(guān)系。最后寺董,保存后臺(tái)上下文覆糟,這會(huì)出發(fā)一個(gè)通知去把后臺(tái)上下問(wèn)的改變合并到主上下文中。相應(yīng)的視圖也會(huì)通過(guò)KVO進(jìn)行更新遮咖。呼~滩字。
結(jié)果很令人振奮。我們顯著的改善了啟動(dòng)時(shí)間御吞,并且整個(gè)界面頁(yè)面的極為流暢麦箍。
理論上,我們做的所有改變都應(yīng)該在后臺(tái)管理對(duì)象向下文中進(jìn)行陶珠。如果我必須重做的個(gè)方案的話挟裂,我會(huì)把主管理對(duì)象上下文模型實(shí)例設(shè)置為只讀(語(yǔ)義上的)并且只能在后臺(tái)上下文中執(zhí)行更新。通過(guò)這種方式揍诽,可以避免在我需要在后臺(tái)訪問(wèn)對(duì)象的時(shí)候必須先保存主上下文的麻煩诀蓉。
這個(gè)經(jīng)驗(yàn)教訓(xùn)告訴我們一定要對(duì)應(yīng)用啟動(dòng)進(jìn)行測(cè)試。優(yōu)化界面和啟動(dòng)時(shí)間僅僅只是耗費(fèi)了幾天時(shí)間暑脆。如果我們?cè)侔l(fā)布之前投入了這幾天做優(yōu)化渠啤,我們可能會(huì)在應(yīng)用啟動(dòng)的時(shí)候獲得更加流暢的用戶體驗(yàn),而不是在迭代之后添吗。
案例研究 3:用戶信息視圖
Krush
的用戶信息是一個(gè)復(fù)雜的東西沥曹。不管是從設(shè)計(jì)的角度還是從編碼的角度來(lái)看,正確的處理是很重要的根资。我們看到的設(shè)計(jì)中有三個(gè)tab
:Krushes
架专,Influence
和Network
。
不僅如此玄帕,這些tab
需要進(jìn)行模塊化部脚,因?yàn)閷?duì)于一個(gè)品牌的用戶頁(yè),我們可能會(huì)需要不同的tabs
裤纹。這是一個(gè)很有趣的架構(gòu)問(wèn)題委刘;如何以模塊化的方式重用代碼丧没?
我們可以使用子視圖控制器,但是我更想嘗試一下數(shù)據(jù)驅(qū)動(dòng)锡移。我僅僅只使用了一個(gè)由UITableViewController
控制的table view
呕童。這個(gè)控制器強(qiáng)引用了一個(gè)遵守協(xié)議的datasource
。
當(dāng)選中不同的tab
時(shí)淆珊,數(shù)據(jù)源就會(huì)發(fā)生改變夺饲。此外,當(dāng)數(shù)據(jù)元發(fā)生改變時(shí)施符,table view
也會(huì)重新加載⊥現(xiàn)在,當(dāng)table view
詢問(wèn)控制器應(yīng)該展示什么的時(shí)候戳吝,它則會(huì)去查詢數(shù)據(jù)源浩销。
數(shù)據(jù)源用來(lái)填充我們自己寫的選項(xiàng)卡選擇控件。不同的數(shù)據(jù)源是否可用听哭,取決于新式的用戶是否是一個(gè)品牌慢洋。通過(guò)使用ReactiveCocoa
,我們可以在viewDidLoad
方法中獲取視圖控制器中數(shù)據(jù)源的狀態(tài)陆盘。沒(méi)有把布局方面的考量委托給數(shù)據(jù)源普筹,這使得我們的table view
控制器的邏輯非常簡(jiǎn)單。
每一個(gè)數(shù)據(jù)源竇澤提過(guò)像列表行數(shù)礁遣,行高斑芜,定制個(gè)別行cell
等這樣的信息。每一個(gè)數(shù)據(jù)源還有一個(gè)類屬性和重用標(biāo)識(shí)祟霍,這個(gè)重用標(biāo)識(shí)用來(lái)在viewDidLoad
方法中注冊(cè)自定義的UITableViewCell
子類杏头。最后,每一個(gè)數(shù)據(jù)源也負(fù)責(zé)暴露將觸發(fā)tableview
重載的ReactiveCocoa
信號(hào)沸呐。
當(dāng)設(shè)計(jì)在項(xiàng)目的迭代中發(fā)生改變時(shí)醇王,這種數(shù)據(jù)源方法也很有效。它仍然保持著代碼的簡(jiǎn)潔崭添,解耦寓娩。使用這種方法的一個(gè)缺點(diǎn)是,當(dāng)把Network
選項(xiàng)卡中的各個(gè)設(shè)計(jì)整合到Krushes
選項(xiàng)卡中后呼渣,在不同的數(shù)據(jù)源間共享相同的邏輯就變得不是那么輕松了棘伴。我希望Objective-C在語(yǔ)言層面支持抽象類,因?yàn)檫@樣就可以有助于減少數(shù)據(jù)源對(duì)象之間的重復(fù)代碼屁置。
案例研究 4:Feed模塊中的MVVM
早期發(fā)布的應(yīng)用版本有一個(gè)簡(jiǎn)單地反饋?lái)?yè)面和簡(jiǎn)單地用戶培訓(xùn)教程焊夸。當(dāng)我們把它演示給同事時(shí),大家都認(rèn)為教程部分是最初用戶體驗(yàn)的一個(gè)弱點(diǎn)蓝角。Geoff建議在用戶第一次啟動(dòng)應(yīng)用的時(shí)候整合信息卡片到feed模塊中阱穗,向用戶展示如何使用應(yīng)用饭冬。這樣,他們?cè)诳梢允褂脩?yīng)用之前就不需要記住教程中的用法說(shuō)明揪阶。
在那時(shí)昌抠,feed視圖控制器使用一個(gè)NSFetchedResultsController
去展示存儲(chǔ)在Core Data
中的內(nèi)容。不是把新用戶培訓(xùn)卡邏輯整合到feed視圖控制器中鲁僚,我更想探索一下一個(gè)新出現(xiàn)的Objective-C架構(gòu)模式:Model-View-ViewModel
簡(jiǎn)而言之炊苫,我們把視圖控制器中展示所有內(nèi)容的邏輯抽象到視圖模型(view model
)中,這對(duì)實(shí)際的UI是不可知的蕴茴。視圖模型只是提供Endorse和Save按鈕是否顯示或者用于特定列表單元格的圖片等信息劝评。我們也會(huì)把fetched results controller
的協(xié)議代碼從視圖控制器中移動(dòng)到視圖模型(view model
)中,這些代碼會(huì)把培訓(xùn)模型插入到它所維護(hù)的內(nèi)部數(shù)組中倦淀。
當(dāng)用戶達(dá)到feed的終點(diǎn)獲取跟多數(shù)據(jù)或者當(dāng)用戶下拉刷新的時(shí)候,視圖模型也會(huì)接收到通知声畏。
當(dāng)我們把話題標(biāo)簽整合到應(yīng)用中時(shí)撞叽,這種架構(gòu)很有效。使用相同的視圖控制器僅僅是視圖模型和展示邏輯不同插龄。通過(guò)讓不同的視圖模型遵守視圖控制器可以依賴的相同協(xié)議愿棋,我們能夠讓視圖控制器不需要知道它展示什么東西,以及如何展示的均牢。
很高興這種方法對(duì)我們很有用糠雨。如果必須重做,我會(huì)盡力減少視圖模型之間的重復(fù)代碼徘跪。同樣的甘邀,抽象類也可以在這兒起作用。
結(jié)論
在Teehan+Lax垮庐,這個(gè)項(xiàng)目令我們興奮異常松邪。在整個(gè)項(xiàng)目期間,我們學(xué)到了很多并且還在開(kāi)發(fā)期間獲得了諸多樂(lè)趣哨查。我想通過(guò)分享一些在項(xiàng)目期間的經(jīng)驗(yàn)教訓(xùn)逗抑,開(kāi)發(fā)者可以開(kāi)發(fā)出屬于他們自己的了不起的應(yīng)用。請(qǐng)叫我雷鋒寒亥!