先說下背景:
我們想通過一種統(tǒng)一的跳轉(zhuǎn)鏈接杨赤,來實(shí)現(xiàn)H5、原生及端外跳轉(zhuǎn)到端內(nèi)等幾種場景下的統(tǒng)一跳轉(zhuǎn)。這就涉及到頁面跳轉(zhuǎn)組件风皿。這些跳轉(zhuǎn)組件的設(shè)計(jì)比較考驗(yàn)開發(fā)人員的能力,如何做才能減少頁面間的耦合匠璧,提高模塊化呢桐款?本文將為你講述一種實(shí)現(xiàn)思路,至于優(yōu)劣夷恍,見仁見智魔眨。
首先。我們的跳轉(zhuǎn)鏈接大致如下:?
qimi://juanpi?type=1&content={"a":"ssssss","b":"gggg"}
這里面type是跳轉(zhuǎn)頁面的類型酿雪,content是需要的參數(shù)遏暴,用json形式組裝,由各個(gè)頁面自己解析指黎。
一朋凉、原始想法
最開始我的想法是這樣的:
? ? ?首先,所有跳轉(zhuǎn)都得經(jīng)過一個(gè)類醋安,這個(gè)類里面得包含所有需要跳轉(zhuǎn)的頁面杂彭,然后根據(jù)參數(shù)來區(qū)分具體跳轉(zhuǎn)到哪里。這個(gè)類我們暫定為QMAction茬故,核心的方法大概是這樣的:
-(void)handleJumpWithType:(NSInteger)type withParams:(NSDictionary:)params ?jumpController:(UINavigationController*)controller?{ ? ?
? ? ?UIViewController *destVC;
? ? ?if (type == 1) //首頁?{
? ? ? ? ? ?destVC = [[HomeViewController alloc] init];
? ? ? } else if(type == 2) //列表頁面 {
? ? ? ? ? ? destVC = [[ListViewController alloc] init];
? ? ? }?
? ? ? .........//還有很多類似的判斷
? ? ? if(destVC) {
? ? ? ? ? ? [controller pushViewController:destVC];
? ? ? }
}
但是這么做有幾個(gè)問題:
1. 首先盖灸,跳轉(zhuǎn)的來源包括H5頁面、外部app磺芭、推送或者任何本地頁面赁炎,有的情況下需要push一個(gè)新頁面,有的情況需要先pop回首頁再push钾腺,如何適應(yīng)多樣化的需求徙垫?
2. 每次新增type,就要多引用一個(gè)頭文件放棒,即引用依賴過多姻报。
3. 創(chuàng)建VC的代碼大部分都是重復(fù)的,這種重復(fù)的alloc init 實(shí)際上是在扼殺編程的熱情间螟。我們需要減少重復(fù)代碼吴旋。
基于這幾個(gè)問題损肛,我們嘗試重新設(shè)計(jì)頁面跳轉(zhuǎn)組件。
二荣瑟、解決引用依賴
熟悉OC的朋友都知道治拿,OC有一個(gè)動(dòng)態(tài)運(yùn)行機(jī)制,所有的類和方法都可以通過字符串來獲得笆焰,比如NSSelectorFromString,NSClassFromString劫谅。如果我用這個(gè)方法,讓外部把類名傳遞進(jìn)來嚷掠,是不是就不用那么多頭文件了呢捏检?而且這樣做if...else if...else 這種判斷也大大減少了。
? ? 抱著這個(gè)想法不皆,我嘗試了修改handleJumpWithType:params:jumpController:這個(gè)方法贯城,確實(shí)是可以動(dòng)態(tài)的去獲取類,但是有一個(gè)問題粟焊,有的VC類名特別長冤狡,而且在推送和H5頁面跳轉(zhuǎn)的場景下,得針對安卓和ios做區(qū)分项棠,因?yàn)閮蛇叺念惷⒉幌嗤āS谑牵@個(gè)方案很快就被否決了香追。那么合瓢,還有沒有其它的方式呢?
? ?可不可以在QMAction內(nèi)部做一個(gè)映射呢透典?通過一個(gè)字典獎(jiǎng)type和類名對應(yīng)起來晴楔,這樣后端只需要知道某個(gè)頁面對應(yīng)的type,就可以設(shè)置跳轉(zhuǎn)鏈接了峭咒。這個(gè)方法有一定的靈活性税弃,但還是面臨一個(gè)問題,就是當(dāng)類名改了以后凑队,這個(gè)映射的字典里面也得做對應(yīng)的修改则果,如果開發(fā)人員忘記改了,就跳轉(zhuǎn)不了了漩氨。這種方式維護(hù)起來還是不夠簡單西壮,而且還是需要引用頭文件。
? ? ?那么叫惊,能不能用插件的方式呢款青?從之前了解過的一些瀏覽器插件管理器的實(shí)現(xiàn)方式來看,我們可以將每一個(gè)頁面當(dāng)作一個(gè)插件霍狰,通過輪詢的方式來得到哪一個(gè)插件能響應(yīng)我需要調(diào)用的方法抡草。他的代碼大概是這樣的:
for (Plugin * plugin in self.plugins) {
? ? if([plugin respondToSelector:NSSelectorFromString(xxxx)]) {
? ? ? ? [plugin performSelector:NSSelectorFromString(xxxx)];
? ? ? ? ?break;
? ? }
}
這里的前提是饰及,每一個(gè)插件都繼承自Plugin的基類。而self.plugins數(shù)組里面包含的就是所有的插件渠牲,這些插件是在程序啟動(dòng)的時(shí)候通過代碼或者plist文件添加到這個(gè)數(shù)組里面的旋炒。
我們把每一個(gè)頁面當(dāng)做一個(gè)插件來處理,在QMActon中只需要引入一個(gè)Plugin基類签杈,再在各個(gè)類中加一個(gè)方法,判斷能不能處理跳轉(zhuǎn)的請求鼎兽,這樣不就大大減少了引用的文件了嗎答姥?
三、協(xié)議與遍歷
通過第二步的改良谚咬,其實(shí)我們已經(jīng)算是解決了引用依賴過多的問題鹦付,但是這樣做代碼的侵入性還是有點(diǎn)高,因?yàn)槊總€(gè)VC都得繼承自同一個(gè)基類择卦,這對于之前沒有共同基類的VC來說改動(dòng)還是有點(diǎn)大敲长,那么能不能在不改變原來類的繼承關(guān)系的條件下來實(shí)現(xiàn)呢?
這時(shí)候我想到了協(xié)議(或者說代理)秉继。
協(xié)議是目前所有重構(gòu)方案里面代碼侵入性最小的祈噪,一不需要改變基類,對協(xié)議的宿主(也就是實(shí)現(xiàn)協(xié)議的類)沒有類型上的要求尚辑,方便移植;二不強(qiáng)制要求宿主類必須實(shí)現(xiàn)某個(gè)方法(協(xié)議方法分為可選和必選兩種)。
比如之前那段輪詢的代碼昼扛,用協(xié)議實(shí)現(xiàn)起來是這樣的:
for (id * plugin in self.plugins) {
? ? if([id<QMActionProtocol> respondToSelector:NSSelectorFromString(xxxx)]) ? ? {
? ? ? ?[id<QMActionProtocol> performSelector:NSSelectorFromString(xxxx)];
? ? ? ?break;
? ? }
}
這樣瞭恰,我并不需要引用具體的子類,是不是看起來好一點(diǎn)呢瓢喉?
但是宁赤,還有一個(gè)問題,如果我要像H5插件那樣在程序啟動(dòng)時(shí)把所有插件類注冊一遍栓票,未免有些low了决左,這種引用依賴關(guān)系或多或少還是存在一點(diǎn)。有沒有更徹底的辦法呢逗载?
這時(shí)候哆窿,就要用到runtime了。
我們知道runtime可以動(dòng)態(tài)判斷一個(gè)類有沒有實(shí)現(xiàn)一個(gè)方法厉斟,那他應(yīng)該也可以判斷一個(gè)類有沒有實(shí)現(xiàn)一個(gè)協(xié)議挚躯。帶著疑問,我查閱了一下runtime的文檔擦秽,找到了這個(gè)方法:
OBJC_EXPORT BOOL class_conformsToProtocol(Class cls, Protocol *protocol)
有了這個(gè)方法码荔,我就可以不需要自己去寫代碼一行行注冊插件了漩勤,只需要先遍歷系統(tǒng)的所有類,然后判斷每個(gè)類是否實(shí)現(xiàn)了這個(gè)協(xié)議就可以了缩搅。
為了減少每次遍歷的性能消耗(類越多越败,消耗越大),我們可以用一個(gè)數(shù)組把這些類緩存下來硼瓣,下次直接從數(shù)組里面讀出究飞。
四、協(xié)議的具體實(shí)現(xiàn)
前面只是通過運(yùn)行時(shí)的方法拿到了所有類堂鲤,但是輪詢是避免不了的亿傅,我們需要對每一個(gè)類做判斷,是否對應(yīng)了我們的type瘟栖。另外葵擎,還需要一個(gè)方法來創(chuàng)建VC,以及另一個(gè)方法來做一些特殊處理
每個(gè)遵循了QMActionProtocol的類需要實(shí)現(xiàn)這些協(xié)議方法(1必須實(shí)現(xiàn)半哟,2酬滤、3可選),其中QMAction里面就含有type和content寓涨。 同一個(gè)類可以對應(yīng)多個(gè)type盯串,但一個(gè)type只能對應(yīng)一個(gè)類,否則系統(tǒng)就會根據(jù)遍歷的順序跳到優(yōu)先跳到第一個(gè)對應(yīng)這個(gè)type的類缅茉,而這個(gè)順序是隨機(jī)的嘴脾。
當(dāng)所有類都輪詢過一遍之后,就有了type和class的對應(yīng)關(guān)系蔬墩,而這個(gè)關(guān)系以后會頻繁用到译打,所以我們可以用NSCache(一種Key-Value的容器,類似NSDictionary拇颅,但內(nèi)存不足時(shí)會自動(dòng)釋放)緩存下來奏司。
創(chuàng)建VC的那個(gè)方法所做的就是解析content,給VC初始化并對相應(yīng)屬性賦值樟插,對于一些簡單的VC韵洋,只需要一句
id viewcontroller = ?[[[self class] alloc] init];
就可以了。
參數(shù)的賦值可以通過字典和KVC 解決黄锤,我們將自定義的一個(gè)字典對象當(dāng)作參數(shù)容器傳給VC搪缨,VC拿到后,調(diào)用setValuesForObject 方法鸵熟,把字典的key當(dāng)作屬性名稱副编,value當(dāng)作屬性值,直接賦值流强。
其中一個(gè)例子:
五痹届、子類化QMAction與默認(rèn)值
前面說的是如何運(yùn)用協(xié)議和運(yùn)行時(shí)來解決依賴和耦合的問題呻待,現(xiàn)在要解決的另一個(gè)問題是如何隱藏不同類之間的差異。
我們知道QMAction是一個(gè)跳轉(zhuǎn)組件队腐, 一般情況下需要?jiǎng)?chuàng)建一個(gè)VC蚕捉,然后跳轉(zhuǎn)過去, 但是有些特殊場景是不需要?jiǎng)?chuàng)建VC的柴淘,比如說在當(dāng)前VC上添加一個(gè)view迫淹,或者分享某個(gè)頁面到第三方平臺,這種看似差不多的功能實(shí)際上處理起來很不一樣悠就。為了讓QMAction能適應(yīng)所有的場景千绪,就需要統(tǒng)一調(diào)用方式,隱藏內(nèi)部的差異梗脾。
我們把QMAction拆成了三類,QMPushAction, QMShareAction和QMCustomAction盹靴。三個(gè)類分別對應(yīng)普通的跳轉(zhuǎn)炸茧、分享以及自定義的場景(如彈起浮層,關(guān)閉某個(gè)頁面)等稿静。這三個(gè)類共同繼承自QMAction梭冠。同時(shí),在類的內(nèi)部改备,我們加上了一段代碼指定某個(gè)type對應(yīng)的默認(rèn)class控漠。這么做的好處是不用在調(diào)用的代碼里顯式申明action的類型,在H5悬钳、外部app等非原生代碼調(diào)用的場景下能自動(dòng)匹配到對應(yīng)的類型盐捷。
同樣的技巧我們也用在了QMPushAction的transitionStyle上,這個(gè)屬性指定了跳轉(zhuǎn)的方式默勾,如Push碉渡,Pop,Present等母剥。除非是調(diào)用的時(shí)候顯式的指定了transitionStyle滞诺,否則以下這些type會按默認(rèn)的跳轉(zhuǎn)方式來執(zhí)行,這在H5頁面跳轉(zhuǎn)到原生頁面時(shí)特別有效环疼。這里transitionStyle的類型用了NSNumber而不是NSInteger 是因?yàn)镹SNumber默認(rèn)是nil习霹,可以使用懶加載,而NSInteger不行炫隶。
下面是使用transitionStyle的代碼: