作者|周吾昆
前言
近三年,抖音直播業(yè)務(wù)實(shí)現(xiàn)了爆發(fā)式增長(zhǎng),直播間的功能也增添了許多的可玩性。為了高效滿足業(yè)務(wù)快速迭代的訴求,抖音直播非常深度的使用了依賴注入架構(gòu)。
在軟件工程中,依賴注入(dependency injection)的意思為:給予調(diào)用方它所需要的事物。
“依賴”是指可被方法調(diào)用的事物。依賴注入形式下,調(diào)用方不再直接使用“依賴”,取而代之是“注入” 。
“注入”是指將“依賴”傳遞給調(diào)用方的過(guò)程。在“注入”之后,調(diào)用方才會(huì)調(diào)用該“依賴”。
傳遞依賴給調(diào)用方,而不是讓讓調(diào)用方直接獲得依賴,這個(gè)是該設(shè)計(jì)的根本需求。該設(shè)計(jì)的目的是為了分離調(diào)用方和依賴方,從而實(shí)現(xiàn)代碼的高內(nèi)聚低耦合,提高可讀性以及重用性。
本文試圖從原理入手,講清楚什么是依賴,什么是反轉(zhuǎn),依賴反轉(zhuǎn)與控制反轉(zhuǎn)的關(guān)系又是什么?一個(gè)依賴注入框架應(yīng)該具備哪些能力?抖音直播又是如何通過(guò)依賴注入優(yōu)雅的實(shí)現(xiàn)模塊間的解耦?通過(guò)對(duì)依賴注入架構(gòu)優(yōu)缺點(diǎn)的分析,能對(duì)其能有更全面的了解,為后續(xù)的架構(gòu)設(shè)計(jì)工作帶來(lái)更多的靈感。
什么是依賴
對(duì)象間依賴
面向?qū)ο笤O(shè)計(jì)及編程的基本思想,簡(jiǎn)單來(lái)說(shuō)就是把復(fù)雜系統(tǒng)分解成相互合作的對(duì)象,這些對(duì)象類通過(guò)封裝以后,內(nèi)部實(shí)現(xiàn)對(duì)外部是透明的,從而降低了解決問(wèn)題的復(fù)雜度,而且服務(wù)可以靈活地被重用和擴(kuò)展。而面向?qū)ο笤O(shè)計(jì)帶來(lái)的最直接的問(wèn)題,就是對(duì)象間的依賴。
我們舉一個(gè)開(kāi)發(fā)中最常見(jiàn)的例子:
在 A 類里用到 B 類的實(shí)例化構(gòu)造,就可以說(shuō) A 依賴于 B。軟件系統(tǒng)在沒(méi)有引入 IOC 容器之前,對(duì)象 A 依賴于對(duì)象 B,那么對(duì)象 A 在初始化或者運(yùn)行到某一點(diǎn)的時(shí)候,自己必須主動(dòng)去創(chuàng)建對(duì)象 B 或者使用已經(jīng)創(chuàng)建的對(duì)象 B。無(wú)論是創(chuàng)建還是使用對(duì)象 B,控制權(quán)都在 A 自己手上。
這個(gè)直接依賴會(huì)導(dǎo)致什么問(wèn)題?
過(guò)渡暴露細(xì)節(jié)
- A 只關(guān)心 B 提供的接口服務(wù),并不關(guān)心 B 的內(nèi)部實(shí)現(xiàn)細(xì)節(jié),A 因?yàn)橐蕾嚩?B 類,間接的關(guān)心了 B 的實(shí)現(xiàn)細(xì)節(jié)
對(duì)象間強(qiáng)耦合
- B 發(fā)生任何變化都會(huì)影響到 A,開(kāi)發(fā) A 和開(kāi)發(fā) B 的人可能不是一個(gè)人,B 把一個(gè) A 需要用到的方法參數(shù)改了,B 的修改能編譯通過(guò),能繼續(xù)用,但是 A 就跑不起來(lái)了
擴(kuò)展性差
- A 是服務(wù)使用者,B 是提供一個(gè)具體服務(wù)的,假如 C 也能提供類似服務(wù),但是 A 已經(jīng)嚴(yán)重依賴于 B 了,想換成 C 非常之困難
學(xué)過(guò)面向?qū)ο蟮耐瑢W(xué)馬上會(huì)知道可以使用接口來(lái)解決上面幾個(gè)問(wèn)題。如果早期實(shí)現(xiàn)類 B 的時(shí)候就定義了一個(gè)接口,B 和 C 都實(shí)現(xiàn)這個(gè)接口里的方法,這樣從 B 切換到 C 是不是就只需很小的改動(dòng)就可以完成。
A 對(duì) B 或 C 的依賴變成對(duì)抽象接口的依賴了,上面說(shuō)的幾個(gè)問(wèn)題都解決了。但是目前還是得實(shí)例化 B 或者 C,因?yàn)?new 只能 new 對(duì)象,不能 new 一個(gè)接口,還不能說(shuō) A 徹底只依賴于接口了。從 B 切換到 C 還是需要修改代碼,能做到更少的依賴嗎?能做到 A 在運(yùn)行的時(shí)候想切換 B 就 B,想切換 C 就 C,不用改任何代碼甚至還能支持以后切換成 D 嗎?
通過(guò)反射可以簡(jiǎn)單實(shí)現(xiàn)上面的訴求。例如常用的接口NSClassFromString,通過(guò)字符串可以轉(zhuǎn)換成同名的類。通過(guò)讀取本地的配置文件,或者服務(wù)端下發(fā)的數(shù)據(jù),通過(guò) OC 的提供的反射接口得到對(duì)應(yīng)的類,就可以做到運(yùn)行時(shí)動(dòng)態(tài)控制依賴對(duì)象的引入。
軟件系統(tǒng)的依賴
讓我們把視角放到更大的軟件系統(tǒng)中,這種依賴問(wèn)題會(huì)更加突出。
在面向?qū)ο笤O(shè)計(jì)的軟件系統(tǒng)中,它的底層通常都是由 N 個(gè)對(duì)象構(gòu)成的,各個(gè)對(duì)象或模塊之間通過(guò)相互合作,最終實(shí)現(xiàn)系統(tǒng)地業(yè)務(wù)邏輯。

