操作抽象設(shè)計(jì)-實(shí)踐

前言

最近在做程序優(yōu)化和代碼總結(jié)的工作,在優(yōu)化和總結(jié)的過程中發(fā)現(xiàn),程序中存在著許多重復(fù)性的交互代碼,特別是在業(yè)務(wù)邏輯層,雖然業(yè)務(wù)模塊本身具有獨(dú)立性,各業(yè)務(wù)模塊之間也有比較明確的分界,但各業(yè)務(wù)模塊內(nèi)部不免還是存在著一些重復(fù)性的交互代碼。

例如,模塊A有一個(gè)收藏功能,模塊B有一個(gè)評(píng)論功能,完成收藏事件與評(píng)論事件都需要確認(rèn)用戶是否登錄,這時(shí)模塊A需要與登錄模塊交互,模塊B也需要與登錄模塊交互,為了確保登錄后可以繼續(xù)完成相應(yīng)的事件,可能還需要保存一些臨時(shí)的數(shù)據(jù)與狀態(tài),這些模塊都需要與登錄模塊交互,且都存在這些交互過程,這就使得各業(yè)務(wù)模塊都存在著一定重復(fù)性的交互代碼,我們需要一種方式來解決這些重復(fù)性交互的問題。


目錄

-操作抽象設(shè)計(jì)

-改善Alert用戶體驗(yàn)

--方式一

--方式二

--方式三

-優(yōu)化啟動(dòng)流程

--main函數(shù)調(diào)用前

--main函數(shù)調(diào)用后

-操作抽象實(shí)現(xiàn)

--操作抽象

--NSOperation的內(nèi)部實(shí)現(xiàn)

--操作管理抽象


Demo


操作抽象設(shè)計(jì)

程序中的各模塊之間,或多或少都會(huì)存在著一些重復(fù)性的交互代碼,邏輯對(duì)象存在重復(fù)的交互過程,如前言中的例子。我們?cè)撛趺唇鉀Q這類問題?以什么方式,什么思路來解決?要回答這些問題,首先我們需要知道哪些交互過程是重復(fù)的,是怎么重復(fù)的,所以我們先來看看例子中的交互過程是怎樣的。

由于不同程序的構(gòu)架不同,不同團(tuán)隊(duì)的處理方式也不同,所以我們忽略這個(gè)交互過程是否合理。觀察兩個(gè)交互過程我們會(huì)發(fā)現(xiàn),其中有一部分交互是完全相同的。

相同的交互過程存在于兩個(gè)不同的獨(dú)立模塊中,面對(duì)這種情況,我們可以選擇擴(kuò)展現(xiàn)有的抽象,把相同的交互過程封裝到擴(kuò)展的抽象中,例如擴(kuò)展用戶抽象(User),把相同的交互過程封裝到用戶抽象中,那么整個(gè)交互過程就改變了。

經(jīng)過這樣的處理,問題好像得到了解決,把相同的交互過程封裝到用戶抽象后,模塊A或模塊B完成相應(yīng)事件的交互過程就被簡(jiǎn)化了,所需要交互的對(duì)象也減少了。但程序的架構(gòu)或處理方式會(huì)隨著程序的迭代而作出一些調(diào)整,交互過程也可能需要作出相應(yīng)的調(diào)整,而直接依賴具體模塊或具體組件會(huì)使得程序難以維護(hù)和擴(kuò)展。例如隨著程序的迭代,需要把原本封裝在用戶抽象中的相同交互過程,調(diào)整為封裝在登錄控制器抽象中,那么接下來的工作就是把原本依賴于用戶抽象的模塊都改為依賴登錄控制器抽象。

這樣的依賴關(guān)系,別說擴(kuò)展了,連維護(hù)都是個(gè)大問題,所以在日常開發(fā)當(dāng)中,我們都會(huì)盡可能地避免這種情況的發(fā)生。如何避免直接的依賴關(guān)系,這是解耦的問題,通常我們可以選擇在具體模塊或組件之上加一層抽象來避免這種直接的依賴關(guān)系,我們還可以選擇加入中間件,讓各模塊或組件通過中間件來交互。用中間件來交互的例子有很多,例如在mach內(nèi)核中,各任務(wù)通過端口傳遞mach報(bào)文的方式來交互。

通過與中間件交互,而不是直接與具體模塊交互,雖然可以解決直接依賴具體模塊的耦合問題,但這僅僅只是解決了模塊之間的耦合問題,而并沒有減少模塊所需要處理的交互。

通過前后對(duì)比就會(huì)發(fā)現(xiàn),模塊A的交互過程并沒有發(fā)生變化,仍然是先開始登錄操作,收藏操作在登錄操作完成后才開始,而模塊A為了能夠正確地開始收藏操作,還可能需要保存一些收藏操作所需的臨時(shí)數(shù)據(jù)和狀態(tài)。解決耦合問題并不是我們的目標(biāo),我們所期望的是既可以解決耦合問題,又可以減少交互,還要盡可能地避免重復(fù)的交互過程,所以單純地加入中間件并不能滿足我們的需求。我們需要設(shè)計(jì)一種新的方式,所以先拋開過往的一切,拋開那些慣性的思維,回到最初的起點(diǎn),重新來分析問題。

我們先把視角切換成面向過程,模塊A需要完成收藏事件,所以先對(duì)收藏事件做算法分解,收藏事件的整個(gè)處理過程由登錄和收藏兩個(gè)子過程組成。

算法分解完成后,現(xiàn)在我們把視角切換成面向?qū)ο蟆R赃^往的處理方式,我們會(huì)把登錄和收藏這兩個(gè)子過程抽象成對(duì)象的行為,再由模塊A與相關(guān)的對(duì)象來完成事件,但正如前面所看到的,模塊A需要順序地和相關(guān)對(duì)象進(jìn)行交互,也就是說模塊A需要處理整個(gè)交互過程,而我們的目標(biāo)是減少交互和避免重復(fù)的交互過程,所以這種方式不符合我們的需求。這里需要明確提出的一點(diǎn)是,這種處理方式本身并沒有什么問題,我們所討論的并不是這種處理方式的對(duì)與錯(cuò),所以你完全可以基于這種方式來開發(fā)。

