我為什么要用委托模式(Delegate)來(lái)代替消息機(jī)制

首先,消息機(jī)制并沒(méi)有錯(cuò),錯(cuò)在我們使用它的場(chǎng)景。每一種技術(shù)都有他的優(yōu)點(diǎn)和價(jià)值,但是不能濫用,就像繼承一樣。沒(méi)有一種技術(shù)是放之四海而皆準(zhǔn)的,除了幾個(gè)設(shè)計(jì)原則必須遵守。

在討論這個(gè)話題之前,先來(lái)描述一下我在項(xiàng)目中被廣播消息坑過(guò)的痛苦經(jīng)歷。大家知道,新聞?lì)愋畔⒘鳟a(chǎn)品會(huì)有多個(gè)頻道,每個(gè)頻道是一個(gè)ListView或RecycleView,一個(gè)ListView里面會(huì)有很多張卡片,每張卡片就對(duì)應(yīng)一條新聞。為方便說(shuō)明問(wèn)題,借用典型信息流新聞?lì)惍a(chǎn)品的屏幕截圖來(lái)展示一下:

圖中可以直觀看出信息流產(chǎn)品的UI整體布局,頂部是頻道Tab,底部是專欄Tab,點(diǎn)擊專欄Tab例如視頻按鈕,就會(huì)進(jìn)入視頻專欄,里面又會(huì)有很多新的頻道。每個(gè)頻道都會(huì)有很多卡片,每張卡片上面會(huì)有一個(gè)刪除按鈕。

我們來(lái)還原一張類(lèi)圖:

該類(lèi)圖隱去了大量細(xì)節(jié)內(nèi)容,只展示了構(gòu)造大致UI所需的基本元素。

接下來(lái)我們接到一個(gè)需求:

需求定義是當(dāng)用戶點(diǎn)擊卡片上的刪除按鈕時(shí),彈出一個(gè)用戶反饋彈框。之前的現(xiàn)狀是直接刪除卡片,然后彈出一個(gè)Toast,Toast會(huì)在兩秒后自動(dòng)消失。這個(gè)彈框需求本身很簡(jiǎn)單,但是,當(dāng)我把這個(gè)彈框做出來(lái),集成到項(xiàng)目中后,運(yùn)行,發(fā)現(xiàn)兩個(gè)問(wèn)題:

1. 界面卡頓很厲害;

2. 點(diǎn)擊選項(xiàng)后無(wú)法關(guān)閉。

然而我將這個(gè)彈框放在Demo中,或者項(xiàng)目中其它地方,都沒(méi)有問(wèn)題。項(xiàng)目中我是這樣做的,之前是在一個(gè)刪除消息中調(diào)用doDislike方法,直接將卡片從列表中刪除。于是我在調(diào)用doDislike的地方,替換為這個(gè)新的用戶反饋彈框,當(dāng)用戶點(diǎn)擊其中一項(xiàng)后,關(guān)閉彈框,再執(zhí)行doDislike方法。感覺(jué)這個(gè)邏輯并沒(méi)有錯(cuò)啊,但是事實(shí)是程序就是出現(xiàn)了嚴(yán)重卡頓。于是在彈框入口添加Log輸出,天啊,一瞬間執(zhí)行了二十次彈框。這不科學(xué),于是查看代碼,看看刪除流程執(zhí)行過(guò)程,原來(lái),每一個(gè)ListViewController綁定了一個(gè)事件處理對(duì)象,專門(mén)用于處理來(lái)自卡片的事件,這個(gè)對(duì)象注冊(cè)了一個(gè)消息,DISLIKE_CLICK_MESSAGE,而且這個(gè)消息還是使用的項(xiàng)目?jī)?nèi)廣播消息。當(dāng)用戶點(diǎn)擊Delete按鈕的時(shí)候,卡片類(lèi)將使用廣播消息接口發(fā)送DISLIKE_CLICK_MESSAGE消息,當(dāng)ListViewController的事件處理對(duì)象收到這個(gè)消息時(shí),執(zhí)行doDislike方法。這樣使用消息的好處是回避了卡片組件跟ListView之間的空間距離,參考前面的類(lèi)圖,真實(shí)情況比這個(gè)圖要復(fù)雜得多。

但是這帶來(lái)了一個(gè)問(wèn)題,前面說(shuō)過(guò),項(xiàng)目中頻道是很多的,一個(gè)首頁(yè)專欄就有20幾個(gè)頻道,還有視頻專欄以及其它專欄等。由于考慮到用戶體驗(yàn)與加載性能,往往需要提前準(zhǔn)備緩存,也就是說(shuō)后臺(tái)已經(jīng)創(chuàng)建了多個(gè)頻道對(duì)象。這些對(duì)象都將收到DISLIKE_CLICK_MESSAGE消息,他們都會(huì)執(zhí)行彈框,因此就出現(xiàn)了剛才描述的情況,一瞬間二十幾個(gè)彈框,當(dāng)然就會(huì)異常卡頓了,而且行為也不正確。為什么之前的刪除就沒(méi)有問(wèn)題呢,因?yàn)閺?0個(gè)Toast疊加在一起,文案都是一樣的,而且都是在兩秒后消失,那么也就沒(méi)法引起注意了。