如果我們打開(kāi)機(jī)械式手表的后蓋,就會(huì)看到與上面類似的情形,各個(gè)齒輪分別帶動(dòng)時(shí)針、分針和秒針順時(shí)針旋轉(zhuǎn),從而在表盤(pán)上產(chǎn)生正確的時(shí)間。
上圖描述的就是這樣的一個(gè)齒輪組,它擁有多個(gè)獨(dú)立的齒輪,這些齒輪相互嚙合在一起,協(xié)同工作,共同完成某項(xiàng)任務(wù)。我們可以看到,在這樣的齒輪組中,如果有一個(gè)齒輪出了問(wèn)題,就可能會(huì)影響到整個(gè)齒輪組的正常運(yùn)轉(zhuǎn)。
齒輪組中齒輪之間的嚙合關(guān)系,與軟件系統(tǒng)中對(duì)象之間的耦合關(guān)系非常相似。
對(duì)象之間的耦合關(guān)系是無(wú)法避免的,也是必要的,這是協(xié)同工作的基礎(chǔ)。功能越復(fù)雜的應(yīng)用,對(duì)象之間的依賴關(guān)系一般也越復(fù)雜,經(jīng)常會(huì)出現(xiàn)對(duì)象之間的多重依賴性關(guān)系,因此,架構(gòu)師對(duì)于系統(tǒng)的分析和設(shè)計(jì),將面臨更大的挑戰(zhàn)。對(duì)象之間耦合度過(guò)高的系統(tǒng),必然會(huì)出現(xiàn)牽一發(fā)而動(dòng)全身的情形。

耦合關(guān)系不僅會(huì)出現(xiàn)在對(duì)象與對(duì)象之間,也會(huì)出現(xiàn)在軟件系統(tǒng)的各模塊之間。如何降低系統(tǒng)之間、模塊之間和對(duì)象之間的耦合度,是軟件工程永遠(yuǎn)追求的目標(biāo)之一。
控制反轉(zhuǎn)
為了解決對(duì)象之間的耦合度過(guò)高的問(wèn)題,軟件專家 Michael Mattson 1996 年提出了 IOC 理論,用來(lái)實(shí)現(xiàn)對(duì)象之間的“解耦”,目前這個(gè)理論已經(jīng)被成功地應(yīng)用到實(shí)踐當(dāng)中。
1996 年,Michael Mattson 在一篇有關(guān)探討面向?qū)ο罂蚣艿奈恼轮校紫忍岢隽?IOC (Inversion of Control / 控制反轉(zhuǎn))這個(gè)概念。
IOC 理論提出的觀點(diǎn)大體為:借助于“第三方”實(shí)現(xiàn)具有依賴關(guān)系的對(duì)象之間的解耦。如下圖:

由于引進(jìn)了中間位置的“第三方”,也就是 IOC 容器,使得 A、B、C、D 這 4 個(gè)對(duì)象沒(méi)有了耦合關(guān)系,齒輪之間的傳動(dòng)全部依靠“第三方”了,全部對(duì)象的控制權(quán)全部上繳給“第三方”IOC 容器,所以,IOC 容器成了整個(gè)系統(tǒng)的關(guān)鍵核心,它起到了一種類似“粘合劑”的作用,把系統(tǒng)中的所有對(duì)象粘合在一起發(fā)揮作用,如果沒(méi)有這個(gè)“粘合劑”,對(duì)象與對(duì)象之間會(huì)彼此失去聯(lián)系,這就是有人把 IOC 容器比喻成“粘合劑”的由來(lái)。
我們?cè)賮?lái)做個(gè)試驗(yàn):把上圖中間的 IOC 容器拿掉,然后再來(lái)看看這套系統(tǒng):

我們現(xiàn)在看到的畫(huà)面,就是我們要實(shí)現(xiàn)整個(gè)系統(tǒng)所需要完成的全部?jī)?nèi)容。這時(shí)候,A、B、C、D 這 4 個(gè)對(duì)象之間已經(jīng)沒(méi)有了耦合關(guān)系,彼此毫無(wú)聯(lián)系,這樣的話,當(dāng)你在實(shí)現(xiàn) A 的時(shí)候,根本無(wú)須再去考慮 B、C 和 D 了,對(duì)象之間的依賴關(guān)系已經(jīng)降低到了最低程度。所以,如果真能實(shí)現(xiàn) IOC 容器,對(duì)于系統(tǒng)開(kāi)發(fā)而言,這將是一件多么美好的事情,參與開(kāi)發(fā)的每一成員只要實(shí)現(xiàn)自己的類就可以了,跟別人沒(méi)有任何關(guān)系!
軟件系統(tǒng)在引入 IOC 容器之后,對(duì)象間依賴的情況就完全改變了,由于 IOC 容器的加入,對(duì)象 A 與對(duì)象 B 之間失去了直接聯(lián)系,所以,當(dāng)對(duì)象 A 運(yùn)行到需要對(duì)象 B 的時(shí)候,IOC 容器會(huì)主動(dòng)創(chuàng)建一個(gè)對(duì)象 B 注入到對(duì)象 A 需要的地方
通過(guò)前后的對(duì)比,我們不難看出來(lái):對(duì)象 A 獲得依賴對(duì)象 B 的過(guò)程,由主動(dòng)行為變?yōu)榱吮粍?dòng)行為,控制權(quán)顛倒過(guò)來(lái)了,這就是“控制反轉(zhuǎn)”這個(gè)名稱的由來(lái)。
依賴反轉(zhuǎn)與控制反轉(zhuǎn)
沒(méi)有反轉(zhuǎn)
當(dāng)我們考慮如何去解決一個(gè)高層次的問(wèn)題的時(shí)候,我們會(huì)將其拆解成一系列更細(xì)節(jié)的較低層次的問(wèn)題,再將每個(gè)較低層次的問(wèn)題拆解為一系列更低層次的問(wèn)題,這就是業(yè)務(wù)邏輯(控制流)的走向,是「自頂向下」的設(shè)計(jì)。
如果按照這樣的拆解問(wèn)題的思路去組織我們的代碼,那么代碼架構(gòu)的走向也就和業(yè)務(wù)邏輯的走向一致了,也就是沒(méi)有反轉(zhuǎn)的情況。
沒(méi)有依賴反轉(zhuǎn)的情況下,系統(tǒng)行為決定了控制流,控制流決定了代碼的依賴關(guān)系
以抖音直播為例:直播有房間的概念,房間內(nèi)包含多個(gè)功能組件。對(duì)應(yīng)的,代碼里有一個(gè)房間服務(wù)的控制器類(如RoomController),一個(gè)組件管理的類(ComponentLoader),以及若干組件類(如紅包組件RedEnvelopeComponent 、禮物組件 GiftComponent)。
進(jìn)入直播房間時(shí),先創(chuàng)建房間控制器,控制器會(huì)創(chuàng)建組件管理類,接著組件管理類會(huì)初始化房間內(nèi)所有組件。這里的描述就是業(yè)務(wù)邏輯(控制流)的方向。
如果按照沒(méi)有反轉(zhuǎn)的情況,控制流和代碼依賴的示意圖如下:

無(wú)反轉(zhuǎn)偽代碼示例如下:
@implementation RoomController
- (void)viewDidLoad {
// 初始化房間服務(wù)
self.componentLoader = [[ComponentLoader alloc] init];
[self.componentLoader setupComponents];
}
@end
@implementation ComponentLoader
- (void)setupComponents {
// 初始化所有房間組件
ComponentA *a = [[ComponentA alloc] init];
ComponentB *b = [[ComponentB alloc] init];
ComponentC *c = [[ComponentC alloc] init];
self.components = @[a, b, c];
[a setup];
[b setup];
[c setup];
}
@end
@implementation ComponentA
- (void)setup {
}
@end
@implementation ComponentB
- (void)setup {
}
@end
@implementation ComponentC
- (void)setup {
}
@end
依賴反轉(zhuǎn)(DIP)
SOLID 原則之一:DIP(Dependency Inversion Principle)。這里的依賴指的是代碼層面的依賴,上層模塊不應(yīng)該依賴底層模塊,它們都應(yīng)該依賴于抽象(上層模塊定義并依賴抽象接口,底層模塊實(shí)現(xiàn)該接口)。
反轉(zhuǎn)指的是:反轉(zhuǎn)源代碼的依賴方向,使其與控制流的方向相反

依賴反轉(zhuǎn)代碼示例如下:
@protocol ComponentInterface
- (void)setup;
@end
@interface ComponentA <ComponentInterface>
@end
@interface ComponentB <ComponentInterface>
@end
@interface ComponentC <ComponentInterface>
@end
@implementation ComponentLoader
- (void)setModules {
// 初始化組件
ComponentA *a = [[ComponentA alloc] init];
ComponentB *b = [[ComponentB alloc] init];
ComponentC *c = [[ComponentC alloc] init];
self.components = @[a, b, c];
for (NSObject<ComponentInterface> *aComponent in self.components) {
[aComponent setup];
}
}
@end
這樣做有什么好處呢?
符合開(kāi)閉原則
- 接口通常比實(shí)現(xiàn)穩(wěn)定,因此可以使得高層模塊對(duì)修改封閉,對(duì)擴(kuò)展開(kāi)放。我們很容易去替換或擴(kuò)展新的底層實(shí)現(xiàn),而不用對(duì)高層模塊進(jìn)行修改
高內(nèi)聚低耦合
- 代碼不再受到控制流依賴的限制,利于插件化,組件化
舉個(gè)例子:Apple 的智能家居系統(tǒng)定義了 Homekit 接口,但沒(méi)有依賴于任何一款具體的 Homekit 產(chǎn)品。任何滿足 Homekit 接口的產(chǎn)品,都可以自由接入智能家居的系統(tǒng)中。
但 DIP 原則只是提供了架構(gòu)設(shè)計(jì)的原則,并沒(méi)有提供具體的實(shí)現(xiàn)措施。底層模塊由誰(shuí)來(lái)創(chuàng)建?如何創(chuàng)建?如何與高層模塊進(jìn)行注入和綁定?上面 ???? 藍(lán)色的箭頭如何處理?這就是 IoC 想要解決的問(wèn)題。
控制反轉(zhuǎn)(IoC )
這里的控制是指:一個(gè)類除了自己的本職工作以外的邏輯。典型的如創(chuàng)建其依賴的對(duì)象的邏輯。將這些控制邏輯移出這個(gè)類中,就稱為控制反轉(zhuǎn)。
那么這些邏輯由誰(shuí)來(lái)實(shí)現(xiàn)呢?各種框架、工廠類、IoC (Inversion of Control)容器等等該上場(chǎng)了……

一個(gè)類的實(shí)現(xiàn)需要依賴其他的類,那么其他類就是該類的依賴。依賴分兩部分:
- 創(chuàng)建對(duì)象時(shí)的依賴
- 使用對(duì)象時(shí)的依賴
上面依賴反轉(zhuǎn)的代碼示例中,使用對(duì)象時(shí)的依賴,實(shí)質(zhì)上已經(jīng)通過(guò)依賴反轉(zhuǎn)得到了解決(self.components 類型聲明的是 id< ComponentInterface >的對(duì)象,而不是依賴具體類的對(duì)象)。
但創(chuàng)建對(duì)象時(shí)的依賴的問(wèn)題仍然存在,ComponentLoader 內(nèi)部直接創(chuàng)建了對(duì)應(yīng)類的實(shí)例,因此依賴于 ComponentA,ComponentB,ComponentC 等具體的類。
如何解決創(chuàng)建對(duì)象時(shí)的依賴?把這個(gè)任務(wù)交給專業(yè)的人去做,由第三方進(jìn)行創(chuàng)建:如工廠,IoC 容器...
這里創(chuàng)建邏輯就發(fā)生了反轉(zhuǎn),即將「對(duì)象的創(chuàng)建」這一邏輯轉(zhuǎn)移到了第三方身上。
就好像 Apple 的 Homekit 不負(fù)責(zé)生產(chǎn)具體的產(chǎn)品,也不負(fù)責(zé)將這些產(chǎn)品接入到 Homekit 的系統(tǒng)中。誰(shuí)來(lái)做呢?生產(chǎn)產(chǎn)品是由具體產(chǎn)品的工廠來(lái)做,接入是由具體產(chǎn)品的工程師來(lái)做。