既然過往的抽象方式不能達(dá)到目的,所以我們需要嘗試其它的抽象方式。不管是減少交互,還是避免重復(fù)的交互過程,實(shí)際上都是在改變模塊A(客戶)的交互,既然要改變模塊A的交互,首先要知道模塊A目前處理事件的關(guān)注點(diǎn)是什么。在過往的處理方式中,模塊A的關(guān)注點(diǎn)在于完成事件需要哪些模塊參與,怎么和這些模塊交互,也就是模塊A需要處理整個(gè)交互過程。既然交互和關(guān)注點(diǎn)有關(guān),那么我們是否能從改變關(guān)注點(diǎn)入手來改變交互過程呢?該怎么改變關(guān)注點(diǎn)呢?

怎么改變關(guān)注點(diǎn)?不妨把程序問題轉(zhuǎn)換成生活問題,把模塊A的處理方式轉(zhuǎn)換成實(shí)際情況來思考。假設(shè)有一個(gè)技術(shù)團(tuán)隊(duì),模塊A是這個(gè)團(tuán)隊(duì)的負(fù)責(zé)人,其它各模塊是團(tuán)隊(duì)中的開發(fā)人員,收藏事件是一次迭代開發(fā),現(xiàn)在模塊A接收到完成這次迭代開發(fā)的命令,所以開始組織開發(fā)人員參與開發(fā),在開發(fā)過程中,模塊A需要記錄登錄任務(wù)的開發(fā)狀態(tài),等待相應(yīng)的人員完成開發(fā),當(dāng)?shù)卿浫蝿?wù)完成后,模塊A需要告訴收藏任務(wù)的相應(yīng)人員開始開發(fā),然后記錄收藏任務(wù)的開發(fā)狀態(tài),當(dāng)收藏任務(wù)開發(fā)完成時(shí),模塊A把這次迭代開發(fā)標(biāo)記為已完成。

假如你是模塊A,你有什么感覺?如果我是模塊A,我感覺腰腿酸痛、精神不振,好像身體被掏空了(把腎虛說得這么理直氣壯??)!完成一次迭代開發(fā),我需要把相應(yīng)任務(wù)分配給相應(yīng)的開發(fā)人員,需要管理每一個(gè)開發(fā)任務(wù),需要記錄每一個(gè)任務(wù)的狀態(tài),還要通知和組織相應(yīng)的開發(fā)人員完成開發(fā)。額!好吧,這些好像確實(shí)是負(fù)責(zé)人應(yīng)該做的事,但我想偷懶,我希望有一個(gè)系統(tǒng)可以幫助我管理開發(fā)過程,我需要做的只是創(chuàng)建任務(wù),設(shè)置任務(wù)之間的依賴關(guān)系,再把任務(wù)輸入到系統(tǒng)即可,我不再需要去關(guān)注這個(gè)任務(wù)怎么完成,什么時(shí)候完成,什么時(shí)候下一個(gè)開始任務(wù)。

既然有了方向,我們把問題轉(zhuǎn)換回程序問題,如果程序中存在這么一個(gè)系統(tǒng),這個(gè)系統(tǒng)管理著一系列的操作,那么對(duì)客戶而言,需要做的只是創(chuàng)建操作,設(shè)置操作之間的依賴關(guān)系,把操作輸入系統(tǒng)即可,客戶不再需要去關(guān)注這個(gè)操作怎么完成,什么時(shí)候完成,什么時(shí)候開始下一個(gè)操作。那么這時(shí)模塊A的關(guān)注點(diǎn)就被改變了,模塊A不再關(guān)注完成事件需要哪些模塊參與,怎么和這些模塊交互,而是關(guān)注完成事件需要哪些操作,這些操作之間有什么依賴關(guān)系,但這些操作背后的一切對(duì)模塊A來說都是透明的,也就是操作被自動(dòng)化管理了。

有需求才有產(chǎn)出,需求提供了方向。我們的需求是建立一個(gè)系統(tǒng)(組件或模塊),這個(gè)系統(tǒng)用于管理一系列的操作,管理操作之間的關(guān)系,觀察操作的狀態(tài),啟動(dòng)操作,也就是我們要建立一個(gè)自動(dòng)化操作的管理系統(tǒng)。

有了需求之后,我們要怎么著手搭建這個(gè)系統(tǒng)呢?應(yīng)該怎么開始呢?最基本的,先分析需求,從需求中提取關(guān)鍵抽象,怎么提取關(guān)鍵抽象?從需求的關(guān)鍵詞入手,當(dāng)前需求中的關(guān)鍵詞是操作和操作管理,所以當(dāng)前的關(guān)鍵抽象就是操作和操作管理,接下來需要做的是確定這兩個(gè)抽象的基礎(chǔ)外部視圖,什么是外部視圖?也就是這個(gè)抽象的職責(zé),屬性和行為。

我們的需求是要建立一個(gè)復(fù)雜的自動(dòng)化操作的管理系統(tǒng),但為什么得到的基礎(chǔ)外部視圖的內(nèi)容卻如此簡(jiǎn)單?這里需要說明一下,在前期分析和設(shè)計(jì)階段,我們關(guān)注的是抽象與抽象之間的關(guān)系,所以只需要基礎(chǔ)外部視圖就已經(jīng)夠了,即使到了后期的階段,也只需要一個(gè)相對(duì)完整的外部視圖就,像抽象的內(nèi)部視圖(結(jié)構(gòu),具體實(shí)現(xiàn)等)是在實(shí)現(xiàn)階段確定的,現(xiàn)階段不需要也不應(yīng)該去關(guān)注內(nèi)部視圖。

