自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

深入淺出依賴注入及其在抖音直播中的應用

原創(chuàng) 精選
移動開發(fā)
本文試圖從原理入手,講清楚什么是依賴,什么是反轉,依賴反轉與控制反轉的關系又是什么?一個依賴注入框架應該具備哪些能力?抖音直播又是如何通過依賴注入優(yōu)雅的實現模塊間的解耦?通過對依賴注入架構優(yōu)缺點的分析,能對其能有更全面的了解,為后續(xù)的架構設計工作帶來更多的靈感。

作者|周吾昆

前言

近三年,抖音直播業(yè)務實現了爆發(fā)式增長,直播間的功能也增添了許多的可玩性。為了高效滿足業(yè)務快速迭代的訴求,抖音直播非常深度的使用了依賴注入架構。

在軟件工程中,依賴注入(dependency injection)的意思為:給予調用方它所需要的事物。

“依賴”是指可被方法調用的事物。依賴注入形式下,調用方不再直接使用“依賴”,取而代之是“注入” 。

“注入”是指將“依賴”傳遞給調用方的過程。在“注入”之后,調用方才會調用該“依賴”。

傳遞依賴給調用方,而不是讓讓調用方直接獲得依賴,這個是該設計的根本需求。該設計的目的是為了分離調用方和依賴方,從而實現代碼的高內聚低耦合,提高可讀性以及重用性。

本文試圖從原理入手,講清楚什么是依賴,什么是反轉,依賴反轉與控制反轉的關系又是什么?一個依賴注入框架應該具備哪些能力?抖音直播又是如何通過依賴注入優(yōu)雅的實現模塊間的解耦?通過對依賴注入架構優(yōu)缺點的分析,能對其能有更全面的了解,為后續(xù)的架構設計工作帶來更多的靈感。

什么是依賴

對象間依賴

面向對象設計及編程的基本思想,簡單來說就是把復雜系統分解成相互合作的對象,這些對象類通過封裝以后,內部實現對外部是透明的,從而降低了解決問題的復雜度,而且服務可以靈活地被重用和擴展。而面向對象設計帶來的最直接的問題,就是對象間的依賴。

我們舉一個開發(fā)中最常見的例子:

在 A 類里用到 B 類的實例化構造,就可以說 A 依賴于 B。軟件系統在沒有引入 IOC 容器之前,對象 A 依賴于對象 B,那么對象 A 在初始化或者運行到某一點的時候,自己必須主動去創(chuàng)建對象 B 或者使用已經創(chuàng)建的對象 B。無論是創(chuàng)建還是使用對象 B,控制權都在 A 自己手上。

這個直接依賴會導致什么問題?

過渡暴露細節(jié)

  • A 只關心 B 提供的接口服務,并不關心 B 的內部實現細節(jié),A 因為依賴而引入 B 類,間接的關心了 B 的實現細節(jié)

對象間強耦合

  • B 發(fā)生任何變化都會影響到 A,開發(fā) A 和開發(fā) B 的人可能不是一個人,B 把一個 A 需要用到的方法參數改了,B 的修改能編譯通過,能繼續(xù)用,但是 A 就跑不起來了

擴展性差

  • A 是服務使用者,B 是提供一個具體服務的,假如 C 也能提供類似服務,但是 A 已經嚴重依賴于 B 了,想換成 C 非常之困難

學過面向對象的同學馬上會知道可以使用接口來解決上面幾個問題。如果早期實現類 B 的時候就定義了一個接口,B 和 C 都實現這個接口里的方法,這樣從 B 切換到 C 是不是就只需很小的改動就可以完成。

A 對 B 或 C 的依賴變成對抽象接口的依賴了,上面說的幾個問題都解決了。但是目前還是得實例化 B 或者 C,因為 new 只能 new 對象,不能 new 一個接口,還不能說 A 徹底只依賴于接口了。從 B 切換到 C 還是需要修改代碼,能做到更少的依賴嗎?能做到 A 在運行的時候想切換 B 就 B,想切換 C 就 C,不用改任何代碼甚至還能支持以后切換成 D 嗎?

通過反射可以簡單實現上面的訴求。例如常用的接口NSClassFromString,通過字符串可以轉換成同名的類。通過讀取本地的配置文件,或者服務端下發(fā)的數據,通過 OC 的提供的反射接口得到對應的類,就可以做到運行時動態(tài)控制依賴對象的引入。

軟件系統的依賴

讓我們把視角放到更大的軟件系統中,這種依賴問題會更加突出。

在面向對象設計的軟件系統中,它的底層通常都是由 N 個對象構成的,各個對象或模塊之間通過相互合作,最終實現系統地業(yè)務邏輯。

圖片

如果我們打開機械式手表的后蓋,就會看到與上面類似的情形,各個齒輪分別帶動時針、分針和秒針順時針旋轉,從而在表盤上產生正確的時間。

上圖描述的就是這樣的一個齒輪組,它擁有多個獨立的齒輪,這些齒輪相互嚙合在一起,協同工作,共同完成某項任務。我們可以看到,在這樣的齒輪組中,如果有一個齒輪出了問題,就可能會影響到整個齒輪組的正常運轉。

齒輪組中齒輪之間的嚙合關系,與軟件系統中對象之間的耦合關系非常相似。

對象之間的耦合關系是無法避免的,也是必要的,這是協同工作的基礎。功能越復雜的應用,對象之間的依賴關系一般也越復雜,經常會出現對象之間的多重依賴性關系,因此,架構師對于系統的分析和設計,將面臨更大的挑戰(zhàn)。對象之間耦合度過高的系統,必然會出現牽一發(fā)而動全身的情形。

圖片

耦合關系不僅會出現在對象與對象之間,也會出現在軟件系統的各模塊之間。如何降低系統之間、模塊之間和對象之間的耦合度,是軟件工程永遠追求的目標之一。

控制反轉

為了解決對象之間的耦合度過高的問題,軟件專家 Michael Mattson 1996 年提出了 IOC 理論,用來實現對象之間的“解耦”,目前這個理論已經被成功地應用到實踐當中。

1996 年,Michael Mattson 在一篇有關探討面向對象框架的文章中,首先提出了 IOC (Inversion of Control / 控制反轉)這個概念。

IOC 理論提出的觀點大體為:借助于“第三方”實現具有依賴關系的對象之間的解耦。如下圖:

圖片

由于引進了中間位置的“第三方”,也就是 IOC 容器,使得 A、B、C、D 這 4 個對象沒有了耦合關系,齒輪之間的傳動全部依靠“第三方”了,全部對象的控制權全部上繳給“第三方”IOC 容器,所以,IOC 容器成了整個系統的關鍵核心,它起到了一種類似“粘合劑”的作用,把系統中的所有對象粘合在一起發(fā)揮作用,如果沒有這個“粘合劑”,對象與對象之間會彼此失去聯系,這就是有人把 IOC 容器比喻成“粘合劑”的由來。

我們再來做個試驗:把上圖中間的 IOC 容器拿掉,然后再來看看這套系統:

圖片

我們現在看到的畫面,就是我們要實現整個系統所需要完成的全部內容。這時候,A、B、C、D 這 4 個對象之間已經沒有了耦合關系,彼此毫無聯系,這樣的話,當你在實現 A 的時候,根本無須再去考慮 B、C 和 D 了,對象之間的依賴關系已經降低到了最低程度。所以,如果真能實現 IOC 容器,對于系統開發(fā)而言,這將是一件多么美好的事情,參與開發(fā)的每一成員只要實現自己的類就可以了,跟別人沒有任何關系!

軟件系統在引入 IOC 容器之后,對象間依賴的情況就完全改變了,由于 IOC 容器的加入,對象 A 與對象 B 之間失去了直接聯系,所以,當對象 A 運行到需要對象 B 的時候,IOC 容器會主動創(chuàng)建一個對象 B 注入到對象 A 需要的地方

通過前后的對比,我們不難看出來:對象 A 獲得依賴對象 B 的過程,由主動行為變?yōu)榱吮粍有袨?,控制權顛倒過來了,這就是“控制反轉”這個名稱的由來。

依賴反轉與控制反轉

沒有反轉

當我們考慮如何去解決一個高層次的問題的時候,我們會將其拆解成一系列更細節(jié)的較低層次的問題,再將每個較低層次的問題拆解為一系列更低層次的問題,這就是業(yè)務邏輯(控制流)的走向,是「自頂向下」的設計。

如果按照這樣的拆解問題的思路去組織我們的代碼,那么代碼架構的走向也就和業(yè)務邏輯的走向一致了,也就是沒有反轉的情況。

沒有依賴反轉的情況下,系統行為決定了控制流,控制流決定了代碼的依賴關系

以抖音直播為例:直播有房間的概念,房間內包含多個功能組件。對應的,代碼里有一個房間服務的控制器類(如RoomController),一個組件管理的類(ComponentLoader),以及若干組件類(如紅包組件RedEnvelopeComponent 、禮物組件 GiftComponent)。

進入直播房間時,先創(chuàng)建房間控制器,控制器會創(chuàng)建組件管理類,接著組件管理類會初始化房間內所有組件。這里的描述就是業(yè)務邏輯(控制流)的方向。

如果按照沒有反轉的情況,控制流和代碼依賴的示意圖如下:

圖片

無反轉偽代碼示例如下:

@implementation RoomController

- (void)viewDidLoad {
// 初始化房間服務
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

依賴反轉(DIP)

SOLID 原則之一:DIP(Dependency Inversion Principle)。這里的依賴指的是代碼層面的依賴,上層模塊不應該依賴底層模塊,它們都應該依賴于抽象(上層模塊定義并依賴抽象接口,底層模塊實現該接口)。

反轉指的是:反轉源代碼的依賴方向,使其與控制流的方向相反

圖片

依賴反轉代碼示例如下:

@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

這樣做有什么好處呢?

符合開閉原則

  • 接口通常比實現穩(wěn)定,因此可以使得高層模塊對修改封閉,對擴展開放。我們很容易去替換或擴展新的底層實現,而不用對高層模塊進行修改

高內聚低耦合

  • 代碼不再受到控制流依賴的限制,利于插件化,組件化

舉個例子:Apple 的智能家居系統定義了 Homekit 接口,但沒有依賴于任何一款具體的 Homekit 產品。任何滿足 Homekit 接口的產品,都可以自由接入智能家居的系統中。

但 DIP 原則只是提供了架構設計的原則,并沒有提供具體的實現措施。底層模塊由誰來創(chuàng)建?如何創(chuàng)建?如何與高層模塊進行注入和綁定?上面 ???? 藍色的箭頭如何處理?這就是 IoC 想要解決的問題。

控制反轉(IoC )

這里的控制是指:一個類除了自己的本職工作以外的邏輯。典型的如創(chuàng)建其依賴的對象的邏輯。將這些控制邏輯移出這個類中,就稱為控制反轉。

那么這些邏輯由誰來實現呢?各種框架、工廠類、IoC (Inversion of Control)容器等等該上場了……

圖片

一個類的實現需要依賴其他的類,那么其他類就是該類的依賴。依賴分兩部分:

  • 創(chuàng)建對象時的依賴
  • 使用對象時的依賴

上面依賴反轉的代碼示例中,使用對象時的依賴,實質上已經通過依賴反轉得到了解決(self.components 類型聲明的是 id< ComponentInterface >的對象,而不是依賴具體類的對象)。

但創(chuàng)建對象時的依賴的問題仍然存在,ComponentLoader 內部直接創(chuàng)建了對應類的實例,因此依賴于 ComponentA,ComponentB,ComponentC 等具體的類。

如何解決創(chuàng)建對象時的依賴?把這個任務交給專業(yè)的人去做,由第三方進行創(chuàng)建:如工廠,IoC 容器...

這里創(chuàng)建邏輯就發(fā)生了反轉,即將「對象的創(chuàng)建」這一邏輯轉移到了第三方身上。

就好像 Apple 的 Homekit 不負責生產具體的產品,也不負責將這些產品接入到 Homekit 的系統中。誰來做呢?生產產品是由具體產品的工廠來做,接入是由具體產品的工程師來做。

圖片

使用更通用的結構圖表述:

圖片

這樣做有什么好處呢?

符合單一原則

  • 該類只處理自己的本職工作,不負責其依賴對象的創(chuàng)建

高可測試性

  • 通過接口,可以方便的進行單元測試

提高類的穩(wěn)定性

  • 減少了依賴的類,依賴的類出錯不影響本類的正常編譯

依賴反轉與控制反轉的關系

依賴反轉(DIP)是設計原則,控制反轉(IoC)只是原則或模式,并沒有提供具體的實現措施??刂品崔D與依賴反轉沒有直接關系。

IoC 是 DIP 的實現嗎?我認為不是的。它們分別描述了兩個方面的原則

  • DIP 原則是「架構設計方面」的原則,給不同模塊(高層模塊,底層模塊)應當如何設計提供了范式(對應上圖中橙色的箭頭)
  • IoC 原則是指導「代碼如何編寫」的原則,是控制流(業(yè)務邏輯)如何實現方面的原則(對應上圖中藍色的箭頭)

使用 IoC 原則,并不意味著一定會使用 DIP:

  • 可以在任意地方使用其他方式(如工廠模式)進行對象的創(chuàng)建,而不一定是在依賴對象與被依賴對象之間。
  • IoC 原則也沒有規(guī)范創(chuàng)建對象的類型。通過 IoC 容器創(chuàng)建出來的也可以是一個具體的類的實例,并不是依賴抽象接口的實現,因而也不一定滿足 DIP 所要求的的「依賴于抽象」

同樣,使用 DIP 原則也不一定會使用 IoC:

  • 參考依賴反轉章節(jié)中的代碼中,雖然 ComponentA,ComponentB,ComponentC 都使用 DIP 原則依賴了接口,但 a,b,c 的實例仍然是由 ComponentLoader 來創(chuàng)建的,因此并沒有控制反轉發(fā)生

但 IoC 可以和 DIP 一起使用,即,使用 IoC 來解決 DIP 中底層組件的創(chuàng)建和與高層組件的注入、綁定等問題。這樣可以最大程度解決類耦合的問題,得到一個純凈無污染的類。

依賴注入框架原理

依賴注入框架的能力

依賴注入是控制反轉( IoC)原則的一種具體實現方式,具體來說,是創(chuàng)建依賴對象反轉的實現方式之一。

依賴注入的目的,是為了將「依賴對象的創(chuàng)建」與「依賴對象的使用」分離,通俗講就是使用方不負責服務的創(chuàng)建。

依賴注入將對象的創(chuàng)建邏輯,轉移到了依賴注入框架中。一個類只需要定義自己的依賴,然后直接使用該依賴就可以,依賴注入框架負責創(chuàng)建、綁定、維護被依賴對象的生命周期。

一個 DI 框架一般需要具備這些能力:

依賴關系的配置

  • 被依賴的對象與其實現協議之間的映射關系

依賴對象生命周期的管理

  • 注入對象的創(chuàng)建與銷毀

依賴對象的獲取

  • 通過依賴對象綁定的協議,獲取到對應的對象

依賴對象的注入

  • 即被依賴的對象如何注入到使用者內

下面就這四種能力分別展開討論。

依賴關系配置

依賴關系的配置常見的有以下幾種方式:

  • 編譯時配置
  • 鏈接時配置
  • +load 方法配置
  • 運行時配置
  • 代碼配置

編譯時配置

既然是只需要一份配置關系,那么可以將該配置關系在編譯時寫到 Mach-O 的 __DATA 段中,運行時需要用到的時候進行懶加載獲取即可。

寫配置關系也有多種方法:

  1. 寫入 protocol 和 Class 的映射關系,需要用的時候,根據 protocol 讀出來 Class,再進行對象的創(chuàng)建
  2. 定義一個函數,負責創(chuàng)建對象。寫入的是 protocol 和該函數指針的映射關系。需要用的時候,根據 protocol 讀出來函數指針,直接進行調用

相關原理可以參考《一種延遲 premain code 的方法》。

鏈接時配置

也是將 protocol 和一個負責創(chuàng)建對象的函數進行綁定。不同的是不需要綁定函數指針,只需要配置和使用的地方對齊函數名,再通過 extern 進行調用即可,其本質是使用鏈接器完成了綁定的過程。

static inline id creator_testProtocol_imp(void) {
id<TestProtocol> imp = [[TestClass alloc] init];
return imp;
}

//實現
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)點

  • 輕量,沒有注冊,沒有運行時的映射表
  • 編譯期檢查比較完善,如果 Service 沒有實現創(chuàng)建對象的方法,那么在鏈接的時候因為 C 方法會找不到而報錯

缺點

  • 調用 C 方法之前必須 extern,所以在.m 的最開始需要寫對應的宏
  • 每個 service 都需要實現對應的創(chuàng)建對象方法,使用不友好

+load 方法配置

即在類的+load方法中進行注冊,將protocol與imp綁定。

+ (void)load {
BIND(protocol, imp);
}

缺點

  • load 在 main 函數之前執(zhí)行,影響啟動速度,且崩潰后自研 Crash 監(jiān)控平臺捕獲不到
  • load 方法執(zhí)行順序依賴于該類的鏈接順序,假如有其他使用者在 load 里獲取 Service,很可能獲取的時候 Service 還沒有注冊
  • load 方法只執(zhí)行一次,在服務被銷毀后,無法提供重新注入能力

開源 DI 框架 objection 就是使用的該原理實現的綁定。

運行時配置

定義一個 DIContainer,創(chuàng)建與 protocol 同名的分類,利用 category,將 protocol 與實現類的綁定關系寫到 DIContainer 的方法列表里(分類方法里)。

以TestProtocol?為例,當使用者通過調用 DIContainer 的prototypeObjectWithProtocol?:方法將 Protocol 作為參數傳入時,會通過約定的provideTestProtocol方法,獲取對應的實例對象。偽代碼如下:

@implementation DIContainer(TestProtocol)

//這里將TestProtocol與創(chuàng)建的TestClass的對象imp進行了綁定
- (id<TestProtocol>)provideTestProtocol {
id<TestProtocol> imp = [[TestClass alloc] init];
return imp;
}

@end

@implementation DIContainer
//通過DIContainer的該方法獲取protocol對應綁定的實例對象
- (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

抖音直播也使用了一樣的原理來實現依賴關系的配置,只不過 provideXXX 方法不是寫在分類里,而是寫在對應的協議實現者里面,不管是寫在 category 里還是寫在協議實現者類里,本質上沒有區(qū)別,只不過是選擇集中式還是分散式管理。

代碼配置

代碼配置是一種靜態(tài)注冊方式,將配置關系的邏輯寫到代碼中,運行時在合適的時機進行關系的配置。

在抖音直播的使用場景中,很多 Component 都是在特定時機將 self 與其實現的 protocol 進行綁定。這就是屬于代碼配置的形式。

優(yōu)點

  • 可以直觀的看到依賴關系
  • 可以自由控制綁定時機與解綁時機

缺點

  • 擴展性較差,依賴關系變更時,需要維護對應的代碼
  • 需要處理好配置的時機,避免其他地方使用時,還沒有進行配置導致獲取不到對應的服務

依賴對象生命周期管理

生命周期管理主要包括依賴對象的創(chuàng)建與銷毀。

依賴對象的創(chuàng)建

依賴對象被注入的前提,是需要先創(chuàng)建被依賴的對象。在完成依賴關系配置之后,就需要在適當的時機進行依賴對象的創(chuàng)建。

按照上文的要將「依賴對象的創(chuàng)建」與「依賴對象的使用」分離的目的,那么依賴對象的創(chuàng)建就不能是依賴對象的使用者。

那么誰來負責依賴對象的創(chuàng)建呢?通常有以下選擇:

  • DI 容器負責創(chuàng)建
  • 依賴對象管理者負責創(chuàng)建

DI 容器負責創(chuàng)建

DI 容器創(chuàng)建對象有兩種時機

  • 隱式創(chuàng)建,DI 容器在某個特定時機創(chuàng)建對象,比如 DI 容器準備完成的時候,會創(chuàng)建所有依賴注入的對象。這種方式使用方便,但會對整個代碼可讀性、可理解性產生較大的影響,同時也會帶來一些沒有必要的對象的創(chuàng)建(非常駐功能的使用需要觸發(fā)條件)。
  • 懶加載,即在有使用者首次通過 DI 容器獲取該對象的時候才進行創(chuàng)建,這種方式會更友好。上文中編譯時配置、鏈接時配置、運行時配置都屬于懶加載的方式。

管理者負責創(chuàng)建

通過專門的管理者來創(chuàng)建對象,被創(chuàng)建的對象,可以通過 DI 容器提供的setObject:forKey方法,將對象存儲在 DI 容器的字典里,其中 key 一般為 protocol 對應的字符串,value 為傳入的對象。

在抖音直播里,是由 ComponentLoader 來創(chuàng)建所有的 Component,然后在特定時機將 Component 與 protocol 進行綁定,最終就是調用的 DI 容器的setObject:forKey方法。

依賴對象的銷毀

DI容器一般需要提供銷毀某個協議對應的注入對象的接口,同時也應該提供銷毀容器本身的接口。

例如在抖音直播中,從一個直播間切換到另一個直播間,上一個直播間的容器就應該被銷毀。

依賴對象獲取

DI 容器一般維護了一個 map,來存儲 protocol 與 imp 之間的映射關系,并且會提供通過 key 來獲取綁定對象的接口,這里的 key 一般就是 protocol 的字符串來充當。而想要通過 protocol 獲取到對應的對象,前提是已經創(chuàng)建了對應的依賴對象,并且完成與 protocol 的綁定。

在 DI 容器隱式創(chuàng)建的情況下,首次進行依賴對象獲取,會觸發(fā)對象的懶加載完成對象的創(chuàng)建。

依賴對象注入

假如對象 A 需要使用對象 B 的能力,如果實現這個過程?

一般有兩種方式,一種是直接在 A 里直接創(chuàng)建對象 B 并且使用它能力,另一種是通過注入的方式,將依賴對象 B 引入到對象 A 中再使用。

依賴注入通常有三種方式:

  • 構造方法注入
  • 接口注入
  • 取值注入

構造方法注入

在一個類的構造函數中,增加該類依賴的其他對象。

優(yōu)點

  • 在構造完成后,依賴對象就進入了就緒狀態(tài),可以馬上使用

缺點

  • 依賴對象比較多時,構造方法冗長,不夠優(yōu)雅,也不利于拓展,有一定的維護成本
@interface ComponentLoader

- (instancetype)initWithInjectComponent:(id<TestProtocol>)component;

@end

接口注入

通過定義一個注入依賴的接口,進行依賴對象的注入。

缺點:對象的注入時機不太可控,且中途外部能修改,存在隱藏風險。

@interface ComponentLoader

- (void)injectComponent:(id<TestProtocol>)component;

@end

取值注入

在使用依賴對象的地方通過 DI 提供的接口,獲取依賴對象并直接使用。

通過 DI 容器提供的接口,配合包裝的宏定義,我們可以輕松的獲取到對應的依賴對象,但是如果一個類中在多處依賴了該對象,就會在多處存在 DI 的宏,代碼層面上增加了對 DI 的依賴,因此可以把依賴對象聲明為屬性,并通過 getter 方法對依賴對象的屬性進行賦值。

其偽代碼如下:

@interface ComponentLoader

@property (nonatomic, strong) id<TestProtocol> component;

@end

@implementation ComponentLoader

//將屬性component與TestProtocol綁定的的依賴注入對象進行關聯
XLink(component,TestProtocol)

//宏定義展開后的代碼為
- (id<TestProtocol>)component {
return xlink_get_property(@protocol(TestProtocol), (NSObject *)_component, @component, (NSObject *)self);
}
@end

依賴注入在抖音直播中的應用

抖音直播間將每個細分功能設計為一個組件,將功能相近或關聯較強的組件打包到同一個模塊,通過模塊化、組件化的設計,來讓業(yè)務得到合理的粒度拆分。目前抖音直播設計有幾十個模塊,數百個組件。

抖音直播里的依賴主要指的是一個組件依賴另一個組件提供的能力,而依賴注入的使用主要也是解決組件間的耦合問題。

組件的創(chuàng)建

在打開抖音直播間時,RoomController?會先創(chuàng)建一個ComponentLoader,ComponentLoader負責創(chuàng)建直播間中需要的組件。

如果一進直播間就一股腦加載幾百個組件,一方面會因為設備性能瓶頸導致首屏體驗慢,另一方面每個組件加載的耗時存在差異,展示的優(yōu)先級也有差別,同時加載必然帶來不好的用戶觀感體驗。

因此針對這幾百個組件,設計了優(yōu)先級的劃分,按優(yōu)先級分批次進行組件的創(chuàng)建與加載,來保障絲滑的首屏秒開體驗。

DI 容器隔離

依賴注入框架的本質是一個單例來維護協議與實現協議的對象之間的映射關系,單例也就意味著全局獨一份。如果業(yè)務相對比較清晰,處理好注入對象的生命周期管理,使用單例來管理,清晰明了簡單易用,也沒什么大問題。但是在抖音直播這種大型的業(yè)務上面,業(yè)務場景過于復雜,單例帶來的維護成本也會顯著上升。

單例最致命的問題是在于:所有服務都會注冊到同一個的 DI 容器中,若存在多個直播間,多直播間之間的服務很難做到優(yōu)雅的隔離。

例如直播間上下滑場景,滑動過程中會同時存在兩個直播間,兩個直播間都存在禮物組件,這兩個禮物組件需要在同一個 DI 容器中被管理。

同一容器中多直播間之間同類對象的區(qū)分管理,會帶來比較大的復雜度與維護成本。

由于抖音直播過早地、很深地依賴了依賴注入框架,當發(fā)現它本身的限制性時,已經很難把原有框架替換掉,只能在原有功能基礎上進行能力迭代。

最終的解決方案是:分層與隔離。我們設計了多層的 DI 容器來實現隔離

直播通用的服務,注冊到 LiveDI 容器中,如配置下發(fā)服務、用戶信息服務等;

單個房間級別的服務,注冊到 RoomDI 容器中,如一般的直播間內組件(禮物、紅包等)。

通常情況下,同時只存在一個 LiveDI 容器跟一個 RoomDI 容器。

在直播間上下滑場景中,會同時存在兩個 RoomDI 容器,這兩個容器之間實現互相隔離。如上一個直播間中的禮物組件與下一個新直播間的禮物組件是兩個獨立的對象,分別注冊在兩個獨立的 RoomDI 容器中,當新直播間完全展示時,消失直播間的 RoomDI 容器就會被銷毀,其內維護的組件便也一并跟著釋放。

通過這種多容器的設計,實現了不同直播間的隔離。

依賴注入的優(yōu)缺點

優(yōu)點

穩(wěn)定性好

  • 兩個存在依賴關系的對象,只有在真正使用時,兩者才發(fā)生聯系。所以,無論兩者中的任何一方出現什么的問題,都可以做到不影響另一方的開發(fā)與調試。例如心愿單模塊依賴了手勢模塊,在手勢模塊存在代碼問題無法運行時,可以通過修改配置去掉手勢模塊的加載,心愿模塊依然能正常運行,只是涉及到手勢相關功能存在問題而已,不會影響到整體的開發(fā)調試。

可維護性好

  • 代碼中的每一個 Class 都可以單獨測試,彼此之間互不影響,只要保證自身的功能無誤即可。我們可以通過 mock 依賴對象來代替需要注入的對象,進行單元測試。

耦合性低,開發(fā)提效

  • 各個模塊間解耦后,只需要定義并遵守相應的協議,無需關心其他模塊的細節(jié)。每個開發(fā)團隊的成員都只需要關心實現自身的業(yè)務邏輯,完全不用去關心其它的人工作進展,因為你的任務跟別人沒有任何關系,你的任務可以單獨測試,你的任務也不用依賴于別人的組件,再也不用扯不清責任了。所以,在一個大中型項目中,團隊成員分工明確、責任明晰,很容易將一個大的任務劃分為細小的任務,開發(fā)效率和產品質量必將得到大幅度的提高。

復用性好

  • 我們可以把具有普遍性的常用組件獨立出來,反復利用到項目中的其它部分,或者是其它項目,當然這也是面向對象的基本特征。顯然,IOC 不僅更好地貫徹了這個原則,提高了模塊的可復用性。符合接口標準的實現,都可以插接到支持此標準的模塊中。

支持熱插拔

  • IOC 生成對象的方式轉為外置方式,也就是把對象生成放在配置文件里進行定義,這樣,當我們增刪一個模塊,或者更換一個實現子類,將會變得很簡單,只要修改配置文件就可以了,完全具有熱插撥的特性。

缺點

使用 IOC 框架產品能夠給我們的開發(fā)過程帶來很大的好處,但是也要充分認識引入 IOC 框架的缺點,做到心中有數,杜絕濫用框架。

提高了上手成本

  • 軟件系統中由于引入了第三方 IOC 容器,生成對象的步驟變得有些復雜,本來是兩者之間的事情,又憑空多出一道手續(xù),所以,我們在剛開始使用 IOC 框架的時候,會感覺系統變得不太直觀。所以,引入了一個全新的框架,就會增加團隊成員學習和認識的培訓成本,并且在以后的運行維護中,還得讓新同學具備同樣的知識體系。

引入不成熟框架帶來風險

  • IOC 框架產品本身的成熟度需要進行評估,如果引入一個不成熟的 IOC 框架產品,那么會影響到整個項目,所以這也是一個隱性的風險。例如直播中臺很早就引入了 IESDI 庫,但是 IESDI 存在一些能力缺陷,比如做不到不同 DI 容器之間的隔離。因為 IESDI 在項目中被深度使用,如果換 DI 框架會帶來涉及面很廣的改動,風險不可控,所以只能基于現有框架進行修補,因此也會帶來一些比較 trick 的邏輯。

對運行效率帶來一定影響

  • 由于 IOC 容器生成對象有些是通過反射的方式,在運行效率上有一定的損耗。如果要追求運行效率的話,就必須對此進行權衡。

通過對優(yōu)缺點的分析,我們大體可以得出這樣的結論:

一些工作量不大的項目或者產品,不太適合使用 IOC 框架產品。另外,如果團隊成員的知識能力欠缺,對于 IOC 框架產品缺乏深入的理解,也不要貿然引入,可能會帶來額外的風險與成本。

但如果你經歷的是一個復雜度較高的項目,需要通過組件化、模塊化等形式來降低耦合,提高開發(fā)效率,那么依賴注入就值得被納入考慮范圍,或許你會得到不一樣的開發(fā)體驗。

結語

得益于依賴注入框 架強大的解耦能力,在實現抖音直播間這種復雜的功能聚合型頁面時,仍然能保持高效的組織協作與模塊分工,為高質量的業(yè)務迭代與追求極致的用戶體驗提供穩(wěn)固的基礎技術基石。在抖音如此龐大的 APP 里做架構層面的重構,任何風吹草動都可能傷筋動骨,這就要求我們在做架構設計時,多抬頭看看前方的路。我們不提倡過度設計,但是時刻保持思考,始終創(chuàng)業(yè),才能讓架構伴隨業(yè)務一起成長,共述華章。?

責任編輯:未麗燕 來源: 字節(jié)跳動技術團隊
相關推薦

2020-06-17 18:10:01

TypeScript前端

2010-01-27 16:13:43

2021-03-16 08:54:35

AQSAbstractQueJava

2011-07-04 10:39:57

Web

2023-05-05 18:33:15

2022-03-23 18:58:11

ZookeeperZAB 協議

2019-11-11 14:51:19

Java數據結構Properties

2022-12-02 09:13:28

SeataAT模式

2009-11-30 16:46:29

學習Linux

2017-07-02 18:04:53

塊加密算法AES算法

2019-01-07 15:29:07

HadoopYarn架構調度器

2021-07-20 15:20:02

FlatBuffers阿里云Java

2012-05-21 10:06:26

FrameworkCocoa

2022-09-26 09:01:15

語言數據JavaScript

2024-11-27 06:50:58

元組函數返回值

2019-11-14 09:53:30

Set集合存儲

2009-12-25 15:49:43

Linux rescu

2023-03-20 09:48:23

ReactJSX

2022-01-11 07:52:22

CSS 技巧代碼重構

2019-12-04 10:13:58

Kubernetes存儲Docker
點贊
收藏

51CTO技術棧公眾號