使用更通用的結(jié)構(gòu)圖表述:

這樣做有什么好處呢?
符合單一原則
- 該類只處理自己的本職工作,不負(fù)責(zé)其依賴對(duì)象的創(chuàng)建
高可測(cè)試性
- 通過(guò)接口,可以方便的進(jìn)行單元測(cè)試
提高類的穩(wěn)定性
- 減少了依賴的類,依賴的類出錯(cuò)不影響本類的正常編譯
依賴反轉(zhuǎn)與控制反轉(zhuǎn)的關(guān)系
依賴反轉(zhuǎn)(DIP)是設(shè)計(jì)原則,控制反轉(zhuǎn)(IoC)只是原則或模式,并沒(méi)有提供具體的實(shí)現(xiàn)措施??刂品崔D(zhuǎn)與依賴反轉(zhuǎn)沒(méi)有直接關(guān)系。
IoC 是 DIP 的實(shí)現(xiàn)嗎?我認(rèn)為不是的。它們分別描述了兩個(gè)方面的原則
- DIP 原則是「架構(gòu)設(shè)計(jì)方面」的原則,給不同模塊(高層模塊,底層模塊)應(yīng)當(dāng)如何設(shè)計(jì)提供了范式(對(duì)應(yīng)上圖中橙色的箭頭)
- IoC 原則是指導(dǎo)「代碼如何編寫(xiě)」的原則,是控制流(業(yè)務(wù)邏輯)如何實(shí)現(xiàn)方面的原則(對(duì)應(yīng)上圖中藍(lán)色的箭頭)
使用 IoC 原則,并不意味著一定會(huì)使用 DIP:
- 可以在任意地方使用其他方式(如工廠模式)進(jìn)行對(duì)象的創(chuàng)建,而不一定是在依賴對(duì)象與被依賴對(duì)象之間。
- IoC 原則也沒(méi)有規(guī)范創(chuàng)建對(duì)象的類型。通過(guò) IoC 容器創(chuàng)建出來(lái)的也可以是一個(gè)具體的類的實(shí)例,并不是依賴抽象接口的實(shí)現(xiàn),因而也不一定滿足 DIP 所要求的的「依賴于抽象」
同樣,使用 DIP 原則也不一定會(huì)使用 IoC:
- 參考依賴反轉(zhuǎn)章節(jié)中的代碼中,雖然 ComponentA,ComponentB,ComponentC 都使用 DIP 原則依賴了接口,但 a,b,c 的實(shí)例仍然是由 ComponentLoader 來(lái)創(chuàng)建的,因此并沒(méi)有控制反轉(zhuǎn)發(fā)生
但 IoC 可以和 DIP 一起使用,即,使用 IoC 來(lái)解決 DIP 中底層組件的創(chuàng)建和與高層組件的注入、綁定等問(wèn)題。這樣可以最大程度解決類耦合的問(wèn)題,得到一個(gè)純凈無(wú)污染的類。
依賴注入框架原理
依賴注入框架的能力
依賴注入是控制反轉(zhuǎn)( IoC)原則的一種具體實(shí)現(xiàn)方式,具體來(lái)說(shuō),是創(chuàng)建依賴對(duì)象反轉(zhuǎn)的實(shí)現(xiàn)方式之一。
依賴注入的目的,是為了將「依賴對(duì)象的創(chuàng)建」與「依賴對(duì)象的使用」分離,通俗講就是使用方不負(fù)責(zé)服務(wù)的創(chuàng)建。
依賴注入將對(duì)象的創(chuàng)建邏輯,轉(zhuǎn)移到了依賴注入框架中。一個(gè)類只需要定義自己的依賴,然后直接使用該依賴就可以,依賴注入框架負(fù)責(zé)創(chuàng)建、綁定、維護(hù)被依賴對(duì)象的生命周期。
一個(gè) DI 框架一般需要具備這些能力:
依賴關(guān)系的配置
- 被依賴的對(duì)象與其實(shí)現(xiàn)協(xié)議之間的映射關(guān)系
依賴對(duì)象生命周期的管理
- 注入對(duì)象的創(chuàng)建與銷毀
依賴對(duì)象的獲取
- 通過(guò)依賴對(duì)象綁定的協(xié)議,獲取到對(duì)應(yīng)的對(duì)象
依賴對(duì)象的注入
- 即被依賴的對(duì)象如何注入到使用者內(nèi)
下面就這四種能力分別展開(kāi)討論。
依賴關(guān)系配置
依賴關(guān)系的配置常見(jiàn)的有以下幾種方式:
- 編譯時(shí)配置
- 鏈接時(shí)配置
- +load 方法配置
- 運(yùn)行時(shí)配置
- 代碼配置
編譯時(shí)配置
既然是只需要一份配置關(guān)系,那么可以將該配置關(guān)系在編譯時(shí)寫(xiě)到 Mach-O 的 __DATA 段中,運(yùn)行時(shí)需要用到的時(shí)候進(jìn)行懶加載獲取即可。
寫(xiě)配置關(guān)系也有多種方法:
- 寫(xiě)入 protocol 和 Class 的映射關(guān)系,需要用的時(shí)候,根據(jù) protocol 讀出來(lái) Class,再進(jìn)行對(duì)象的創(chuàng)建
- 定義一個(gè)函數(shù),負(fù)責(zé)創(chuàng)建對(duì)象。寫(xiě)入的是 protocol 和該函數(shù)指針的映射關(guān)系。需要用的時(shí)候,根據(jù) protocol 讀出來(lái)函數(shù)指針,直接進(jìn)行調(diào)用
相關(guān)原理可以參考《一種延遲 premain code 的方法》。
鏈接時(shí)配置
也是將 protocol 和一個(gè)負(fù)責(zé)創(chuàng)建對(duì)象的函數(shù)進(jìn)行綁定。不同的是不需要綁定函數(shù)指針,只需要配置和使用的地方對(duì)齊函數(shù)名,再通過(guò) extern 進(jìn)行調(diào)用即可,其本質(zhì)是使用鏈接器完成了綁定的過(guò)程。
static inline id creator_testProtocol_imp(void) {
id<TestProtocol> imp = [[TestClass alloc] init];
return imp;
}
//實(shí)現(xiàn)
FOUNDATION_EXPORT id _di_provider_testProtocol(void) {
return creator_testProtocol_imp();
}
//使用
extern id _di_provider_testProtocol(void);
id<Protocol> obj = _di_provider_testProtocol();優(yōu)點(diǎn)
- 輕量,沒(méi)有注冊(cè),沒(méi)有運(yùn)行時(shí)的映射表
- 編譯期檢查比較完善,如果 Service 沒(méi)有實(shí)現(xiàn)創(chuàng)建對(duì)象的方法,那么在鏈接的時(shí)候因?yàn)?C 方法會(huì)找不到而報(bào)錯(cuò)
缺點(diǎn)
- 調(diào)用 C 方法之前必須 extern,所以在.m 的最開(kāi)始需要寫(xiě)對(duì)應(yīng)的宏
- 每個(gè) service 都需要實(shí)現(xiàn)對(duì)應(yīng)的創(chuàng)建對(duì)象方法,使用不友好
+load 方法配置
即在類的+load方法中進(jìn)行注冊(cè),將protocol與imp綁定。
+ (void)load {
BIND(protocol, imp);
}缺點(diǎn)
- load 在 main 函數(shù)之前執(zhí)行,影響啟動(dòng)速度,且崩潰后自研 Crash 監(jiān)控平臺(tái)捕獲不到
- load 方法執(zhí)行順序依賴于該類的鏈接順序,假如有其他使用者在 load 里獲取 Service,很可能獲取的時(shí)候 Service 還沒(méi)有注冊(cè)
- load 方法只執(zhí)行一次,在服務(wù)被銷毀后,無(wú)法提供重新注入能力
開(kāi)源 DI 框架 objection 就是使用的該原理實(shí)現(xiàn)的綁定。
運(yùn)行時(shí)配置
定義一個(gè) DIContainer,創(chuàng)建與 protocol 同名的分類,利用 category,將 protocol 與實(shí)現(xiàn)類的綁定關(guān)系寫(xiě)到 DIContainer 的方法列表里(分類方法里)。
以TestProtocol?為例,當(dāng)使用者通過(guò)調(diào)用 DIContainer 的prototypeObjectWithProtocol?:方法將 Protocol 作為參數(shù)傳入時(shí),會(huì)通過(guò)約定的provideTestProtocol方法,獲取對(duì)應(yīng)的實(shí)例對(duì)象。偽代碼如下:
@implementation DIContainer(TestProtocol)
//這里將TestProtocol與創(chuàng)建的TestClass的對(duì)象imp進(jìn)行了綁定
- (id<TestProtocol>)provideTestProtocol {
id<TestProtocol> imp = [[TestClass alloc] init];
return imp;
}
@end
@implementation DIContainer
//通過(guò)DIContainer的該方法獲取protocol對(duì)應(yīng)綁定的實(shí)例對(duì)象
- (id)prototypeObjectWithProtocol:(Protocol *)protocol {
id bean = [super prototypeObjectWithProtocol:protocol];
if (bean) {
return bean;
} else {
NSString *factoryMethodName = [NSString stringWithFormat:@"provide%@", NSStringFromProtocol(protocol)];
SEL factorySEL = NSSelectorFromString(factoryMethodName);
if ([self respondsToSelector:factorySEL]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
return [self performSelector:factorySEL];
#pragma clang diagnostic pop
} else {
return nil;
}
}
}
@end
抖音直播也使用了一樣的原理來(lái)實(shí)現(xiàn)依賴關(guān)系的配置,只不過(guò) provideXXX 方法不是寫(xiě)在分類里,而是寫(xiě)在對(duì)應(yīng)的協(xié)議實(shí)現(xiàn)者里面,不管是寫(xiě)在 category 里還是寫(xiě)在協(xié)議實(shí)現(xiàn)者類里,本質(zhì)上沒(méi)有區(qū)別,只不過(guò)是選擇集中式還是分散式管理。
代碼配置
代碼配置是一種靜態(tài)注冊(cè)方式,將配置關(guān)系的邏輯寫(xiě)到代碼中,運(yùn)行時(shí)在合適的時(shí)機(jī)進(jìn)行關(guān)系的配置。
在抖音直播的使用場(chǎng)景中,很多 Component 都是在特定時(shí)機(jī)將 self 與其實(shí)現(xiàn)的 protocol 進(jìn)行綁定。這就是屬于代碼配置的形式。
優(yōu)點(diǎn)
- 可以直觀的看到依賴關(guān)系
- 可以自由控制綁定時(shí)機(jī)與解綁時(shí)機(jī)
缺點(diǎn)
- 擴(kuò)展性較差,依賴關(guān)系變更時(shí),需要維護(hù)對(duì)應(yīng)的代碼
- 需要處理好配置的時(shí)機(jī),避免其他地方使用時(shí),還沒(méi)有進(jìn)行配置導(dǎo)致獲取不到對(duì)應(yīng)的服務(wù)
依賴對(duì)象生命周期管理
生命周期管理主要包括依賴對(duì)象的創(chuàng)建與銷毀。
依賴對(duì)象的創(chuàng)建
依賴對(duì)象被注入的前提,是需要先創(chuàng)建被依賴的對(duì)象。在完成依賴關(guān)系配置之后,就需要在適當(dāng)?shù)臅r(shí)機(jī)進(jìn)行依賴對(duì)象的創(chuàng)建。
按照上文的要將「依賴對(duì)象的創(chuàng)建」與「依賴對(duì)象的使用」分離的目的,那么依賴對(duì)象的創(chuàng)建就不能是依賴對(duì)象的使用者。
那么誰(shuí)來(lái)負(fù)責(zé)依賴對(duì)象的創(chuàng)建呢?通常有以下選擇:
- DI 容器負(fù)責(zé)創(chuàng)建
- 依賴對(duì)象管理者負(fù)責(zé)創(chuàng)建
DI 容器負(fù)責(zé)創(chuàng)建
DI 容器創(chuàng)建對(duì)象有兩種時(shí)機(jī)
- 隱式創(chuàng)建,DI 容器在某個(gè)特定時(shí)機(jī)創(chuàng)建對(duì)象,比如 DI 容器準(zhǔn)備完成的時(shí)候,會(huì)創(chuàng)建所有依賴注入的對(duì)象。這種方式使用方便,但會(huì)對(duì)整個(gè)代碼可讀性、可理解性產(chǎn)生較大的影響,同時(shí)也會(huì)帶來(lái)一些沒(méi)有必要的對(duì)象的創(chuàng)建(非常駐功能的使用需要觸發(fā)條件)。
- 懶加載,即在有使用者首次通過(guò) DI 容器獲取該對(duì)象的時(shí)候才進(jìn)行創(chuàng)建,這種方式會(huì)更友好。上文中編譯時(shí)配置、鏈接時(shí)配置、運(yùn)行時(shí)配置都屬于懶加載的方式。
管理者負(fù)責(zé)創(chuàng)建
通過(guò)專門(mén)的管理者來(lái)創(chuàng)建對(duì)象,被創(chuàng)建的對(duì)象,可以通過(guò) DI 容器提供的setObject:forKey方法,將對(duì)象存儲(chǔ)在 DI 容器的字典里,其中 key 一般為 protocol 對(duì)應(yīng)的字符串,value 為傳入的對(duì)象。
在抖音直播里,是由 ComponentLoader 來(lái)創(chuàng)建所有的 Component,然后在特定時(shí)機(jī)將 Component 與 protocol 進(jìn)行綁定,最終就是調(diào)用的 DI 容器的setObject:forKey方法。
依賴對(duì)象的銷毀
DI容器一般需要提供銷毀某個(gè)協(xié)議對(duì)應(yīng)的注入對(duì)象的接口,同時(shí)也應(yīng)該提供銷毀容器本身的接口。
例如在抖音直播中,從一個(gè)直播間切換到另一個(gè)直播間,上一個(gè)直播間的容器就應(yīng)該被銷毀。
依賴對(duì)象獲取
DI 容器一般維護(hù)了一個(gè) map,來(lái)存儲(chǔ) protocol 與 imp 之間的映射關(guān)系,并且會(huì)提供通過(guò) key 來(lái)獲取綁定對(duì)象的接口,這里的 key 一般就是 protocol 的字符串來(lái)充當(dāng)。而想要通過(guò) protocol 獲取到對(duì)應(yīng)的對(duì)象,前提是已經(jīng)創(chuàng)建了對(duì)應(yīng)的依賴對(duì)象,并且完成與 protocol 的綁定。
在 DI 容器隱式創(chuàng)建的情況下,首次進(jìn)行依賴對(duì)象獲取,會(huì)觸發(fā)對(duì)象的懶加載完成對(duì)象的創(chuàng)建。
依賴對(duì)象注入
假如對(duì)象 A 需要使用對(duì)象 B 的能力,如果實(shí)現(xiàn)這個(gè)過(guò)程?
一般有兩種方式,一種是直接在 A 里直接創(chuàng)建對(duì)象 B 并且使用它能力,另一種是通過(guò)注入的方式,將依賴對(duì)象 B 引入到對(duì)象 A 中再使用。
依賴注入通常有三種方式:
- 構(gòu)造方法注入
- 接口注入
- 取值注入
構(gòu)造方法注入
在一個(gè)類的構(gòu)造函數(shù)中,增加該類依賴的其他對(duì)象。
優(yōu)點(diǎn)
- 在構(gòu)造完成后,依賴對(duì)象就進(jìn)入了就緒狀態(tài),可以馬上使用
缺點(diǎn)
- 依賴對(duì)象比較多時(shí),構(gòu)造方法冗長(zhǎng),不夠優(yōu)雅,也不利于拓展,有一定的維護(hù)成本
@interface ComponentLoader
- (instancetype)initWithInjectComponent:(id<TestProtocol>)component;
@end
接口注入
通過(guò)定義一個(gè)注入依賴的接口,進(jìn)行依賴對(duì)象的注入。
缺點(diǎn):對(duì)象的注入時(shí)機(jī)不太可控,且中途外部能修改,存在隱藏風(fēng)險(xiǎn)。
@interface ComponentLoader
- (void)injectComponent:(id<TestProtocol>)component;
@end
取值注入
在使用依賴對(duì)象的地方通過(guò) DI 提供的接口,獲取依賴對(duì)象并直接使用。
通過(guò) DI 容器提供的接口,配合包裝的宏定義,我們可以輕松的獲取到對(duì)應(yīng)的依賴對(duì)象,但是如果一個(gè)類中在多處依賴了該對(duì)象,就會(huì)在多處存在 DI 的宏,代碼層面上增加了對(duì) DI 的依賴,因此可以把依賴對(duì)象聲明為屬性,并通過(guò) getter 方法對(duì)依賴對(duì)象的屬性進(jìn)行賦值。
其偽代碼如下:
@interface ComponentLoader
@property (nonatomic, strong) id<TestProtocol> component;
@end
@implementation ComponentLoader
//將屬性component與TestProtocol綁定的的依賴注入對(duì)象進(jìn)行關(guān)聯(lián)
XLink(component,TestProtocol)
//宏定義展開(kāi)后的代碼為
- (id<TestProtocol>)component {
return xlink_get_property(@protocol(TestProtocol), (NSObject *)_component, @component, (NSObject *)self);
}
@end
依賴注入在抖音直播中的應(yīng)用
抖音直播間將每個(gè)細(xì)分功能設(shè)計(jì)為一個(gè)組件,將功能相近或關(guān)聯(lián)較強(qiáng)的組件打包到同一個(gè)模塊,通過(guò)模塊化、組件化的設(shè)計(jì),來(lái)讓業(yè)務(wù)得到合理的粒度拆分。目前抖音直播設(shè)計(jì)有幾十個(gè)模塊,數(shù)百個(gè)組件。
抖音直播里的依賴主要指的是一個(gè)組件依賴另一個(gè)組件提供的能力,而依賴注入的使用主要也是解決組件間的耦合問(wèn)題。
組件的創(chuàng)建
在打開(kāi)抖音直播間時(shí),RoomController?會(huì)先創(chuàng)建一個(gè)ComponentLoader,ComponentLoader負(fù)責(zé)創(chuàng)建直播間中需要的組件。
如果一進(jìn)直播間就一股腦加載幾百個(gè)組件,一方面會(huì)因?yàn)樵O(shè)備性能瓶頸導(dǎo)致首屏體驗(yàn)慢,另一方面每個(gè)組件加載的耗時(shí)存在差異,展示的優(yōu)先級(jí)也有差別,同時(shí)加載必然帶來(lái)不好的用戶觀感體驗(yàn)。
因此針對(duì)這幾百個(gè)組件,設(shè)計(jì)了優(yōu)先級(jí)的劃分,按優(yōu)先級(jí)分批次進(jìn)行組件的創(chuàng)建與加載,來(lái)保障絲滑的首屏秒開(kāi)體驗(yàn)。
DI 容器隔離
依賴注入框架的本質(zhì)是一個(gè)單例來(lái)維護(hù)協(xié)議與實(shí)現(xiàn)協(xié)議的對(duì)象之間的映射關(guān)系,單例也就意味著全局獨(dú)一份。如果業(yè)務(wù)相對(duì)比較清晰,處理好注入對(duì)象的生命周期管理,使用單例來(lái)管理,清晰明了簡(jiǎn)單易用,也沒(méi)什么大問(wèn)題。但是在抖音直播這種大型的業(yè)務(wù)上面,業(yè)務(wù)場(chǎng)景過(guò)于復(fù)雜,單例帶來(lái)的維護(hù)成本也會(huì)顯著上升。
單例最致命的問(wèn)題是在于:所有服務(wù)都會(huì)注冊(cè)到同一個(gè)的 DI 容器中,若存在多個(gè)直播間,多直播間之間的服務(wù)很難做到優(yōu)雅的隔離。
例如直播間上下滑場(chǎng)景,滑動(dòng)過(guò)程中會(huì)同時(shí)存在兩個(gè)直播間,兩個(gè)直播間都存在禮物組件,這兩個(gè)禮物組件需要在同一個(gè) DI 容器中被管理。
同一容器中多直播間之間同類對(duì)象的區(qū)分管理,會(huì)帶來(lái)比較大的復(fù)雜度與維護(hù)成本。
由于抖音直播過(guò)早地、很深地依賴了依賴注入框架,當(dāng)發(fā)現(xiàn)它本身的限制性時(shí),已經(jīng)很難把原有框架替換掉,只能在原有功能基礎(chǔ)上進(jìn)行能力迭代。
最終的解決方案是:分層與隔離。我們?cè)O(shè)計(jì)了多層的 DI 容器來(lái)實(shí)現(xiàn)隔離
直播通用的服務(wù),注冊(cè)到 LiveDI 容器中,如配置下發(fā)服務(wù)、用戶信息服務(wù)等;
單個(gè)房間級(jí)別的服務(wù),注冊(cè)到 RoomDI 容器中,如一般的直播間內(nèi)組件(禮物、紅包等)。
通常情況下,同時(shí)只存在一個(gè) LiveDI 容器跟一個(gè) RoomDI 容器。
在直播間上下滑場(chǎng)景中,會(huì)同時(shí)存在兩個(gè) RoomDI 容器,這兩個(gè)容器之間實(shí)現(xiàn)互相隔離。如上一個(gè)直播間中的禮物組件與下一個(gè)新直播間的禮物組件是兩個(gè)獨(dú)立的對(duì)象,分別注冊(cè)在兩個(gè)獨(dú)立的 RoomDI 容器中,當(dāng)新直播間完全展示時(shí),消失直播間的 RoomDI 容器就會(huì)被銷毀,其內(nèi)維護(hù)的組件便也一并跟著釋放。
通過(guò)這種多容器的設(shè)計(jì),實(shí)現(xiàn)了不同直播間的隔離。
依賴注入的優(yōu)缺點(diǎn)
優(yōu)點(diǎn)
穩(wěn)定性好
- 兩個(gè)存在依賴關(guān)系的對(duì)象,只有在真正使用時(shí),兩者才發(fā)生聯(lián)系。所以,無(wú)論兩者中的任何一方出現(xiàn)什么的問(wèn)題,都可以做到不影響另一方的開(kāi)發(fā)與調(diào)試。例如心愿單模塊依賴了手勢(shì)模塊,在手勢(shì)模塊存在代碼問(wèn)題無(wú)法運(yùn)行時(shí),可以通過(guò)修改配置去掉手勢(shì)模塊的加載,心愿模塊依然能正常運(yùn)行,只是涉及到手勢(shì)相關(guān)功能存在問(wèn)題而已,不會(huì)影響到整體的開(kāi)發(fā)調(diào)試。
可維護(hù)性好
- 代碼中的每一個(gè) Class 都可以單獨(dú)測(cè)試,彼此之間互不影響,只要保證自身的功能無(wú)誤即可。我們可以通過(guò) mock 依賴對(duì)象來(lái)代替需要注入的對(duì)象,進(jìn)行單元測(cè)試。
耦合性低,開(kāi)發(fā)提效
- 各個(gè)模塊間解耦后,只需要定義并遵守相應(yīng)的協(xié)議,無(wú)需關(guān)心其他模塊的細(xì)節(jié)。每個(gè)開(kāi)發(fā)團(tuán)隊(duì)的成員都只需要關(guān)心實(shí)現(xiàn)自身的業(yè)務(wù)邏輯,完全不用去關(guān)心其它的人工作進(jìn)展,因?yàn)槟愕娜蝿?wù)跟別人沒(méi)有任何關(guān)系,你的任務(wù)可以單獨(dú)測(cè)試,你的任務(wù)也不用依賴于別人的組件,再也不用扯不清責(zé)任了。所以,在一個(gè)大中型項(xiàng)目中,團(tuán)隊(duì)成員分工明確、責(zé)任明晰,很容易將一個(gè)大的任務(wù)劃分為細(xì)小的任務(wù),開(kāi)發(fā)效率和產(chǎn)品質(zhì)量必將得到大幅度的提高。
復(fù)用性好
- 我們可以把具有普遍性的常用組件獨(dú)立出來(lái),反復(fù)利用到項(xiàng)目中的其它部分,或者是其它項(xiàng)目,當(dāng)然這也是面向?qū)ο蟮幕咎卣?。顯然,IOC 不僅更好地貫徹了這個(gè)原則,提高了模塊的可復(fù)用性。符合接口標(biāo)準(zhǔn)的實(shí)現(xiàn),都可以插接到支持此標(biāo)準(zhǔn)的模塊中。
支持熱插拔
- IOC 生成對(duì)象的方式轉(zhuǎn)為外置方式,也就是把對(duì)象生成放在配置文件里進(jìn)行定義,這樣,當(dāng)我們?cè)鰟h一個(gè)模塊,或者更換一個(gè)實(shí)現(xiàn)子類,將會(huì)變得很簡(jiǎn)單,只要修改配置文件就可以了,完全具有熱插撥的特性。
缺點(diǎn)
使用 IOC 框架產(chǎn)品能夠給我們的開(kāi)發(fā)過(guò)程帶來(lái)很大的好處,但是也要充分認(rèn)識(shí)引入 IOC 框架的缺點(diǎn),做到心中有數(shù),杜絕濫用框架。
提高了上手成本
- 軟件系統(tǒng)中由于引入了第三方 IOC 容器,生成對(duì)象的步驟變得有些復(fù)雜,本來(lái)是兩者之間的事情,又憑空多出一道手續(xù),所以,我們?cè)趧傞_(kāi)始使用 IOC 框架的時(shí)候,會(huì)感覺(jué)系統(tǒng)變得不太直觀。所以,引入了一個(gè)全新的框架,就會(huì)增加團(tuán)隊(duì)成員學(xué)習(xí)和認(rèn)識(shí)的培訓(xùn)成本,并且在以后的運(yùn)行維護(hù)中,還得讓新同學(xué)具備同樣的知識(shí)體系。
引入不成熟框架帶來(lái)風(fēng)險(xiǎn)
- IOC 框架產(chǎn)品本身的成熟度需要進(jìn)行評(píng)估,如果引入一個(gè)不成熟的 IOC 框架產(chǎn)品,那么會(huì)影響到整個(gè)項(xiàng)目,所以這也是一個(gè)隱性的風(fēng)險(xiǎn)。例如直播中臺(tái)很早就引入了 IESDI 庫(kù),但是 IESDI 存在一些能力缺陷,比如做不到不同 DI 容器之間的隔離。因?yàn)?IESDI 在項(xiàng)目中被深度使用,如果換 DI 框架會(huì)帶來(lái)涉及面很廣的改動(dòng),風(fēng)險(xiǎn)不可控,所以只能基于現(xiàn)有框架進(jìn)行修補(bǔ),因此也會(huì)帶來(lái)一些比較 trick 的邏輯。
對(duì)運(yùn)行效率帶來(lái)一定影響
- 由于 IOC 容器生成對(duì)象有些是通過(guò)反射的方式,在運(yùn)行效率上有一定的損耗。如果要追求運(yùn)行效率的話,就必須對(duì)此進(jìn)行權(quán)衡。
通過(guò)對(duì)優(yōu)缺點(diǎn)的分析,我們大體可以得出這樣的結(jié)論:
一些工作量不大的項(xiàng)目或者產(chǎn)品,不太適合使用 IOC 框架產(chǎn)品。另外,如果團(tuán)隊(duì)成員的知識(shí)能力欠缺,對(duì)于 IOC 框架產(chǎn)品缺乏深入的理解,也不要貿(mào)然引入,可能會(huì)帶來(lái)額外的風(fēng)險(xiǎn)與成本。
但如果你經(jīng)歷的是一個(gè)復(fù)雜度較高的項(xiàng)目,需要通過(guò)組件化、模塊化等形式來(lái)降低耦合,提高開(kāi)發(fā)效率,那么依賴注入就值得被納入考慮范圍,或許你會(huì)得到不一樣的開(kāi)發(fā)體驗(yàn)。
結(jié)語(yǔ)
得益于依賴注入框 架強(qiáng)大的解耦能力,在實(shí)現(xiàn)抖音直播間這種復(fù)雜的功能聚合型頁(yè)面時(shí),仍然能保持高效的組織協(xié)作與模塊分工,為高質(zhì)量的業(yè)務(wù)迭代與追求極致的用戶體驗(yàn)提供穩(wěn)固的基礎(chǔ)技術(shù)基石。在抖音如此龐大的 APP 里做架構(gòu)層面的重構(gòu),任何風(fēng)吹草動(dòng)都可能傷筋動(dòng)骨,這就要求我們?cè)谧黾軜?gòu)設(shè)計(jì)時(shí),多抬頭看看前方的路。我們不提倡過(guò)度設(shè)計(jì),但是時(shí)刻保持思考,始終創(chuàng)業(yè),才能讓架構(gòu)伴隨業(yè)務(wù)一起成長(zhǎng),共述華章。?



