回到我們的最初目標(biāo),減少模塊A的交互,避免重復(fù)的交互過程。有了上面兩個(gè)抽象的基礎(chǔ)外部視圖后,現(xiàn)在把登錄和收藏這兩個(gè)子過程抽象成兩個(gè)操作對(duì)象,模塊A設(shè)置這兩個(gè)操作對(duì)象的依賴關(guān)系,傳到操作管理對(duì)象即可,模塊A不再需要關(guān)注有哪些模塊參與,怎么和這些模塊交互了。

那模塊B處理評(píng)論事件又變成怎樣了?

當(dāng)交互過程抽象成操作對(duì)象后,模塊A和模塊B在交互中的if else也被封裝到操作對(duì)象中了。例如,模塊A在處理收藏事件時(shí)需要判斷用戶是否已判斷,用戶已登錄則直接開始處理收藏,若用戶未登錄則先進(jìn)行用戶登錄的處理,但現(xiàn)在這個(gè)判斷的過程及處理對(duì)模塊A已經(jīng)是透明的了,OperationLogin對(duì)象會(huì)根據(jù)用戶狀態(tài)來處理這一個(gè)過理,當(dāng)用戶已登錄,OperationLogin對(duì)象會(huì)直接標(biāo)記為已完成,這時(shí)下一個(gè)操作對(duì)象就會(huì)開始完成其相應(yīng)的事件,模塊A和模塊B都不需要再去處理這些判斷。

改善Alert用戶體驗(yàn)

App通常會(huì)以Alert的形式來提示你是否允許某些操作,例如App需要訪問你的相冊(cè)時(shí),會(huì)彈出一個(gè)Alert提示你是否允許訪問。但很多時(shí)候在我們還沒操作或看清楚本次Alert時(shí),下一個(gè)Alert又彈出了,甚至?xí)蝗粡棾鰩讉€(gè)Alert,就像這樣。

雖然很多時(shí)候確實(shí)需要彈出多個(gè)Alert來提示用戶,但這樣的用戶體驗(yàn)會(huì)很糟糕。這種用戶體驗(yàn)更多地是跟產(chǎn)品設(shè)計(jì)相關(guān)的,但我們還是應(yīng)該在程序中盡可能地去改善,至少讓Alert彈的方式變得優(yōu)雅一些。

接連彈出Alert,等待用戶處理完一個(gè)Alert再?gòu)棾鱿乱粋€(gè)Alert。我們要怎么實(shí)現(xiàn)呢?

方式一:

一種實(shí)現(xiàn)方式是提供一個(gè)AlertManager抽象,然后通過hook UIAlertView的show方法,把當(dāng)前需要顯示的AlertView添加到AlertManager,再由AlertManager做串行顯示的管理。AlertManager的職責(zé)是管理一個(gè)個(gè)AlertView,通過一個(gè)串行隊(duì)列來管理這些AlertView的顯示。

這種實(shí)現(xiàn)方式的優(yōu)點(diǎn)在于,不需要改動(dòng)程序原有的代碼即可完成Alert事件的管理,但缺點(diǎn)也很明顯,那就是無法應(yīng)對(duì)API的更新,在iOS8之后,UIAlertView已經(jīng)被UIAlertController所取代了。如果程序當(dāng)中出現(xiàn)了一個(gè)模塊用UIAlertView,另一個(gè)模塊用UIAlertController的情況,那只能又去hook?UIViewController的presentViewController方法了,而且AlertManager還需要監(jiān)聽AlertView的狀態(tài),當(dāng)AlertView被用戶處理后,把隊(duì)列中的下一個(gè)AlertView顯示出來。

方式二:

提供一個(gè)AlertManager抽象,向AlertManager傳遞一個(gè)Alert事件所需的信息,由AlertManager創(chuàng)建與信息相應(yīng)的AlertTask,通過一個(gè)串行隊(duì)列來管理這些AlertTask,在AlertTask出隊(duì)列時(shí),創(chuàng)建AlertView并把信息顯示出來。

這種實(shí)現(xiàn)方式的優(yōu)點(diǎn)是,客戶只需要關(guān)注Alert的信息,不需要關(guān)注Alert的信息是怎么顯示的,同時(shí)也解決了API更新的問題,缺點(diǎn)是AlertManager的管理機(jī)制只限于Alert事件,其它類型的事件也想實(shí)現(xiàn)一個(gè)接一個(gè)的方式,只能再提供一個(gè)不同類型但管理機(jī)制相同的抽象了,這樣程序中會(huì)存在很多機(jī)制相同的抽象,而且會(huì)增加程序的復(fù)雜度,增加學(xué)習(xí)成本。跟方式一的AlertManager一樣,需要監(jiān)聽AlertView的狀態(tài)。

方式三:

把Alert事件抽象成Alert操作對(duì)象,設(shè)置Alert操作對(duì)象之間的依賴關(guān)系,由于先前我們已經(jīng)完成了一個(gè)自動(dòng)化操作的管理系統(tǒng),所以只需要把Alert操作對(duì)象輸入管理系統(tǒng)即可。

這種實(shí)現(xiàn)方式的優(yōu)點(diǎn)是,客戶只需要傳遞Alert的信息給Alert操作對(duì)象,不需要關(guān)注Alert的信息是怎么顯示的,同時(shí)也解決了API更新的問題,還可以通過設(shè)置Alert操作對(duì)象之間的依賴關(guān)系來控制Alert出現(xiàn)的順序,也避免了程序中存在相同機(jī)制的情況,缺點(diǎn)是會(huì)增加程序的復(fù)雜度,增加學(xué)習(xí)成本。

