iOS解耦實(shí)踐

回顧2017,整年對(duì)公司現(xiàn)有App進(jìn)行了大大小小接近20版本的迭代,因?yàn)樵许?xiàng)目創(chuàng)建較早,代碼質(zhì)量上并不算高(早年的技術(shù)你懂得,那時(shí)候可能才有MVVM,那時(shí)候runtime還沒有被廣泛使用,runloop可能只是了解階段),所以伴隨著每一版本迭代都會(huì)做一部分代碼優(yōu)化、重構(gòu),而目的就是為了針對(duì)原有類似線團(tuán)一樣的業(yè)務(wù)進(jìn)行解耦。

都做了那些工作呢?總結(jié)了一下基本就是以下幾點(diǎn),無非就是怎樣解耦:

1、組件化

2、結(jié)合MVVM架構(gòu)和數(shù)據(jù)驅(qū)動(dòng)UI模式對(duì)原有MVC架構(gòu)進(jìn)行了兼容性優(yōu)化

3、通過AOP技術(shù)對(duì)部分業(yè)務(wù)進(jìn)行拆分解耦

4、優(yōu)化事件傳遞方式


下面來詳細(xì)說一說

一、組件化,組件化實(shí)現(xiàn)方案較多,網(wǎng)上也算是百家爭(zhēng)鳴,而我們當(dāng)時(shí)完全是從無到有,自己一步一步躺著坑總結(jié)了一套比較low的方案。

最開始有這個(gè)想法的時(shí)候,是因?yàn)楫a(chǎn)品需求放突然有了想在多個(gè)應(yīng)用引入同一功能的想法,所以大家開始集思廣益,開始了組件化之路。

之后我們基于git的子工程進(jìn)行了組件化的開發(fā),首先進(jìn)行了公共業(yè)務(wù)的組件化抽離,將一些SDK(網(wǎng)絡(luò)、第三方工具等)進(jìn)行了歸類并放入組件中,之后,對(duì)App內(nèi)部業(yè)務(wù)進(jìn)行梳理,將大的業(yè)務(wù)模塊逐步拆分成了各個(gè)小的組件。直到現(xiàn)在,已經(jīng)拆分出來的組件大概7個(gè)左右,基本上已經(jīng)對(duì)模塊上的解耦合做了最大努力,接下來部分沒有優(yōu)化的模塊內(nèi)部的繁重業(yè)務(wù)會(huì)繼續(xù)去優(yōu)化。而在今年,可能會(huì)考慮使用通過cocoaPods方式去管理組件。當(dāng)然這是后話,總結(jié)今年的組件化實(shí)現(xiàn),覺得有以下幾點(diǎn)最重要:

(1)要先從公共基礎(chǔ)部分開始抽離,這部分如果不能先行,會(huì)給之后的組件化增加很多不必要的工作,比如AF、SDWebImage,每一次都需要去在組件中重復(fù)導(dǎo)入,而且在SDK維護(hù)的時(shí)候需要多個(gè)組件以及主工程同時(shí)維護(hù)。

(2)公共基礎(chǔ)組件建立后,梳理整體業(yè)務(wù),尋找節(jié)點(diǎn),分割出各個(gè)業(yè)務(wù)模塊,然后以這些模塊為組件進(jìn)行下一步的優(yōu)化重構(gòu),由大到小化整為零。

(3)善用節(jié)點(diǎn)制作組件與組件或組件與主工程的中間層,舉個(gè)栗子:我們工程中有下單支付功能、有充值功能、有購買紅包功能,總體來講都屬于支付,大致流程有3個(gè)步驟:

1)生成訂單

2)利用訂單信息拉起第三方支付

3)支付完畢后客戶端后續(xù)操作

大體上是這樣,只不過不同的功能在細(xì)節(jié)處理上又有些許差異。但有兩個(gè)節(jié)點(diǎn)是不變的,首先都是創(chuàng)建訂單(這是支付功能的發(fā)起),最后是支付完結(jié)后的本地App操作(這是支付功能的結(jié)尾)。