到這里,也許你會(huì)說(shuō),列表中查詢一下數(shù)據(jù)不就行了,如果不存在待刪除的卡片數(shù)據(jù),就不執(zhí)行彈框。沒(méi)錯(cuò),我就是這樣嘗試解決問(wèn)題的,前面只是前奏,現(xiàn)在才是入坑經(jīng)歷的開(kāi)始。首先當(dāng)然就是在列表中查找,找不到則不彈框,不做任何處理,相當(dāng)于讓消息發(fā)動(dòng)機(jī)空轉(zhuǎn)20轉(zhuǎn)。以為這樣就大功告成了,實(shí)測(cè)發(fā)現(xiàn),依然會(huì)出現(xiàn)連續(xù)兩次彈框。繼續(xù)排查原因,因?yàn)槭醉?yè)跟頻道列表頁(yè)是共用了一份數(shù)據(jù),也就是說(shuō)有兩個(gè)ListViewController里面是相同的數(shù)據(jù),因此出現(xiàn)問(wèn)題,而且即便不考慮首頁(yè)數(shù)據(jù)重復(fù)問(wèn)題,誰(shuí)又能保證服務(wù)器不在兩個(gè)頻道下發(fā)同一張卡片呢,我們客戶端程序總不能靠天吃飯吧,總的想辦法解決問(wèn)題才行。我注意到ListViewController有一個(gè)selected狀態(tài),一個(gè)專欄應(yīng)該只有一個(gè)頻道處于選中狀態(tài),那么我們是否可以用這個(gè)狀態(tài)來(lái)判斷,當(dāng)前處于選中狀態(tài)的ListView才響應(yīng)刪除消息,理論上可以,但是也要考慮一個(gè)情況,除了首頁(yè)新聞專欄,還有視頻專欄和其它專欄,每個(gè)專欄都可能有一個(gè)頻道處于selected狀態(tài)。要徹底解決這個(gè)問(wèn)題,還應(yīng)該引入一個(gè)isActive狀態(tài),當(dāng)專欄處于不可見(jiàn),被其它專欄覆蓋的時(shí)候,isActive設(shè)為false,最前端專欄isActive設(shè)為true,那么當(dāng)收到消息的時(shí)候,檢測(cè)selected和isActive狀態(tài),兩者同時(shí)為true,才能處理DISLIKE消息。

當(dāng)我試圖采用上述方案解決問(wèn)題的時(shí)候,問(wèn)題來(lái)了,如果是全新設(shè)計(jì)一個(gè)系統(tǒng),一開(kāi)始就將這些狀態(tài)規(guī)劃好,自然是沒(méi)有問(wèn)題的,但是要在一個(gè)已經(jīng)很龐大的系統(tǒng)上去新添加一種系統(tǒng)性狀態(tài)規(guī)則,難度可想而知,成本很高不說(shuō),還很容易出現(xiàn)問(wèn)題,一旦狀態(tài)維護(hù)不好,又將是無(wú)休止的埋坑、踩坑、填坑的過(guò)程。

雖然想象起來(lái)很高大上,但我還是及時(shí)踩住了剎車(chē),簡(jiǎn)單的嘗試一下后就終止了該方案。

新的方案:DISLIKE事件的處理不再使用消息機(jī)制。將卡片組件、卡片、ListView、事件處理對(duì)象等,使用清晰的鏈條鏈接起來(lái)。因?yàn)檫@些對(duì)象是有關(guān)系鏈的,有明確的生命期管理關(guān)系(雖然有垃圾回收不用考慮何時(shí)銷(xiāo)毀)。我們完全可以使用委托模式(Delegate)在Owner中處理來(lái)自子孫產(chǎn)生的回調(diào)事件。我們的系統(tǒng)中本來(lái)就有UIEventDelegate接口設(shè)計(jì),跟消息機(jī)制的差別,主要是從ListView到卡片組件DeleteButton之間,必須要層層設(shè)置Delegate回調(diào)對(duì)象,事件發(fā)生時(shí)層層上報(bào)。Owner在創(chuàng)建子對(duì)象時(shí)調(diào)用obj.setEventHandler(this),任何對(duì)象在產(chǎn)生事件時(shí)要么自己消化,要么只能將事件回調(diào)給Owner處理,并不是通過(guò)消息一拋了之,唯一的缺點(diǎn)是構(gòu)造對(duì)象時(shí)傳遞Delegate回調(diào)接口略顯麻煩。不過(guò)這樣一來(lái),事件的傳遞是清晰的,誰(shuí)產(chǎn)生事件,誰(shuí)處理事件,流程是很清楚的,不會(huì)再有亂子發(fā)生,再不會(huì)有其它不相干對(duì)象收到事件的情況出現(xiàn)。

通過(guò)這種方案最后圓滿解決問(wèn)題,但是嘗試各種方案解決這個(gè)問(wèn)題的過(guò)程,我足足花了兩天時(shí)間,發(fā)現(xiàn)問(wèn)題、排查問(wèn)題、嘗試方案到解決問(wèn)題,整個(gè)過(guò)程遠(yuǎn)比我這里描述的要復(fù)雜的多。要知道之前實(shí)現(xiàn)彈框這一步需求一共才花了3天,其中還是因?yàn)樽隽艘粋€(gè)通用的Popover組件。那么,試問(wèn)這兩天解Bug的時(shí)間花的值得嗎?

總結(jié):事件傳遞應(yīng)該有明確清晰的鏈條,而不是隨意亂飛,能不能正確處理全看緣分。

像內(nèi)存報(bào)警、應(yīng)用換膚、多語(yǔ)言切換這些非常通用,而又不存在各業(yè)務(wù)間關(guān)聯(lián)的事件通知,使用廣播消息是非常方便的,也是合理的。但是一旦在復(fù)雜的業(yè)務(wù)邏輯中濫用消息機(jī)制尤其是廣播消息,其后果將是災(zāi)難性的。

作者:凡塵卍

鏈接:http://www.itdecent.cn/p/e3b5dbbee782

著作權(quán)歸作者所有。商業(yè)轉(zhuǎn)載請(qǐng)聯(lián)系作者獲得授權(quán),非商業(yè)轉(zhuǎn)載請(qǐng)注明出處。

最后編輯于
?著作權(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)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

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