這里還有一個(gè)問題,Alert可能來自不同的模塊,所以我們需要擴(kuò)展原有的實(shí)現(xiàn),使得管理系統(tǒng)可以控制某一類操作對(duì)象,讓這些操作對(duì)象可以一個(gè)接一個(gè)地執(zhí)行,具體的實(shí)現(xiàn)請(qǐng)看Demo和下面實(shí)現(xiàn)部份的講述。

優(yōu)化啟動(dòng)流程

main函數(shù)調(diào)用前

iOS程序啟動(dòng)通常被分為main函數(shù)調(diào)用前后的兩個(gè)過程,main函數(shù)調(diào)用前的過程大致是這樣的。

從創(chuàng)建進(jìn)程到設(shè)置線程入口屬于kernel內(nèi)核范疇的工作,而加載依賴共享庫(kù)到恢復(fù)App入口屬于動(dòng)態(tài)鏈接器dyld范疇的工作。main函數(shù)調(diào)用前的優(yōu)化重點(diǎn)在于減少操作,例如減少程序中的類和分類,減少資源文件的加載,合并load方法,合并動(dòng)態(tài)庫(kù)等,減少這些操作從而減少前半部份所需要的時(shí)間。網(wǎng)上已經(jīng)有大量關(guān)于前半部份優(yōu)化的文章,所以這里不再提及(說得好像自己會(huì)一樣??)。

main函數(shù)調(diào)用后

main函數(shù)調(diào)用后的優(yōu)化分為效率和流程兩個(gè)方面,這一節(jié)主要討論怎么優(yōu)化啟動(dòng)流程。關(guān)于效率方面的優(yōu)化,網(wǎng)上也有一些文章講解,我感覺好像沒什么可以講的,畢竟每個(gè)項(xiàng)目的情況都不同,只可能提供一個(gè)大方向,各自根據(jù)實(shí)際情況來優(yōu)化了,但會(huì)在討論流程優(yōu)化的時(shí)候穿插一些效率優(yōu)化需要注意的點(diǎn)。

main函數(shù)調(diào)用后的主要工作是加載程序的內(nèi)容,所需要處理的任務(wù)可能有很多,而且每個(gè)程序的情況都有所不同,所以這里只分析一個(gè)簡(jiǎn)單的例子。

程序啟動(dòng)時(shí)可能需要處理二三十個(gè)任務(wù)甚至更多,如果main函數(shù)調(diào)用后在App的主線程一次性處理所有的啟動(dòng)任務(wù),那啟動(dòng)所需要的時(shí)間可能會(huì)很長(zhǎng),用戶需要等待很長(zhǎng)的時(shí)間才能看到App的內(nèi)容。在討論怎么優(yōu)化之前,首先要知道當(dāng)前的關(guān)鍵問題是什么,所以先來了解一下App啟動(dòng)時(shí),主線程的執(zhí)行過程是怎樣的。

站在用戶的角度來看,使用App時(shí)最直接的交互就是可視化元素,所以App啟動(dòng)時(shí),用戶越快能看到App的可視化元素體驗(yàn)就越好。App的可視化元素要顯示在屏幕上,則依賴渲染進(jìn)程來渲染可視化元素,而渲染進(jìn)程只有在接收到渲染事務(wù)后,才會(huì)把事務(wù)中的可視化元素渲染到屏幕上,所以App的可視化元素能否快速地顯示在屏幕上,這取決于App能否及時(shí)地把渲染事務(wù)發(fā)送到渲染進(jìn)程。

由上圖我們可以得知,App能否及時(shí)地把渲染事務(wù)發(fā)送到渲染進(jìn)程取決于處理啟動(dòng)任務(wù)所需要的時(shí)間,即啟動(dòng)任務(wù)的處理時(shí)間越長(zhǎng),用戶看到App的可視化元素所需要的時(shí)間就越長(zhǎng)。知道問題所在之后,我們就可以來討論優(yōu)化的方案了。

既然我們已經(jīng)知道問題在于App的主線程壓力過大,所以最簡(jiǎn)單的處理就是把不需要在主線程處理的任務(wù)放到輔助線程中處理,但這并不能保證主線程的壓力會(huì)大大減少,因?yàn)槿匀挥幸徊糠萑蝿?wù)只能在主線程中處理的,像UI任務(wù),所以我們還需要?jiǎng)e的優(yōu)化方案來解決這些只能在主線程中處理的啟動(dòng)任務(wù)。為了更容易討論,現(xiàn)在我們假設(shè)例子中所有的啟動(dòng)任務(wù)都只能在主線程中處理。

對(duì)于這些只能在主線程中處理的啟動(dòng)任務(wù),通常的解決方法是把這些啟動(dòng)任務(wù)分優(yōu)化級(jí)處理,在main函數(shù)調(diào)用后只處理最高優(yōu)化級(jí)的任務(wù),也就是程序啟動(dòng)所必需的任務(wù),其它優(yōu)先級(jí)的任務(wù)延遲處理。

在高優(yōu)先級(jí)的任務(wù)中,第一個(gè)被處理的任務(wù)一定是啟動(dòng)信息收集系統(tǒng),原因也很簡(jiǎn)單,因?yàn)樵谔幚砗罄m(xù)的任務(wù)時(shí)可能會(huì)出錯(cuò),信息收集系統(tǒng)可以第一時(shí)間把這些錯(cuò)誤信息收集起來。加載啟動(dòng)頁(yè)基本上就是最后一個(gè)任務(wù)了,因?yàn)閱?dòng)頁(yè)的內(nèi)容通常是緩存在本地或者直接通過網(wǎng)絡(luò)加載出來的。需要注意的是,我們應(yīng)該盡可能在加載啟動(dòng)頁(yè)前只處理跟啟動(dòng)頁(yè)相關(guān)的任務(wù),把其它啟動(dòng)任務(wù)放到加載啟動(dòng)頁(yè)之后執(zhí)行,讓用戶盡可能快地看到啟動(dòng)頁(yè),只要啟動(dòng)頁(yè)成功加載到屏幕上,程序就會(huì)有1秒以上的時(shí)間去處理其它的任務(wù)。還需要注意的是,如果啟動(dòng)頁(yè)中存在動(dòng)畫效果,應(yīng)該盡可能地選擇Core Animation作為動(dòng)畫引擎,而不是POP等動(dòng)畫引擎,想了解兩者的差異可以看我之前寫的《iOS動(dòng)畫的基礎(chǔ)知識(shí)》

加載內(nèi)容頁(yè)的任務(wù)并沒有被劃分為高優(yōu)先級(jí),主要是因?yàn)锳pp的內(nèi)容頁(yè)通常都有大量的視圖,創(chuàng)建這些視圖需要消耗大量的時(shí)間,而且還需要同步地處理這些視圖的布局,顯示等,而這些都是遞歸操作,所以加載內(nèi)容頁(yè)的時(shí)間算起來就非??捎^了。把啟動(dòng)任務(wù)劃分為不同的優(yōu)先級(jí)來處理后,App主線程的執(zhí)行過程又是怎樣?

這種優(yōu)化方式跟頁(yè)面性能優(yōu)化的原理類似,把一個(gè)大事務(wù)分割成若干個(gè)小事務(wù),再把這些小事務(wù)分配到不同的Loop中處理。把啟動(dòng)任務(wù)劃分為不同的優(yōu)先級(jí)僅僅只是第一步,還需要一個(gè)類似于操作系統(tǒng)中調(diào)度器的管理系統(tǒng)來管理和調(diào)度這些不同優(yōu)先級(jí)的啟動(dòng)任務(wù)。

實(shí)際上即使沒有這個(gè)管理系統(tǒng)也可以很容易地實(shí)現(xiàn),啟動(dòng)任務(wù)按優(yōu)先級(jí)分配到不同的Loop中處理。

只需要簡(jiǎn)單地劃分一下啟動(dòng)任務(wù),把需要延遲處理的啟動(dòng)任務(wù)dispatch回MainQueue即可。

把需要延遲處理的啟動(dòng)任務(wù)取出,同時(shí)向主線程發(fā)送消息,當(dāng)主線程處理完高優(yōu)先級(jí)的啟動(dòng)任務(wù)后,去檢測(cè)消息列隊(duì)中是否有消息要處理,檢測(cè)到待處理消息后,開始處理延遲的啟動(dòng)任務(wù)。所以就目前的需求而言,啟動(dòng)任務(wù)的管理系統(tǒng)可以以很簡(jiǎn)單的方式便可實(shí)現(xiàn)。

在前面的分析中,所有的啟動(dòng)任務(wù)都是在主線程中處理的,但無論何時(shí),我們都希望App可以快速地響應(yīng)用戶的操作,所以一般情況下都會(huì)盡可能地減少主線程的工作量,使主線程可以快速地處理交互的消息。實(shí)際上我們往往會(huì)把那些不需要在主線程中處理的啟動(dòng)任務(wù)分配到輔助線程中處理,但在多線程環(huán)境下完成App的啟動(dòng),就遠(yuǎn)比前面分析的過程復(fù)雜了。

為了支持多線程環(huán)境,管理系統(tǒng)的內(nèi)部實(shí)現(xiàn)至少會(huì)面臨這么幾個(gè)問題。管理系統(tǒng)的調(diào)度算法是怎樣的,啟動(dòng)任務(wù)的處理順序是否由優(yōu)先級(jí)決定?如果不是由優(yōu)先級(jí)決定,那么怎么保證任務(wù)可以正常地處理?例如,更新用戶信息的任務(wù)比配置網(wǎng)絡(luò)環(huán)境的任務(wù)先處理,由于當(dāng)前還沒有配置好網(wǎng)絡(luò)環(huán)境,更新用戶信息的任務(wù)就無法正確處理了。如果是由優(yōu)先級(jí)決定,那么整個(gè)處理過程的效率就比較低了,因?yàn)榈蛢?yōu)先級(jí)的啟動(dòng)任務(wù)需要等高優(yōu)先級(jí)的啟動(dòng)任務(wù)處理完畢業(yè)才開始處理。

如果整個(gè)處理過程是串行的,那么這樣的多線程處理就沒有什么意義了。按優(yōu)先級(jí)處理啟動(dòng)任務(wù)似乎是現(xiàn)在比較普遍的處理方式,一些開發(fā)書像《高性能iOS應(yīng)用開發(fā)》也是這么介紹的。不知道應(yīng)用這種方式的團(tuán)隊(duì)是怎么解決這些問題的,調(diào)度算法是否經(jīng)常要改。

我們嘗試換一種方式來優(yōu)化main函數(shù)調(diào)用后的流程。觀察例子中的啟動(dòng)任務(wù)就會(huì)發(fā)現(xiàn),這些啟動(dòng)任務(wù)之間存在著一定的關(guān)聯(lián),那么如果把這些啟動(dòng)任務(wù)抽象成相應(yīng)的操作對(duì)象,再設(shè)置這些操作對(duì)象之間的依賴關(guān)系會(huì)怎樣?

設(shè)置了操作對(duì)象之間的依賴關(guān)系之后可以看到,整個(gè)啟動(dòng)流程的架構(gòu)跟我們所期望的,或者在草稿本上畫的相差無幾,啟動(dòng)任務(wù)一個(gè)接一個(gè)地處理,不需要再去劃分這些啟動(dòng)任務(wù)的優(yōu)先級(jí)了。

前后兩種優(yōu)化方式的某些關(guān)注點(diǎn)是一樣的,都關(guān)注啟動(dòng)時(shí)有哪些任務(wù),都把原本屬于AppDelegate的交互抽象出來。但不同的是,現(xiàn)在我們不再需要去關(guān)注啟動(dòng)任務(wù)的優(yōu)先級(jí),或者說優(yōu)先級(jí)的概念已經(jīng)被淡化了,也沒有了劃分優(yōu)先級(jí)處理后所需要考慮的問題,不再需要關(guān)注多線程環(huán)境下怎么保證啟動(dòng)任務(wù)的處理順序了,因?yàn)楫?dāng)操作對(duì)象所依賴的其它對(duì)象處理完畢后,它會(huì)自動(dòng)開始處理自己的事件,例如LaunchCache操作對(duì)象和Networkconfig操作對(duì)象依賴于Collection操作對(duì)象,當(dāng)Collection操作對(duì)象處理完事件后,LaunchCache操作對(duì)象和Networkconfig操作對(duì)象就會(huì)自動(dòng)開始處理任務(wù)。我們需要關(guān)注的只是當(dāng)前有多少個(gè)操作,這些操作之間有什么依賴關(guān)系,哪些操作可以在多線程環(huán)境下處理即可,操作自動(dòng)化運(yùn)行。

從執(zhí)行效率來看,在確定啟動(dòng)流程的架構(gòu)之后,可以盡可能地增加處理啟動(dòng)任務(wù)的并發(fā)數(shù),所以CPU使用率的提升還是比較可觀的。

從開發(fā)效率來看,不需要去管理啟動(dòng)任務(wù)的優(yōu)先級(jí),不需要去考慮和優(yōu)化調(diào)度啟動(dòng)任務(wù)的算法,只需要設(shè)置啟動(dòng)任務(wù)之間的關(guān)系即可,減少的工作量也比較可觀。

從后期維護(hù)來看,當(dāng)啟動(dòng)任務(wù)增加,減少或需要重新調(diào)整啟動(dòng)任務(wù)之間的關(guān)系時(shí),我們只需要去調(diào)整啟動(dòng)任務(wù)之間的依賴關(guān)系即可,不需要去考慮和優(yōu)化調(diào)度算法,提高了穩(wěn)定性。

從測(cè)試來看,減少了測(cè)試項(xiàng),簡(jiǎn)化了測(cè)試項(xiàng),因?yàn)椴恍枰y(cè)試調(diào)度算法是否高效了,只需要把注意力放在操作對(duì)象的實(shí)現(xiàn)上即可。

操作抽象實(shí)現(xiàn)

操作抽象

我們要怎么實(shí)現(xiàn)前面所提到的操作抽象呢?實(shí)際上Cocoa中已經(jīng)存在這種帶有狀態(tài)的操作抽象了,NSOperation??吹竭@里你可能想說,早說NSOperation不就行了,扯這么多干嘛。前面的分析與設(shè)計(jì)是不針對(duì)任何具體實(shí)現(xiàn)的,純粹用抽象概念來討論的,現(xiàn)在到了實(shí)現(xiàn)階段,才去考慮前面整個(gè)的設(shè)計(jì)要怎么實(shí)現(xiàn),用什么來實(shí)現(xiàn)的。分析設(shè)計(jì)和實(shí)現(xiàn)這兩個(gè)過程是不能反過來的,也就是不能因?yàn)橄胗肗SOperation再去設(shè)計(jì)使用場(chǎng)景。例如,你要吃面,所以你需要一雙筷子,而不是你有一雙筷子,所以你要吃面。

在考慮具體實(shí)現(xiàn)時(shí),我們會(huì)找現(xiàn)有的框架或過往的實(shí)現(xiàn)中是否存在相同的對(duì)象模型,如果有現(xiàn)成的對(duì)象模型,那么會(huì)優(yōu)先選擇用現(xiàn)成的對(duì)象模型而不是做一個(gè)新的對(duì)象模型,這并不是為了偷懶,理由很簡(jiǎn)單,因?yàn)楝F(xiàn)成的對(duì)象模型是經(jīng)過實(shí)踐和測(cè)試的,是相對(duì)穩(wěn)定的,沒有一個(gè)對(duì)象模型是一創(chuàng)造出來就可以穩(wěn)定應(yīng)對(duì)所有場(chǎng)景的,都要經(jīng)過長(zhǎng)期地應(yīng)用,維護(hù)和擴(kuò)展才會(huì)變得相對(duì)穩(wěn)定的,所以優(yōu)先選擇現(xiàn)在的對(duì)象模型,如果沒有現(xiàn)成的對(duì)象模型才會(huì)去創(chuàng)造一個(gè)。

NSOperation的內(nèi)部實(shí)現(xiàn)

看到NSOperation和NSOperationQueue,可能很多人的第一反應(yīng)是多線程,NSOperation與NSOperationQueue結(jié)合實(shí)現(xiàn)了多線程技術(shù),但多線程技術(shù)僅僅只是它們的一部份,而且在我看來,多線程技術(shù)并不是NSOperation和NSOperationQueue的核心。在iOS4以后,NSOperation和NSOperationQueue的多線程是用GCD來實(shí)現(xiàn)的,也就是NSOperation與NSOperationQueue作為GCD的面向?qū)ο蟮某橄笪锒嬖凇5绻阒皇窍胍悦嫦驅(qū)ο蟮姆绞絹硎褂肎CD,你自己實(shí)現(xiàn)Operation和OperationQueue的抽象,也不過四五百行代碼,而且效率要比Cocoa自帶的NSOperation和NSOperationQueue高,首先來了解一下NSOperation的內(nèi)部實(shí)現(xiàn)。

顧名思義,NSOperationInternal是NSOperation的內(nèi)部抽象,向NSOperation對(duì)象發(fā)送的消息最終都會(huì)轉(zhuǎn)發(fā)給內(nèi)部的NSOperationInternal對(duì)象,NSOperationInternal對(duì)象完成具體的工作,也就是說我們平常所用的NSOperation實(shí)際上只是一個(gè)代理,此代理(Proxy)非彼代理(Delegate)。NSOperationInternal的內(nèi)部結(jié)構(gòu)大致是怎樣的?

NSOperationInternal內(nèi)部結(jié)構(gòu)中的queue是NSOperation被添加的queue,operation是外部的NSOperation對(duì)象,dependencies是NSOperation的關(guān)聯(lián)對(duì)象,down_dependencies也就是NSOperation的反關(guān)聯(lián)對(duì)象了,state是NSOperationInternal對(duì)象目前處理事件的狀態(tài),這跟NSOperation不同,NSOperation是用幾個(gè)BOOL值(isFinished,isExecuting,isReady)來表示事件當(dāng)前狀態(tài)的。

NSOperation被添加到NSOperationQueue的過程是怎樣的?

當(dāng)NSOperation被添加到相應(yīng)的NSOperationQueue時(shí),NSOperation的NSOperationInternal就會(huì)把queue設(shè)置為當(dāng)前的NSOperationQueue。

設(shè)置NSOperation對(duì)象之間的依賴關(guān)系的過程又是怎樣的?

當(dāng)設(shè)置NSOperation1與NSOperation2的依賴關(guān)系后,NSOperation2被添加到了NSOperationInternal1的dependencies中,NSOperationInternal1被添加到了NSOperationInternal2的down_dependencies中,然后NSOperationInternal1設(shè)置NSOperation1的isReady狀態(tài)為NO。

NSOperationInternal除了完成NSOperation的具體工作外,它還有一個(gè)職責(zé)就是監(jiān)聽NSOperation的狀態(tài)(isFinished,isExecuting,isReady)。當(dāng)NSOperation的狀態(tài)改變時(shí),NSOperationInternal會(huì)作出不同的響應(yīng)。當(dāng)NSOperation2處理完事件后,NSOperation1是如何得知且開始處理自己的事件?

由于NSOperation被添加到NSOperationQueue時(shí),NSOperation的NSOperationInternal會(huì)把queue設(shè)置為當(dāng)前的NSOperationQueue,所以NSOperation的isReady狀態(tài)從NO變成YES,即表示NSOperation可以處理事件,NSOperationInternal監(jiān)聽到NSOperation狀態(tài)變化時(shí),NSOperationInternal會(huì)向queue發(fā)送啟動(dòng)相應(yīng)的NSOperation的消息。這就是NSOperation可以自動(dòng)化的原因,要注意的是,NSOperation要跟NSOperationQueue結(jié)合才能實(shí)現(xiàn)自動(dòng)化,如果只是設(shè)置了NSOperation之間的依賴關(guān)系,但沒有把NSOperation添加到NSOperationQueue中,NSOperation是不會(huì)自動(dòng)化的。

NSOperationInternal通過監(jiān)聽NSOperation的狀態(tài)來完成相應(yīng)的操作,所以當(dāng)我們自定義NSOperation的子類時(shí)需要手動(dòng)管理NSOperation的isFinished,isExecuting狀態(tài)。當(dāng)我們自定義NSOperation的子類時(shí),要么實(shí)現(xiàn)start方法,要么實(shí)現(xiàn)main方法,如果兩個(gè)方法同時(shí)實(shí)現(xiàn),那么只有start方法會(huì)被調(diào)用。

為了實(shí)現(xiàn)NSOperation的自動(dòng)化,Cocoa為NSOperation增加了NSOperationInternal,讓NSOperationInternal在背后處理一切事務(wù)。如果NSOperation只用于多線程技術(shù),那么它的狀態(tài)機(jī)制就沒有太多意義了。所以當(dāng)你只是想以面向?qū)ο蟮姆绞絹硎褂肎CD,自己實(shí)現(xiàn)的效率會(huì)更高。

操作管理抽象

我們可以用NSOperation來實(shí)現(xiàn)操作抽象(Operation),那么操作管理抽象(OperationManager)又怎么來實(shí)現(xiàn)呢?實(shí)現(xiàn)操作管理抽象需要考慮哪些問題?

實(shí)現(xiàn)操作管理抽象至少要考慮以下問題,是否為每一類操作抽象都創(chuàng)建一個(gè)相應(yīng)的操作管理抽象?如果不為每一類操作抽象配置一個(gè)相應(yīng)的操作管理抽象,那用什么來讓每一類操作抽象保持獨(dú)立性?NSOperation需要跟NSOperationQueue結(jié)合使用才能實(shí)現(xiàn)自動(dòng)化,是否由操作管理抽象來創(chuàng)建和管理NSOperationQueue?不同類型的操作抽象是否可以共用同一個(gè)NSOperationQueue?是否存在全局的NSOperationQueue?

操作抽象有什么獨(dú)立性?類似Alert操作抽象需要一個(gè)接一個(gè)地處理,如果共用同一個(gè)操作管理抽象,那么這個(gè)操作管理抽象的實(shí)現(xiàn)就會(huì)存在許多的判斷語(yǔ)句來區(qū)分不同類型的操作抽象,解決判斷語(yǔ)句多的問題,可以選擇為不同類型的操作抽象創(chuàng)建一個(gè)相應(yīng)的配置抽象,通過配置抽象來提供獨(dú)立性,但這樣會(huì)使得操作管理抽象的實(shí)現(xiàn)越來越復(fù)雜,需要的抽象也越來越多,這對(duì)于以后的維護(hù)和擴(kuò)展都不利,而且學(xué)習(xí)成本也大大地增加了。

如果NSOperationQueue由客戶創(chuàng)建,每一個(gè)要用操作抽象的客戶都需要自己維護(hù)一個(gè)或多個(gè)NSOperationQueue,那么客戶的職責(zé)就增加了,而我們的初衷是減少客戶的交互。如果不同類型的操作抽象不能共用同一個(gè)NSOperationQueue,那么就需要為每一類操作抽象都創(chuàng)建并維護(hù)一個(gè)NSOperationQueue。