這樣就好辦了,我們就以這兩個(gè)節(jié)點(diǎn)作為整個(gè)支付功能的入口和出口。

內(nèi)部定制好各種支付方式的訂單生成,以及統(tǒng)一的拉起第三方支付,之后支付完成后統(tǒng)一返回結(jié)果給外部。。。

內(nèi)部實(shí)現(xiàn)有點(diǎn)復(fù)雜,我們只說中間層的設(shè)計(jì)。

中間層為一個(gè)若引用的單例(可以保證在沒有對(duì)象持有的情況下自動(dòng)銷毀,避免內(nèi)存浪費(fèi)),整個(gè)單例只暴露一個(gè)帶有block的方法,傳入?yún)?shù),之后一系列的訂單生成、拉起支付都不需要外部知道,最后通過block返回支付的結(jié)果。業(yè)務(wù)方只需要告知支付組件詳細(xì)的支付參數(shù),然后靜等支付結(jié)果就可以了。

組件化優(yōu)點(diǎn)太多,大家嘗試過就知道了,畢竟我們的還是比較簡(jiǎn)陋的,低調(diào)點(diǎn),就不繼續(xù)說了。


二、結(jié)合MVVM架構(gòu)和數(shù)據(jù)驅(qū)動(dòng)UI模式對(duì)原有MVC架構(gòu)進(jìn)行了兼容性優(yōu)化

MVC(調(diào)侃一下Massive View Controller),經(jīng)典的設(shè)計(jì)模式,之前看到至少500甚至有的多達(dá)幾千行代碼的ViewController時(shí)都傻眼了,這該如何接手代碼?這么多業(yè)務(wù)咋熟悉?。坑绕涫菑?fù)雜列表代碼實(shí)現(xiàn)中成堆的if-else。。。。。。初始時(shí),是針對(duì)如何去if-else開始思考的,后來蔓延到拆分ViewController的繁重業(yè)務(wù)。

之前對(duì)數(shù)據(jù)驅(qū)動(dòng)模式有過了解,腦中第一時(shí)間想到了這種方式,如果讓model和view一一對(duì)應(yīng)的話,再讓model執(zhí)行統(tǒng)一的一套方法創(chuàng)建對(duì)應(yīng)的View和賦值View不就達(dá)到目的了么。但這時(shí)又有新的問題了,model本來只是負(fù)責(zé)數(shù)據(jù)處理保存的,如果加上這部分業(yè)務(wù)不就成了真正的胖model了么?是不是考慮換一套設(shè)計(jì)模式呢?于是開始了對(duì)MVVM的初步了解,當(dāng)時(shí)也沒有太多精力去仔細(xì)了解和實(shí)踐,大致明白MVVM多了一個(gè)ViewModel這樣的東西,針對(duì)數(shù)據(jù)做了一部分處理,分離了數(shù)據(jù)的處理業(yè)務(wù)。那好,我們也添加一個(gè)ViewModel層,主管特定View的創(chuàng)建以及數(shù)據(jù)的預(yù)處理業(yè)務(wù)(當(dāng)時(shí)沒有去詳細(xì)了解MVVM,因?yàn)楦杏XMVVM其實(shí)本質(zhì)上也還是MVC,其他的架構(gòu)也都是其變種,而如何去使用還是要考慮到本地的業(yè)務(wù),畢竟人與人是不同的,代碼也不同,每個(gè)人都有自己的需求,所以在優(yōu)化的時(shí)候并不一定要完全否定自己的東西,而是需要做一些能很好兼容以往代碼的優(yōu)化,尤其是架構(gòu)的選擇)。

說歸說,利用偽代碼大概縷清流程后擼起袖子就開干:

首先針對(duì)復(fù)雜列表羅列出都有哪幾種類型的Cell,然后創(chuàng)建這些Cell,在創(chuàng)建對(duì)應(yīng)的ViewModel,通過協(xié)議方法讓viewModel創(chuàng)建指定的Cell,最后tableView的代理方法直接讓viewModel走協(xié)議方法返回對(duì)應(yīng)cell,OK,搞定,再也不會(huì)看到if-else,當(dāng)然,這看起來有點(diǎn)草率,我舉個(gè)栗子然后詳細(xì)講解一下:

場(chǎng)景:aVC中有tableView,展示了2中cell,分別是c1 和 c2兩種cell,原有的cellForRowAtIndexPath協(xié)議中需要通過if-else判斷來得知?jiǎng)?chuàng)建哪一種cell?,F(xiàn)在對(duì)原有代碼進(jìn)行修改:

(1)c1、c2不變

(2)創(chuàng)建兩個(gè)ViewModel,VM1、VM2。

(3)創(chuàng)建一套協(xié)議,CellProtocol,定義兩個(gè)方法,creatCell方法、configCell方法,前者用來創(chuàng)建返回cell,后者賦值cell。

(4)修改VC中網(wǎng)絡(luò)請(qǐng)求業(yè)務(wù),在請(qǐng)求轉(zhuǎn)換成Model后,利用Model生成不同的ViewModel,比如雖然每一條model都是屬于一個(gè)類的,但某一參數(shù)決定了他是c1所需要的參數(shù)還是c2所需要的,就根據(jù)這些去創(chuàng)建VM1和VM2,之后把包含這些VM的數(shù)組給tableView當(dāng)做數(shù)據(jù)源。

(5)修改VC中的cellForRowAtIndexPath內(nèi)部的代碼,利用協(xié)議的兩個(gè)方法來進(jìn)行創(chuàng)建和賦值:

id cell = [self.dataSource[indexPath.row] creatCell];//創(chuàng)建

[cell configCell:self.dataSource[indexPath.row]];//賦值cell

return cell;

三行代碼搞定--!

細(xì)心地朋友會(huì)發(fā)現(xiàn),其實(shí)我只是吧if-else放到了數(shù)據(jù)請(qǐng)求回來的地方,在哪里進(jìn)行了if-else處理,沒什么區(qū)別嘛。。。其實(shí)不是這樣的,區(qū)別很大的,請(qǐng)求時(shí)你的菊花多轉(zhuǎn)3-5圈用戶是沒什么感知的,但是如果你的菊花已經(jīng)消失,但是列表渲染時(shí)或者說滾動(dòng)過程中各種掉幀、閃爍、一動(dòng)一卡,用戶體驗(yàn)是很糟糕的,這種做法有點(diǎn)像微博之前針對(duì)cell高度緩存的做法,延長(zhǎng)數(shù)據(jù)的解析時(shí)間換來交互的流暢度,在數(shù)據(jù)處理時(shí)就確定好每一個(gè)cell的高度,而不是在cell拿到數(shù)據(jù)后再去計(jì)算。

在這之后,對(duì)于ViewModel的使用越來越多,再也看不到if-else了,但如何解決VC的繁重問題呢,對(duì)于這一部分的思考是,在理想的MVC中C只是作為M與V的橋接對(duì)象,并對(duì)V的交互做出響應(yīng),橋接部分沒什么可說的,工程中C歷來都是將基本的Model傳給View,view針對(duì)原始數(shù)據(jù)處理一次后再賦值,C中的業(yè)務(wù)目前還剩數(shù)據(jù)請(qǐng)求以及加工、View事件的處理,View的更新。按照理想中的C來看,其實(shí)沒什么不一樣,但現(xiàn)實(shí)是代碼就擺在那里,多的數(shù)不清。于是對(duì)代碼業(yè)務(wù)進(jìn)行了拆分,數(shù)據(jù)的請(qǐng)求還是C去搞,也就是調(diào)用接口,但返回的數(shù)據(jù)交給ViewModel去進(jìn)行處理并返回ViewModel實(shí)例給C,C持有ViewModel并可以通過修改ViewModel另View做出對(duì)應(yīng)的UI更新,而ViewModel掌握著創(chuàng)建對(duì)應(yīng)View的實(shí)現(xiàn),View通過協(xié)議、通知、block等方式將交互事件傳給C去統(tǒng)一處理,看一個(gè)簡(jiǎn)單結(jié)構(gòu):



