愛奇藝組件化探索和實(shí)踐!
BAT
前言
組件化對(duì)于任何一個(gè)業(yè)務(wù)場(chǎng)景復(fù)雜的APP以及經(jīng)過(guò)多次迭代之后的產(chǎn)品來(lái)說(shuō)都是必經(jīng)之路,組件化是指解耦復(fù)雜系統(tǒng)時(shí)將多個(gè)功能模塊拆分、重組的過(guò)程。組件化要做的不僅僅是表面上看到的模塊拆分解耦,其背后還有很多工作來(lái)支撐組件化的進(jìn)行,例如結(jié)合業(yè)務(wù)特性的模塊拆分策略、模塊間的交互方式和構(gòu)建系統(tǒng)等等。
本文主要講述愛奇藝知識(shí)APP如何結(jié)合自身的業(yè)務(wù)特點(diǎn),探索和實(shí)踐了一套高效的移動(dòng)端組件化方案。
01 背景與目標(biāo)
1.1背景
愛奇藝知識(shí)目前有多個(gè)業(yè)務(wù)承載端,包括愛奇藝移動(dòng)端APP的知識(shí)插件、愛奇藝iPad端APP的知識(shí)插件、隨刻移動(dòng)端APP的知識(shí)插件和愛奇藝知識(shí)移動(dòng)端獨(dú)立APP。由于各個(gè)端上線的時(shí)間不同,所承載的業(yè)務(wù)功能也不完全一致,造成了多端多套代碼的情況,維護(hù)成本很高。首先當(dāng)相同或類似的功能需要迭代升級(jí)時(shí),開發(fā)和測(cè)試都需要在多端同步進(jìn)行,成本成指數(shù)級(jí)增長(zhǎng);其次隨著業(yè)務(wù)的快速發(fā)展,業(yè)務(wù)模塊在不斷增加,模塊間的依賴關(guān)系也變得越來(lái)越模糊,代碼耦合度和復(fù)雜度都在增大;另外在現(xiàn)有人力成本的基礎(chǔ)上如果想增加更多的業(yè)務(wù)端,就會(huì)變得非常困難。因此長(zhǎng)期看非常不利于業(yè)務(wù)的高效迭代。下圖描述了組件化之前,愛奇藝知識(shí)各端的業(yè)務(wù)模塊架構(gòu)。

從上圖我們也能看出其實(shí)每個(gè)端之間是存在很多公共業(yè)務(wù)模塊的,各個(gè)業(yè)務(wù)模塊的底層支撐模塊也幾乎相同,所以結(jié)合愛奇藝知識(shí)自身的業(yè)務(wù)特點(diǎn),我們提出了適合于愛奇藝知識(shí)移動(dòng)端的組件化方案。
1.2目標(biāo)
我們將組件化的目標(biāo)定義為以下幾個(gè):
· 解決多端代碼維護(hù)問(wèn)題
根據(jù)業(yè)務(wù)特點(diǎn),橫向和縱向劃分組件,以組件為單位承接迭代需求,各端進(jìn)行組件復(fù)用;
· 解決跨組件調(diào)用和組件間路由的問(wèn)題
業(yè)務(wù)劃分更加清晰、組件間解耦更加徹底、組件間通信更高效,對(duì)原有業(yè)務(wù)模塊進(jìn)行抽離和整合,明確組件間的業(yè)務(wù)邊界;
· 提升開發(fā)效率,方便開發(fā)調(diào)試
組件可以單獨(dú)編譯和調(diào)試,使模塊開發(fā)者更聚焦本模塊業(yè)務(wù);
· 提升集成和提測(cè)效率
各端項(xiàng)目需要哪個(gè)組件,可以直接通過(guò)工具快速集成和提測(cè)。
基于以上目標(biāo),我們?cè)O(shè)計(jì)了適合愛奇藝知識(shí)業(yè)務(wù)的組件劃分策略,下圖為組件化之后的功能架構(gòu)圖,橫向分為基礎(chǔ)組件、功能組件和業(yè)務(wù)組件,縱向?qū)γ總€(gè)層級(jí)的組件又進(jìn)行細(xì)分;從劃分粒度上看,組件不僅包括功能性的sdk,還可能包括業(yè)務(wù)UI,宗旨就是業(yè)務(wù)模塊獨(dú)立,邊界清晰,方便擴(kuò)展和維護(hù)。