面對(duì)這些問題,我們先做出一些假設(shè),嘗試基于這些假設(shè)來實(shí)現(xiàn)操作管理抽象。每一類操作抽象都有一個(gè)相應(yīng)的操作管理抽象,由操作管理抽象來創(chuàng)建和管理NSOperationQueue,不同類型的操作抽象可以共用同一個(gè)NSOperationQueue,存在全局的NSOperationQueue。

考慮一下操作管理抽象的基本實(shí)現(xiàn),基本實(shí)現(xiàn)就是創(chuàng)建NSOperationQueue,根據(jù)實(shí)際需要對(duì)每一類操作抽象做額外的處理,把操作抽象添加到NSOperationQueue中。現(xiàn)在對(duì)基本實(shí)現(xiàn)做一般化的抽象,創(chuàng)建一個(gè)高層抽象,在高層抽象定義一個(gè)基本實(shí)現(xiàn)的模板方法,由具體的操作管理抽象實(shí)現(xiàn)模板方法中相應(yīng)的方法。

現(xiàn)在還有兩個(gè)需求要解決,不同類型的操作抽象可以共用同一個(gè)NSOperationQueue,存在全局的NSOperationQueue??梢詾楦邔映橄?OperationManager)的模板方法中的子方法提供默認(rèn)實(shí)際來解決這兩個(gè)需求,也就是在高層抽象中提供全局公共的NSOperationQueue。這樣,具體的操作管理抽象可以根據(jù)自己的需要選擇實(shí)現(xiàn)或不實(shí)現(xiàn)創(chuàng)建NSOperationQueue的方法,還可以對(duì)相應(yīng)的操作抽象做一些額外的處理。

基于前面的假設(shè)確定了基本設(shè)計(jì),現(xiàn)在來確定實(shí)現(xiàn)基本設(shè)計(jì)的語(yǔ)言,不同的語(yǔ)言會(huì)有不同的特性,在最終實(shí)現(xiàn)時(shí),可以根據(jù)語(yǔ)言特性來做一些特殊的處理,或者選擇更容易實(shí)現(xiàn)設(shè)計(jì)的語(yǔ)言來實(shí)現(xiàn)?,F(xiàn)在我們需要為每一類操作抽象都創(chuàng)建一個(gè)相應(yīng)的操作管理抽象,這種一對(duì)一的關(guān)系是否很像Objective-C中的類對(duì)象和實(shí)例對(duì)象?在Objective-C的世界中,所有的東西都是對(duì)象,包括類,Objective-C中的類對(duì)象主要有兩個(gè)職責(zé),一是提供創(chuàng)建實(shí)例用的原型模板,二是用于消息機(jī)制。用類對(duì)象來充當(dāng)設(shè)計(jì)中的操作管理抽象,這樣在編寫代碼的時(shí)候,不再需要引入新的類型,在運(yùn)行時(shí)也可以減少所需要的內(nèi)存,減少程序中的抽象。

首先對(duì)NSOperation類對(duì)象做職責(zé)擴(kuò)展,為NSOperation添加一個(gè)分類,分類中定義一個(gè)模板方法。

NSOperation的子類可以實(shí)現(xiàn)模板方法中的兩個(gè)子方法,第一個(gè)子方法是一個(gè)hook,讓子類有權(quán)決定是否繼續(xù)執(zhí)行模板方法,如果子類想完全接手操作對(duì)象的管理,只要實(shí)現(xiàn)方法并返回YES即可。提供公共的NSOperationQueue,同時(shí)也留出擴(kuò)展的空間讓子類創(chuàng)建和管理自己的NSOperationQueue。

我們還需要有一個(gè)對(duì)象來控制線程安全,把操作對(duì)象安全地分配到操作管理對(duì)象中。對(duì)NSOperationQueue類對(duì)象做職責(zé)擴(kuò)展,讓NSOperationQueue的類對(duì)象來完成這些操作,選擇NSOperationQueue類對(duì)象的主要原因是,減少程序中的抽象,而且平時(shí)的習(xí)慣也是創(chuàng)建操作對(duì)象,再把操作對(duì)象添加到NSOperationQueue中。

客戶只需要關(guān)注完成某一事件需要什么操作,這些操作怎么組合,不需要關(guān)注這些操作的實(shí)現(xiàn),也不需要關(guān)注這些操作是否并發(fā),這些對(duì)客戶都是透明的。具體的實(shí)現(xiàn)請(qǐng)看Demo

最后編輯于
?著作權(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)容

  • Object C中創(chuàng)建線程的方法是什么?如果在主線程中執(zhí)行代碼,方法是什么?如果想延時(shí)執(zhí)行代碼、方法又是什么? 1...
    AlanGe閱讀 1,927評(píng)論 0 17
  • *面試心聲:其實(shí)這些題本人都沒怎么背,但是在上海 兩周半 面了大約10家 收到差不多3個(gè)offer,總結(jié)起來就是把...
    Dove_iOS閱讀 27,656評(píng)論 30 472
  • 線程、進(jìn)程 1.iOS中的多線程操作、多線程方式? 2.多線程的優(yōu)點(diǎn)和缺點(diǎn)分別是什么? 答:優(yōu)點(diǎn):1、將耗時(shí)較長(zhǎng)的...
    丶逐漸閱讀 1,479評(píng)論 0 8
  • 1.介紹下內(nèi)存的幾大區(qū)域? 2.你是如何組件化解耦的? 3.runtime如何通過selector找到對(duì)應(yīng)的IMP...
    小孩仔閱讀 1,822評(píng)論 0 21
  • 我的心中生長(zhǎng)著一簇花 它的名字叫曼珠沙華 它說,愛要勇敢,愛要熾熱 它說,愛要不懼將來,...
    琳子啊閱讀 160評(píng)論 0 1

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