ps:以后多多學(xué)習(xí)怎么寫文章,實(shí)在是沒搞過,各種手段都有點(diǎn)low,諒解諒解。。。。

基本上就是這樣的一個(gè)結(jié)構(gòu),可能沒描述清除,但是角色的配置大致就是這樣,我的同事分的比較多,他還習(xí)慣利用一個(gè)單獨(dú)的handler把交互事件抽離走,讓VC基本成了承載View的空架子。

架構(gòu)真不是很在行,我也就只是剛剛有個(gè)了解,算不算半只腳踏入都不知道,總之描述一下希望能給某些迷茫的人一些啟發(fā),個(gè)人覺得最總要的還是那句話,不一定要生搬硬套,還得看自己的業(yè)務(wù)最需要什么。



三、通過AOP技術(shù)對(duì)部分業(yè)務(wù)進(jìn)行拆分解耦

項(xiàng)目中有各種第三方SDK的使用,友盟、個(gè)推、GrowingIO等等,都需要在Appdelegate的各個(gè)方法中去集成,久而久之造成AppDelegate中代碼過于難看、復(fù)雜,且耦合度高,可移植性差。針對(duì)這一問題,我的處理方式是通過切片向AppDelegate中切入各個(gè)SDK的集成服務(wù),關(guān)于AOP不做介紹了,大家可以百度一搜,有很多講解的,這里簡(jiǎn)短說一下我的實(shí)現(xiàn)。

具體場(chǎng)景:個(gè)推的集成

1、利用runtime制作AppDelegate的切片(一個(gè)AppDelegate的category),利用runtime交換application: didFinishLaunchingWithOptions:等幾個(gè)方法,新方法為GT_application: didFinishLaunchingWithOptions:

2、在新的GT_application: didFinishLaunchingWithOptions:方法中進(jìn)行個(gè)推的集成,其他幾個(gè)方法中實(shí)現(xiàn)接收推送后的處理,這里就不寫了,只以這個(gè)方法說。

3、GT_application: didFinishLaunchingWithOptions:內(nèi)執(zhí)行過個(gè)推注冊(cè)后執(zhí)行代碼,

[self GT_application: application didFinishLaunchingWithOptions:options];這行代碼是能否形成切片的重點(diǎn),類似子類重寫父類方法后有返回父類實(shí)現(xiàn)去執(zhí)行后續(xù)的代碼。

這樣一個(gè)面向于個(gè)推SDK的切片就OK了,以后如果有別的項(xiàng)目想要同樣集成個(gè)推可以直接把這個(gè)切片搞過去,修改一下注冊(cè)時(shí)候的各種Key、AppID就可以了,不需要粘貼復(fù)制或者重寫一遍

實(shí)際業(yè)務(wù)中SDK的拆分只是切片的一部分,很多本地工具的切片化也處理了很多,另外還利用切片針對(duì)tableView的空數(shù)據(jù)占位顯示做了處理:

app中很多的頁面都包含tableView,但由于之前做的時(shí)候都單獨(dú)對(duì)每一個(gè)tableView做的空數(shù)據(jù)、無網(wǎng)默認(rèn)圖顯示處理,每一次都需要重寫一遍,即使封裝了空數(shù)據(jù)的占位UI還是覺得每一次判斷數(shù)據(jù)有無、網(wǎng)絡(luò)有無比較麻煩,所以就把這個(gè)需求提上了日程,想設(shè)計(jì)一個(gè)能夠自動(dòng)監(jiān)測(cè)tableView當(dāng)前數(shù)據(jù)源有無、并且根據(jù)網(wǎng)絡(luò)環(huán)境來決定展示空數(shù)據(jù)圖還是無網(wǎng)刷新圖的一個(gè)工具。

關(guān)鍵點(diǎn)就在于如何實(shí)現(xiàn)自動(dòng)監(jiān)測(cè)這一功能,經(jīng)過分析梳理發(fā)現(xiàn),reloadData是一個(gè)統(tǒng)一的節(jié)點(diǎn),想要刷新tableView必定要走這個(gè)方法,所以針對(duì)這個(gè)方法只做了一個(gè)切片,在新的N_reloadData方法中針對(duì)tableView當(dāng)前的數(shù)據(jù)源dataSource內(nèi)容進(jìn)行了判斷,又結(jié)合網(wǎng)絡(luò)環(huán)境來決定空數(shù)據(jù)時(shí)展示內(nèi)容,大致的代碼分為以下2個(gè)部分:

1、空數(shù)據(jù)View(分為無網(wǎng)以及正??諗?shù)據(jù))

2、tableView的切片,可配置空數(shù)據(jù)時(shí)展示內(nèi)容,針對(duì)reloadData做了切片處理,提供空數(shù)據(jù)View的交互block

在實(shí)際使用過程中,只需要在創(chuàng)建tableView的時(shí)候配置好空數(shù)據(jù)的展示內(nèi)容即可,其他不需要多做任何處理,如果需要處理空數(shù)據(jù)圖的某些交互,只需要在block內(nèi)實(shí)現(xiàn)即可??匆幌潞?jiǎn)單的代碼:

//配置代碼

_tableView.emptyViewImage = [UIImage imageNamed:@"1.png"];

_tableView.emptyViewContent = @"暫無數(shù)據(jù)哦!";

_tableView.emptyViewDetaileContent = @"請(qǐng)稍后重試";

_tableView.emptyViewButtonTitle = @"點(diǎn)我刷新";


//交互代碼

_tableView.emptyViewUserInteraction:^(){

//刷新頁面

};

不貼詳細(xì)代碼了,詳細(xì)代碼太多,里面涉及到很多細(xì)節(jié)實(shí)現(xiàn),有網(wǎng)絡(luò)有針對(duì)tableView初始化立即執(zhí)行以便reloadData的兼容處理。。。。授人以魚不如授之以漁,思路最重要。



四、優(yōu)化事件傳遞方式

產(chǎn)品業(yè)務(wù)越來越多,為了追求華麗各種復(fù)雜頁面也層出不窮,代碼封裝越來越多,頁面內(nèi)的視圖層級(jí)也越來越多,頁面層級(jí)大于5級(jí)的太常見了:view上承載tableView,cell上承載某個(gè)view,view上有其他的view,其他的view上還有其他view。。。。。一層一層,每到這種時(shí)候就會(huì)發(fā)現(xiàn),事件傳遞太過繁瑣了,無論是協(xié)議、還是block都要經(jīng)歷多層的傳遞才能被VC接收到并處理,如果用通知的話 還好,不用考慮中間的傳遞,但是不敢頻繁的使用通知啊,漫天的通知鬼知道什么時(shí)候給你來個(gè)超級(jí)無厘頭BUG。那該如何避免復(fù)雜的層級(jí)傳遞和管理的難度呢?

響應(yīng)鏈,沒錯(cuò)就是通過響應(yīng)鏈走,收到網(wǎng)友的啟發(fā),我為UIResponder制作了一個(gè)分類,利用響應(yīng)鏈來進(jìn)行事件的傳遞,事實(shí)證明這個(gè)點(diǎn)子溜了(我自己以為的),看一下簡(jiǎn)略代碼吧:

1、建立UIResponder的分類,添加userInteractionWithMethod:params:方法,

傳入2個(gè)參數(shù),一個(gè)是方法的唯一標(biāo)識(shí)符用于告訴VC這次調(diào)用想要執(zhí)行什么交互事件,最后是可能想要傳遞的參數(shù)

內(nèi)部實(shí)現(xiàn)是:

if (self.nextResponder) {

? ? ? ? [self.nextResponder userInteractionWithMethod:eventName params:params];

? ? }沒錯(cuò),就這三行代碼

2、在事件的發(fā)起處,比如N多層級(jí)上的一個(gè)Button點(diǎn)擊事件,調(diào)用方法,并傳入?yún)?shù)

[ABtn userInteractionWithMethod:@"投注按鈕點(diǎn)擊" params:@{@"金額":@"10塊",@"訂單ID":@"1234"}];

3、在VC中重寫userInteractionWithMethod:方法,通過傳過來的唯一標(biāo)識(shí)符來決定處理什么交互業(yè)務(wù),并使用相關(guān)的參數(shù)

比如:AVC.m中 重寫了方法并處理上面按鈕的事件

- (void)userInteractionWithMethod:(NSString *)name params:(id)params{

if([name isEqualToString:@"投注按鈕點(diǎn)擊"])

? ? ? ?[self goPayMent:params];//發(fā)起具體支付業(yè)務(wù)

}

4、特定業(yè)務(wù)情況下,參數(shù)需要多次包裝才能形成完整的參數(shù)并由VC處理,此時(shí)首尾不變,也就是按鈕點(diǎn)擊和VC的邏輯不變,可以再中間某一父類視圖上重寫該方法并讓nextResponder繼續(xù)執(zhí)行即可,如button的父視圖需要加一個(gè)時(shí)間戳到參數(shù)中:

AsuperView.m

- (void)userInteractionWithMethod:(NSString *)name params:(id)params{

NSMutableDictionary *n_params = [NSMutableDictionary allocwithDic:params];

if([name isEqualToString:@"投注按鈕點(diǎn)擊"])

? ? ? ?[self goPayMent:params];//發(fā)起具體支付業(yè)務(wù)

? ? ? ?[n_params setValue:@"1234567890" ForKey:@"時(shí)間戳"];

}

[self.nextResponder userInteractionWithMethod:name params:n_params];

}

這樣就可以做到參數(shù)的持續(xù)集成,不過代碼不是拷貝過來的,手打的,會(huì)有錯(cuò)誤,意會(huì)即可,切勿復(fù)制使用。


另外,還可以創(chuàng)建一個(gè).h 文件來存儲(chǔ)事件標(biāo)識(shí)符的宏定義,來進(jìn)行統(tǒng)一管理,避免單獨(dú)聲明出現(xiàn)重復(fù)。

以上就是針對(duì)事件傳遞解耦的一個(gè)大膽實(shí)踐,目前感覺用起來滿順手的,無論創(chuàng)意上還是使用成本上個(gè)人感覺至少連個(gè)SS評(píng)分。



總結(jié)2017年,有過煩惱,有過悲傷,有懷疑過自己,但從未想過放棄,別人能做到的我也一定可以,只不過受制于個(gè)人能力、智力,慢點(diǎn)、粗糙點(diǎn)而已;而事實(shí)證明至少我收貨了很多,尤其是對(duì)于核心機(jī)制runtime的深入使用,包括仿KVO底層實(shí)現(xiàn)的isa-swizzling也都做了(這個(gè)是硬性的需求,領(lǐng)導(dǎo)不希望一股腦的交換某一個(gè)方法,認(rèn)為這樣會(huì)造成資源浪費(fèi),只想在特定的情境下動(dòng)態(tài)的交換某個(gè)方法實(shí)現(xiàn)),利用自己的雙手讓之前一團(tuán)的代碼至少變成了一個(gè)一個(gè)相同顏色的小團(tuán),收獲頗豐,至少我這么覺得。但實(shí)在是不善于寫文章,這是第一次寫,想起今年一整年有點(diǎn)感慨,以后一定多多學(xué)習(xí)如何寫文章,有鏈接有圖片,爭(zhēng)取從隨筆變成藝術(shù)!

?著作權(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)容

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