02 整體技術(shù)架構(gòu)
基于功能架構(gòu),知識(shí)組件化的技術(shù)架構(gòu)如下圖所示。

最下層是基礎(chǔ)組件,包括baselib和componentService,我們將網(wǎng)絡(luò)庫(kù)、pingback、數(shù)據(jù)庫(kù)、日志和工具類等公共底層實(shí)現(xiàn)構(gòu)成基礎(chǔ)組件,屏蔽了系統(tǒng)和各端的差異,位于功能和業(yè)務(wù)組件的下層。所有功能和業(yè)務(wù)組件都使用同一套基礎(chǔ)組件,可以保證公共部分的統(tǒng)一性?;A(chǔ)組件比較穩(wěn)定,不會(huì)頻繁迭代。
再往上一層是功能組件,如承載播放能力的播放器和歷史記錄組件、承載支付能力的支付和營(yíng)銷組件,、承載多端定制化分享能力的分享&海報(bào)組件等各個(gè)端都有的基礎(chǔ)功能,功能組件位于基礎(chǔ)組件和業(yè)務(wù)組件之間,功能組件會(huì)根據(jù)業(yè)務(wù)組件的需要而不斷迭代升級(jí)。
接下來(lái)是業(yè)務(wù)組件,這層是各個(gè)端有可能包含也有可能不包含核心業(yè)務(wù)模塊,為了開發(fā)和維護(hù)方便,我們將核心業(yè)務(wù)模塊抽取為業(yè)務(wù)組件,如搜索、篩選、發(fā)現(xiàn)feed流、評(píng)論、評(píng)價(jià)、作業(yè)作品等,業(yè)務(wù)組件位于基礎(chǔ)組件和功能組件的上層,迭代較頻繁,但業(yè)務(wù)本身比較獨(dú)立,邊界清晰。
最上層是殼工程,各端都需要一個(gè)主工程負(fù)責(zé)集成所需要的組件,我們統(tǒng)稱為殼工程,殼工程包括了各端的基礎(chǔ)框架,比如組件注冊(cè)和初始化邏輯,平臺(tái)相關(guān)性處理邏輯等,還有各端特有的業(yè)務(wù)模塊,不適合抽離和拆解的部分。
右側(cè)是負(fù)責(zé)管理組件間交互和跳轉(zhuǎn)的MoudleRouter和UIRouter。這部分是公共基礎(chǔ)設(shè)施,各端都要集成。
左側(cè)是構(gòu)建系統(tǒng),它不在組件化代碼中,屬于輔助系統(tǒng),負(fù)責(zé)組件和各端應(yīng)用包的構(gòu)建。
03 核心技術(shù)實(shí)現(xiàn)
組件化實(shí)踐中比較核心的兩個(gè)技術(shù)點(diǎn)是,組件間交互和組件間路由。
3.1組件交互
組件間交互的難點(diǎn)是降低組件耦合度,最好能達(dá)到完全無(wú)侵入式的調(diào)用。經(jīng)過(guò)調(diào)研,iOS端使用ModuleManager的方式,它被定義為最底層的服務(wù)組件,每個(gè)組件都需要對(duì)外提供被調(diào)用的服務(wù)接口,接口的定義存在于ModuleManager組件。ModuleManager的代碼對(duì)其他組件代碼來(lái)說(shuō)是無(wú)侵入的,只負(fù)責(zé)對(duì)傳遞過(guò)來(lái)的數(shù)據(jù)進(jìn)行解析,并將調(diào)用消息傳遞給對(duì)應(yīng)組件。

為了解決URL硬編碼 ,以及字典參數(shù)類型不明確等問(wèn)題,iOS端在組件化方案中選用了Protocol方案,在程序開始運(yùn)行時(shí)將自身的Class注冊(cè)到ModuleManager中,并將Protocol反射為字符串當(dāng)做key,Class遵守協(xié)議并實(shí)現(xiàn)協(xié)議定義的方法,外界通過(guò)Protocol獲取的Class并實(shí)例化為對(duì)象,調(diào)用服務(wù)方實(shí)現(xiàn)的協(xié)議方法。獨(dú)立APP和各個(gè)插件的服務(wù)注冊(cè)的時(shí)機(jī)不同,獨(dú)立APP是在程序啟動(dòng)時(shí),而插件則是外部調(diào)用插件時(shí),在插件退出時(shí)去需要解除注冊(cè)釋放資源。Protocol方案描述如下:

在Android端,組件間交互使用的是ZRouter組件,實(shí)現(xiàn)思路和iOS端類似,是參考了java中SPI機(jī)制(服務(wù)提供發(fā)現(xiàn)機(jī)制),每個(gè)組件對(duì)外提供一個(gè)服務(wù)接口service,接口的實(shí)現(xiàn)交給對(duì)應(yīng)組件內(nèi)。在組件初始化注冊(cè)時(shí)候,會(huì)同時(shí)注冊(cè)該service接口和對(duì)應(yīng)service實(shí)現(xiàn)。業(yè)務(wù)方使用時(shí),只需要通過(guò)service接口調(diào)用組件功能。這樣組件間就沒有了直接依賴關(guān)系,實(shí)現(xiàn)了組件間解耦隔離。具體調(diào)用如下圖所示:

3.2組件路由
組件間路由跳轉(zhuǎn)方面,iOS端采用了注冊(cè)URL的方式,注冊(cè)的時(shí)機(jī)分為靜態(tài)、動(dòng)態(tài)和懶加載三種,懶加載方式即為在調(diào)用跳轉(zhuǎn)方法時(shí)檢查URL與ClassName是否已經(jīng)注冊(cè)綁定,如果未綁定則從模塊靜態(tài)信息表中獲取并完成注冊(cè)綁定,Handler可以在動(dòng)態(tài)注冊(cè)時(shí)進(jìn)行指定,這樣跳轉(zhuǎn)邏輯即可實(shí)現(xiàn)完全自定義而不走底層的統(tǒng)一跳轉(zhuǎn)邏輯,同樣要注意的是插件端需要在退出插件時(shí)釋放資源并取消注冊(cè)。

Android端針對(duì)組件間UI跳轉(zhuǎn)的實(shí)現(xiàn)方面,雖然前面講到的ZRouter也能做到Activity、Fragment、View之間跳轉(zhuǎn),但是代碼實(shí)現(xiàn)過(guò)于復(fù)雜。所以我們借鑒了業(yè)內(nèi)組件化的優(yōu)秀思想,專門開發(fā)了一個(gè)用于組件間UI跳轉(zhuǎn)的UIRouter。在編譯期間,通過(guò)Activity上添加的@RouterPath注解,生成一張Key為Scheme或頁(yè)面短碼,Value為Activity的路由表。跳轉(zhuǎn)任何一個(gè)Activity都交由路由框架,根據(jù)路由表決定啟動(dòng)哪個(gè)Activity。

為了提升開發(fā)效率,減少UIRouter初始化時(shí)重復(fù)開發(fā)的代碼,我們開發(fā)了一個(gè)插入自動(dòng)注冊(cè)代碼的gradle插件,利用此插件在編譯期通過(guò)ASM向指定方法中注入初始化代碼。
同時(shí),在組件庫(kù)注冊(cè)的時(shí)候也有用到這個(gè)技術(shù);組件初始化類在debug模式下通過(guò)反射加入內(nèi)存,在release模式下則通過(guò)ASM插入注冊(cè)代碼。這樣在debug模式下可以縮短編譯時(shí)間提升開發(fā)效率,在release正式包中運(yùn)行時(shí)可減少反射帶來(lái)的消耗。
整個(gè)優(yōu)化流程如下:

04 構(gòu)建系統(tǒng)
有了層次清晰的組件劃分,那么如何快速構(gòu)建組件和項(xiàng)目成了必需要解決的事情。針對(duì)組件化,愛奇藝知識(shí)團(tuán)隊(duì)結(jié)合公司已有的構(gòu)建系統(tǒng),開發(fā)了一套適用于組件化的快速構(gòu)建子系統(tǒng)。
為了解決多端共用一套代碼和在各個(gè)插件端都有包大小的限制的前提下,在組件庫(kù)中存在的差異代碼通過(guò)宏分割來(lái)控制,實(shí)現(xiàn)差異代碼隔離,編譯時(shí)僅編譯當(dāng)前指定的某一端代碼,打包時(shí)通過(guò)指定打包參數(shù)來(lái)設(shè)置宏配置,完成指定端的構(gòu)建。
iOS端每個(gè)組件都是一個(gè)單獨(dú)的工程,由不同的git私有倉(cāng)庫(kù)來(lái)管理,各個(gè)組件是在主項(xiàng)目中通過(guò)CocoaPods來(lái)集成,將所有組件當(dāng)做二方庫(kù)集成到主項(xiàng)目中。愛奇藝知識(shí)APP與各端插件雖然都采用了Cocoapods集成的方案,但是在版本依賴上有所差異,為提升開發(fā)效率,知識(shí)APP作為獨(dú)立應(yīng)用程序直接采用了指定git倉(cāng)庫(kù)tag號(hào)的方式來(lái)依賴組件庫(kù),插件則需要通過(guò)插件庫(kù)的podsec設(shè)置依賴來(lái)集成組件庫(kù)的,這就需要將組件庫(kù)打成二進(jìn)制的庫(kù)文件上傳到云,并上傳組件庫(kù)的podspec到私有庫(kù)中。iOS插件端在主項(xiàng)目中集成組件主要分為兩種方式分為源碼和framework,在開發(fā)調(diào)試階段采用源碼方式,可以直接修改代碼完成需求開發(fā),在打包提測(cè)和發(fā)布時(shí)采用生成framework,可以加快編譯速度不會(huì)對(duì)外暴露源碼。

iOS端選擇Jenkins作為構(gòu)建系統(tǒng),在組件化初始階段,我們組件的構(gòu)建是通過(guò)先構(gòu)建最基礎(chǔ)的組件,然后再構(gòu)建上層組件來(lái)完成的整體構(gòu)建,隨著組件庫(kù)數(shù)量增多,依賴關(guān)系變復(fù)雜之后手動(dòng)逐一觸發(fā)構(gòu)建成為了構(gòu)建過(guò)程的痛點(diǎn),于是開始進(jìn)行構(gòu)建優(yōu)化,引入了Jenkins的ParameterizedTrigger插件并配合shell腳本使用,使得我們支持組件的單個(gè)構(gòu)建的同時(shí),在主項(xiàng)目構(gòu)建時(shí)支持觸發(fā)多個(gè)組件,組件單獨(dú)構(gòu)建時(shí)也支持配置依賴構(gòu)建的項(xiàng)目,實(shí)現(xiàn)了一次觸發(fā)完成全部組件的構(gòu)建。

組件庫(kù)構(gòu)建時(shí)會(huì)對(duì)當(dāng)前迭代分支的代碼進(jìn)行更新檢查,如果存在更新則會(huì)構(gòu)建組件庫(kù),不存在更新則直接跳過(guò)此次構(gòu)建。隨著端的增加構(gòu)建系統(tǒng)支持了多端構(gòu)建,iPhone插件和iPad插件為不同的插件Job,通過(guò)腳本實(shí)現(xiàn)端的區(qū)分完成構(gòu)建。建系統(tǒng)還實(shí)現(xiàn)了版本自增和定時(shí)構(gòu)建,每一次構(gòu)建完成后都會(huì)更新podspec文件中的版本號(hào),在下一次構(gòu)建時(shí)如果未手動(dòng)指定構(gòu)建版本便會(huì)獲取之前的版本進(jìn)行加一實(shí)現(xiàn)版本自增。構(gòu)建系統(tǒng)對(duì)接了企業(yè)內(nèi)部的即時(shí)通訊工具,在構(gòu)建完成后發(fā)送通知給已經(jīng)訂閱的用戶。下圖描述了組件構(gòu)建流程:

在Android端,同樣每一個(gè)業(yè)務(wù)組件都是一個(gè)完整的個(gè)體,可以當(dāng)作獨(dú)立的App來(lái)運(yùn)行,需要滿足單獨(dú)運(yùn)行及測(cè)試的要求,這樣可以提升編譯速度和開發(fā)效率。
目前業(yè)界常規(guī)做法是每一個(gè)組件就是一個(gè)工程,由build.properties中一個(gè)常量控制區(qū)分不同場(chǎng)景,且在build.gradle中sourceSets設(shè)置單獨(dú)調(diào)試組件時(shí)的配置,區(qū)別于發(fā)布組件aar時(shí)候的獨(dú)立運(yùn)行時(shí)的配置;
但是這種方案對(duì)于愛奇藝知識(shí)客戶端來(lái)說(shuō)并不完全適用,因?yàn)槲覀兘M件化最主要的一個(gè)目的是要達(dá)到多端組件復(fù)用,這樣也會(huì)存在需要進(jìn)行多端適配情況,每一個(gè)端的基礎(chǔ)配置信息、基礎(chǔ)UI樣式等都有所不同,不可能在每一個(gè)新的組件工程中都配置一遍。所以,直接使用原本混沌工程的殼工程作為組件調(diào)試的Project,將runalone文件夾設(shè)置在各自殼工程中,在根目錄build.properties中通過(guò)常量isModuleType控制編譯模式,動(dòng)態(tài)加載測(cè)試所需組件依賴,這樣就可以在各個(gè)環(huán)境中單獨(dú)測(cè)試組件了。

此時(shí)的組件單獨(dú)調(diào)試模式其實(shí)等價(jià)于理想狀態(tài)下的組件化殼工程模式:只有少量配置相關(guān)代碼、無(wú)其他組件無(wú)關(guān)頁(yè)面邏輯、動(dòng)態(tài)按需加載組件。
在殼工程根目錄gradle.properties中包含各種常量,包括端控制、組件庫(kù)版本號(hào)、編譯環(huán)境控制、運(yùn)行時(shí)依賴控制和運(yùn)行模式控制參數(shù)等。

isDependenceMaven用來(lái)控制依賴方式是源碼還是遠(yuǎn)程maven,開發(fā)期間debug可以使用源碼方式方便調(diào)試,正式環(huán)境使用遠(yuǎn)程依賴方式節(jié)省編譯時(shí)間,方便復(fù)用。
maven_version用來(lái)統(tǒng)一控制組件版本號(hào),在每一個(gè)版本升級(jí)時(shí)候?qū)?yīng)的升級(jí)組件庫(kù)版本和依賴的組件庫(kù)版本,通過(guò)自定義的maven_publish上傳腳本批量編譯上傳并更新。
對(duì)于Android sdk版本和第三方庫(kù)版本,我們統(tǒng)一抽離到外層的一個(gè)dependencies.gradle中統(tǒng)一控制,這樣能方便且直觀管理版本號(hào)。
總結(jié)與展望
愛奇藝知識(shí)移動(dòng)端已經(jīng)基本實(shí)現(xiàn)了組件化的全部目標(biāo),組件間業(yè)務(wù)邊界已經(jīng)變得非常清晰,做到了一個(gè)組件升級(jí)多端受益,大幅提高了開發(fā)和測(cè)試效率。另外,組件增減靈活,使得新增一個(gè)業(yè)務(wù)承載端的成本很低,只需要組合已有組件并對(duì)組件做針對(duì)這個(gè)新增端的修改即可,使愛奇藝知識(shí)移動(dòng)端做到了較少的人力能夠支撐更多端的能力。組件化過(guò)程中遇到的一些問(wèn)題已經(jīng)全部解決,目前組件化從底層的基礎(chǔ)組件到上層的業(yè)務(wù)組件都已經(jīng)全部上線,組件化構(gòu)建系統(tǒng)也已經(jīng)投入生產(chǎn)環(huán)境。
當(dāng)然組件化不是一蹴而就的,而是一個(gè)持續(xù)的過(guò)程,未來(lái)還需要不斷優(yōu)化和完善,讓組件化在知識(shí)業(yè)務(wù)發(fā)展中起到更大的作用。
推薦閱讀
? 耗時(shí)2年,Android進(jìn)階三部曲第三部《Android進(jìn)階指北》出版!
? 『BATcoder』做了多年安卓還沒編譯過(guò)源碼?一個(gè)視頻帶你玩轉(zhuǎn)!
BATcoder技術(shù)群,讓一部分人先進(jìn)大廠
大家好,我是劉望舒,騰訊云最具價(jià)值專家TVP,著有暢銷書《Android進(jìn)階之光》《Android進(jìn)階解密》《Android進(jìn)階指北》,蟬聯(lián)四屆電子工業(yè)出版社年度優(yōu)秀作者,谷歌開發(fā)者社區(qū)特邀講師,百度百科收錄的技術(shù)專家。
前華為面試官,現(xiàn)大廠技術(shù)負(fù)責(zé)人。
想要加入 BATcoder技術(shù)群,公號(hào)回復(fù)BAT 即可。
為了防止失聯(lián),歡迎關(guān)注我的小號(hào)
微信改了推送機(jī)制,真愛請(qǐng)星標(biāo)本公號(hào)??

