「 51信用卡管家」Android 架構(gòu)背后的演進(jìn)與實(shí)踐過程

隨著業(yè)務(wù)的快速擴(kuò)張烫扼,原本小作坊式的單個(gè)工程的開發(fā)模式越來與不能滿足實(shí)際需求。早在兩年多以前,51信用卡管家就向下沉淀出了單獨(dú)的公用基礎(chǔ)庫(kù),一些通用的功能組件和個(gè)別獨(dú)立的業(yè)務(wù)被拆分成 SDK扭粱,形成了一套中型項(xiàng)目、多人并行的開發(fā)模式震檩,也為未來組件化拆分做準(zhǔn)備琢蛤。

這套框架運(yùn)行了一段時(shí)間之后,伴隨著單應(yīng)用內(nèi)業(yè)務(wù)需求的增加抛虏、開發(fā)人員數(shù)量的增多博其、基礎(chǔ)庫(kù)數(shù)量的膨脹,導(dǎo)致了一些問題:

  • 主工程代碼耦合嚴(yán)重迂猴,牽一發(fā)而動(dòng)全身
  • 需求測(cè)試影響面大慕淡,不能聚焦單一業(yè)務(wù)模塊
  • 主工程代碼越來越多,編譯耗時(shí)
  • 依賴倒置沸毁,業(yè)務(wù)代碼依賴App工程
  • SDK 界限模糊峰髓,基礎(chǔ)庫(kù)和業(yè)務(wù)庫(kù)界限不明確
  • 業(yè)務(wù)模塊間可以任意依賴調(diào)用,依賴規(guī)則不明確
  • 類庫(kù)越來越多息尺,不好管理

除了以上問題儿普,動(dòng)態(tài)化需求也越來越強(qiáng)烈,依賴 Hybrid + H5 打開頁(yè)面慢的問題也凸顯出來掷倔。

這些問題推動(dòng)我們更進(jìn)一步的升級(jí)開發(fā)構(gòu)架。

動(dòng)態(tài)化

最近兩年个绍,插件化框架層出不窮勒葱,各大廠都放出了自家開源的插件化框架。作為 Native 動(dòng)態(tài)化與性能兼顧的插件化方案巴柿,很多公司選擇插件化作為動(dòng)態(tài)化技術(shù)方案凛虽。動(dòng)態(tài)性通常有兩部分的作用:一是動(dòng)態(tài)熱修復(fù);二是動(dòng)態(tài)下發(fā)業(yè)務(wù)插件广恢。對(duì)于第一點(diǎn)凯旋,我們有熱修復(fù)框架可以完成這部分工作;對(duì)于第二點(diǎn)钉迷,我們使用了 Hybrid 加載H5的方式實(shí)現(xiàn)至非,雖然性能上有所欠缺,但完全切到 Native 來做有點(diǎn)推倒重來的意思糠聪,并且跟業(yè)界同學(xué)交流后荒椭,對(duì)于動(dòng)態(tài)下發(fā)業(yè)務(wù)插件用到的情況也不多,業(yè)務(wù)更新主要還是依靠 App 升級(jí)來實(shí)現(xiàn)舰蟆。技術(shù)方案沒有最優(yōu)解趣惠,選擇適合自己的才是最好的狸棍。

由于插件化也存在一些弊端,比如不可避免的 hook framework味悄、修改 aapt草戈、包裝 Gradle Plugin、代理組件等等非常規(guī)操作侍瑟,日常維護(hù)也是一筆不小的開銷唐片,穩(wěn)定性、兼容性丢习、新版本適配等等問題都需要考慮進(jìn)去牵触。對(duì)于 Android 端是否使用插件化,公司內(nèi)部做過一些討論咐低,結(jié)論是不急著上揽思,邊走邊看,先把業(yè)務(wù)組件拆分出來再說见擦。

如今回過頭看钉汗,自從 Android P發(fā)布以來,限制 hook framework 后鲤屡,插件化逐漸開始式微损痰,后面走向大概率是維護(hù)成本越來越高,成本收益比逐漸降低酒来,最終棄坑不用卢未。

除了插件化外,動(dòng)態(tài)化方案近兩年比較火的就是以 ReactNative堰汉、Weex 為代表的大前端方向辽社,結(jié)合51信用卡的實(shí)際情況,最終選擇擁抱大前端翘鸭, Weex 作為動(dòng)態(tài)化方案滴铅,以 Native 為主, Hybrid 離線化方案為輔就乓,Weex 逐步迭代的架構(gòu)開發(fā)模式汉匙。

Weex 的基礎(chǔ)建設(shè)和前端同學(xué)合作,歷經(jīng)大半年時(shí)間生蚁,目前已經(jīng)穩(wěn)定應(yīng)用在51信用卡各個(gè) App 上噩翠,Weex 作為動(dòng)態(tài)化頁(yè)面的首選方案,已經(jīng)完成了線上數(shù)百個(gè)頁(yè)面的開發(fā)需求守伸。配合離線化方案绎秒,各項(xiàng)性能指標(biāo)也都達(dá)到要求。

組件分離

代碼解耦與代碼隔離尼摹,最有效的方案是工程隔離见芹。審視我們最初的方案剂娄,每個(gè) SDK 對(duì)應(yīng)單獨(dú)的倉(cāng)庫(kù),通過 maven 依賴玄呛,通過工程分離隔離代碼阅懦,這種方案沒有問題,只不過需要往前更近一步徘铝,各個(gè)業(yè)務(wù)模塊也需要獨(dú)立主工程耳胎,拆分成獨(dú)立的業(yè)務(wù)組件。

同時(shí)惕它,劃分清楚代碼邊界怕午,控制依賴關(guān)系,梳理清楚層次結(jié)構(gòu)淹魄,最終形成如下圖所示的架構(gòu)郁惜。

整體架構(gòu)上提供三種容器:

  • Native 容器,采用組件化架構(gòu)甲锡,用于原生業(yè)務(wù)開發(fā)
  • Hybrid 容器兆蕉,webview 加載 H5,配合離線化方案
  • Weex 容器缤沦,用于編寫常規(guī)的頁(yè)面虎韵,js 動(dòng)態(tài)轉(zhuǎn)化成 Native 控件,天然具有動(dòng)態(tài)化特性缸废,配合離線化方案包蓝,達(dá)到頁(yè)面秒開的效果,同時(shí)共用 Hybrid 沉淀出的比較完善的 PG 方法

同時(shí)企量,Hybrid 和 Weex 依賴于原生提供的方法养晋,通過 JsBridge 進(jìn)行通信,目前共有 200 多個(gè) PG 方法供 js 調(diào)用梁钾。長(zhǎng)遠(yuǎn)來看,這三種容器并不會(huì)互相取代逊抡,相反地姆泻,它們應(yīng)該是相互依存、取長(zhǎng)補(bǔ)短冒嫡、長(zhǎng)期共存的狀態(tài)拇勃。

組件化實(shí)踐

Native 容器對(duì)應(yīng)上圖中各個(gè)層級(jí)的定義:

  • 工程 App,各個(gè)應(yīng)用工程孝凌,目前已有十多個(gè)應(yīng)用并行開發(fā)方咆,51信用卡管家作為平臺(tái)應(yīng)用,其余應(yīng)用為獨(dú)立的業(yè)務(wù)工程應(yīng)用
  • 業(yè)務(wù)組件蟀架,獨(dú)立的業(yè)務(wù)組件瓣赂,一般為復(fù)合業(yè)務(wù)組件榆骚,api 與實(shí)現(xiàn)分離,相互之間依賴隔離
  • 基礎(chǔ)業(yè)務(wù) SDK煌集,獨(dú)立的小的單功能模塊妓肢,提供基礎(chǔ)功能,目前這一層級(jí)中還包含遺留未改造的部分業(yè)務(wù)組件
  • 基礎(chǔ) Lib苫纤,業(yè)務(wù)無(wú)關(guān)的基礎(chǔ)組件

組件化拆分的核心訴求是解耦合碉钠,提高組件內(nèi)聚,所以應(yīng)該從訴求出發(fā)卷拘,在沿用當(dāng)下開發(fā)模式喊废,并且不強(qiáng)依賴組件化框架的情況下,逐漸的進(jìn)行組件化拆分栗弟。

通過工程隔離進(jìn)而進(jìn)行組件化拆分后污筷,基本可以解決上面提到的問題:

  • 高內(nèi)聚,低耦合横腿,代碼邊界清晰颓屑,代碼變動(dòng)影響面可以準(zhǔn)確評(píng)估
  • 提高開發(fā)效率,每個(gè)組件可以獨(dú)立打包耿焊,單獨(dú)調(diào)試揪惦,最多幾十秒就可以完成打包過程
  • 每個(gè)組件負(fù)責(zé)組件內(nèi)的事情,理論上只要保證組件內(nèi)部穩(wěn)定罗侯,接入工程 App 后也不會(huì)產(chǎn)生新的問題
  • 降低 App 工程編譯時(shí)間器腋,最理想的情況是,App 工程僅僅是一個(gè)空殼钩杰,用于加載各個(gè)組件

解耦纫塌,一般需要避免直接依賴,轉(zhuǎn)為間接依賴讲弄,簡(jiǎn)單來說就是依賴隔離措左。對(duì)于組件化而言,每個(gè)組件都是單獨(dú)的實(shí)現(xiàn)避除,單個(gè)組件對(duì)外提供的服務(wù)盡可能單一怎披,依賴盡可能少;同時(shí)瓶摆,依賴其它組件功能或頁(yè)面的情況下凉逛,盡可能避免直接依賴,最好依賴中間層進(jìn)行集中式管理群井,然后再進(jìn)行邏輯分發(fā)状飞。所以我們一般采用分總分的結(jié)構(gòu):組件內(nèi)部分別注冊(cè),編譯時(shí)生成匯總代碼、運(yùn)行時(shí)集中式管理诬辈,調(diào)用時(shí)處理邏輯分發(fā)酵使。

組件化需要解耦處理的幾個(gè)基礎(chǔ)模塊:

  • 頁(yè)面路由
  • 模塊間調(diào)用
  • 消息總線
  • 數(shù)據(jù)總線

下面依次介紹。

頁(yè)面路由

路由分發(fā)本質(zhì)上是把直接依賴引用轉(zhuǎn)化為中心化管理分發(fā)的一個(gè)過程自晰,由于組件化拆分后凝化,各個(gè)業(yè)務(wù)組件間不存在直接的依賴關(guān)系,所以必然要有一個(gè)統(tǒng)一收集頁(yè)面跳轉(zhuǎn)規(guī)則進(jìn)而再分發(fā)的過程酬荞。

51信用卡在 2017 年就在進(jìn)行路由化實(shí)踐搓劫,以應(yīng)對(duì)后面進(jìn)行的組件化拆分需求,并沉淀出一套自研的路由框架 U51OkDeepLink混巧,它也采用分總分結(jié)構(gòu)枪向,主要原理是組件內(nèi)注冊(cè)路由,編譯時(shí)在組件內(nèi)生成獨(dú)立的路由表咧党,并用 AOP 在編譯時(shí)做好所有組件內(nèi)路由表匯總的工作秘蛔,調(diào)用初始化方法時(shí)進(jìn)行路由表匯總,頁(yè)面跳轉(zhuǎn)時(shí)再進(jìn)行管理分發(fā)傍衡,其用法很簡(jiǎn)單:

//組件內(nèi)注冊(cè)路由
public interface SampleService {
    @Path("/main")
    @Activity(MainActivity.class)
    void startMainActivity(@Query("key") String key);
}

//其余組件喚起頁(yè)面
new DeepLinkClient(context).buildRequest("old://app/main?key=value").addQuery("key2", "2").start();

并且支持強(qiáng)大的異步特性深员,支持跳轉(zhuǎn)過程中的中間邏輯處理。

其原理圖如下

感興趣的讀者可以閱讀 Android 組件化 —— 路由設(shè)計(jì)最佳實(shí)踐 獲取更多技術(shù)細(xì)節(jié)蛙埂。

模塊間調(diào)用

組件間層次和邊界模糊問題的產(chǎn)生倦畅,根本原因是各個(gè)業(yè)務(wù)組件間的相互依賴關(guān)系混亂,為了進(jìn)行業(yè)務(wù)組件間的隔離绣的,首先要做好組件之間的服務(wù)調(diào)用解耦叠赐。

這里采用的是 ServiceLoader 的模式,組件工程目錄一般如下所示

每個(gè)組件內(nèi)一般聲明三個(gè) module:

  • api module屡江,聲明對(duì)外暴露的服務(wù)接口和對(duì)外暴露的實(shí)體類及 Event 事件
  • imp module芭概,依賴 api module,是 api module 的具體實(shí)現(xiàn)惩嘉,不對(duì)外暴露細(xì)節(jié)罢洲,不允許其他組件對(duì) imp module 進(jìn)行直接依賴
  • app module,是工程的殼文黎,可以直接運(yùn)行調(diào)試奏路,通過 SDKTemplate 創(chuàng)建生成,包含各種運(yùn)行時(shí)所需環(huán)境

業(yè)務(wù)組件之間依賴 api 庫(kù)的服務(wù)接口臊诊,imp 庫(kù)作為實(shí)現(xiàn)動(dòng)態(tài)查找。版本發(fā)布時(shí)斜脂,同時(shí)發(fā)布 api 和 imp 兩個(gè)庫(kù)抓艳,并且保證 api 和 imp 具有相同版本號(hào),這個(gè)在組件發(fā)版時(shí)統(tǒng)一管理帚戳。

 //組件內(nèi) api module 接口聲明
@Service
public interface TestService {
    void sayHello();
}

//組件內(nèi) imp module 接口實(shí)現(xiàn)
@ServiceImpl
public class TestServiceImpl implements TestService {
    @Override
    public void sayHello() {
    }
}

//跨組件調(diào)用
compile 'com.u51.android:test-lib-api:$version'

CommentService service = ServicesLoader.getInstance().getService(TestService.class);
service.sayHello();

它的實(shí)現(xiàn)原理與路由類似玷或,也是采用分總分結(jié)構(gòu)儡首,在編譯時(shí)通過 APT 生成匯總代碼,調(diào)用時(shí)動(dòng)態(tài)查找注入 Service 及其實(shí)現(xiàn)類的綁定關(guān)系偏友。

與路由初始化匯總路由表不同的是蔬胯,ServiceLoader 在調(diào)用時(shí)查找,省去了初始化的邏輯位他,Service 不會(huì)像路由這么多氛濒,查找起來不會(huì)存在遍歷太慢的問題。

消息總線

消息總線是基于 EventBus 實(shí)現(xiàn)的跨三端(Native鹅髓、Hybrid舞竿、Weex)事件管理分發(fā)組件 U51EventBus×耄跨三端是指在任意一端注冊(cè)監(jiān)聽后骗奖,在事件觸發(fā)時(shí)都可以得到響應(yīng)。

對(duì)于原生開發(fā)來說醒串,EventBus 本身可以滿足需求执桌,雖然有點(diǎn)事件滿天飛的缺點(diǎn),但是還在可接受范圍之內(nèi)芜赌。對(duì)于業(yè)務(wù)組件來說仰挣,其 Event 類需要放在 api module 中進(jìn)行暴露。

對(duì)于 Hybrid 和 Weex 來說较鼓,一般的 bridge 都是 callback 形式得到異步響應(yīng)椎木,對(duì)于全局事件通知支持不太友好。通過 bridge 通道連接 U51EventBus 消息總線博烂,打通跨三端全局的事件監(jiān)聽及分發(fā)香椎,得以實(shí)現(xiàn)任意事件可以在 Native、H5禽篱、Weex 之間相互發(fā)送和監(jiān)聽畜伐。比如,類似登陸躺率、登出操作在 Native 發(fā)出后玛界,全局已打開的 H5 或 Weex 頁(yè)面可以立即得到感知。

其實(shí)現(xiàn)原理也是采用分總分結(jié)構(gòu)悼吱,在編譯時(shí)對(duì) EventBus 進(jìn)行了定制封裝慎框,事件分發(fā)還是使用的原有的 EventBus 分發(fā)邏輯。

數(shù)據(jù)總線

數(shù)據(jù)存儲(chǔ)采用基于 Room 實(shí)現(xiàn)的統(tǒng)一 KV 存儲(chǔ)框架后添,底層數(shù)據(jù)庫(kù)依然是 sqlite笨枯,性能這塊沒有做特別強(qiáng)調(diào),強(qiáng)制其在子線程中進(jìn)行操作,用于支持日常開發(fā)中配置和業(yè)務(wù)數(shù)據(jù)的存取操作馅精。

另外严嗜,數(shù)據(jù)總線支持按模塊進(jìn)行存取,每個(gè)業(yè)務(wù)組件都可以定義自有 tag洲敢,避免字段沖突問題漫玄。

跨平臺(tái)混合開發(fā)實(shí)踐

無(wú)論從早期的 PhoneGap、Cordova压彭,還是近年來比較火的 ReactNative睦优、Weex,到最近兩年崛起的 Flutter哮塞,跨平臺(tái)混合開發(fā)一直深受眾多開發(fā)青睞刨秆。究其原因,還是其跨平臺(tái)和動(dòng)態(tài)化是原生開發(fā)所不具備的特性忆畅。

Hybrid 容器實(shí)踐

Native 和 H5 混合開發(fā)一般是比較常見的混合開發(fā)模式衡未,H5 開發(fā)效率高、迭代快速家凯、不依賴 App 發(fā)版缓醋,51信用卡眾多 App 產(chǎn)品中,有很多頁(yè)面都是用 H5 來開發(fā)绊诲,嵌入原生 App 中使用 webview 進(jìn)行加載顯示送粱。

早期 H5 容器在各個(gè) App 中分別獨(dú)立實(shí)現(xiàn),沒有統(tǒng)一的架構(gòu)和規(guī)范掂之,導(dǎo)致對(duì) H5 的支持效率較低抗俄,PG 方法(來源于 PhoneGap)的開發(fā)、測(cè)試和維護(hù)都相當(dāng)?shù)幕靵y世舰,重復(fù)性工作太多动雹。

Native 層提供一套通用性強(qiáng)、功能豐富跟压、穩(wěn)定性高的 H5 容器對(duì)業(yè)務(wù)的高速發(fā)展至關(guān)重要胰蝠。

插件管理

由于 H5 不具備直接調(diào)用原生方法,所以原生殼要提供一套通用的通信方式震蒋,一般為 JsBridge茸塞,在 Android 端,實(shí)現(xiàn) JsBridge 通信的通道一般有以下幾種:

  • shouldOverrideUrlLoading
  • addJavascriptInterface
  • onJsPrompt/onJsAlert

而通道不是關(guān)鍵查剖,怎樣管理和維護(hù) PG 方法調(diào)用才是核心钾虐。為此,我們把每個(gè)方法定義為一個(gè) Plugin笋庄,用插件的形式管理 PG 方法效扫,這樣可以做到每個(gè)插件獨(dú)立運(yùn)行效览,互不干擾。插件管理也是采用分總分結(jié)構(gòu)荡短,在各個(gè)業(yè)務(wù)組件中分別注冊(cè),編譯是通過 APT 生成匯總代碼哆键,運(yùn)行時(shí)進(jìn)行插件匯總掘托,最后調(diào)用通過 PluginManager 查找分發(fā)邏輯。

插件注冊(cè)代碼如下籍嘹,其中 onExecute() 方法在 js 調(diào)用該方法時(shí)觸發(fā)闪盔,執(zhí)行結(jié)果通過 evaluateJavaScript() 方法異步返回。

@JsPlugin(name = TestPlugin.PLUGIN_NAME, loadOnInit = false, version = 1)
public class TestPlugin extends EnNiuJsPlugin {
    public static final String PLUGIN_NAME = "TestPlugin";

    @Override
    public String getPluginName() {
        return PLUGIN_NAME;
    }
    ...
    @Override
    public boolean onExecute(String args) {
        doSomething();                    
        callbackContext.callback(...);
        return true;
    }
}

其中辱士,H5 容器和插件都具有 Activity 生命周期感知能力泪掀,插件的生命周期:

配套設(shè)施

插件統(tǒng)一通過插件管理平臺(tái)進(jìn)行維護(hù)管理,目前已有200+插件颂碘。PG 插件作為基礎(chǔ)通用功能异赫,采取集中式管理機(jī)制,任何人在新增头岔、修改插件都需要進(jìn)行相關(guān)負(fù)責(zé)人審核塔拳,以避免出現(xiàn) Android、iOS 兩端實(shí)現(xiàn)不統(tǒng)一峡竣,版本間實(shí)現(xiàn)不統(tǒng)一等問題靠抑。

插件調(diào)試通過調(diào)試平臺(tái)進(jìn)行操作,瀏覽器中打開調(diào)試地址适掰,App 端通過調(diào)試工具掃碼建立連接颂碧,即可進(jìn)行插件調(diào)試。

離線加載

Hybrid 混合開發(fā)的一大劣勢(shì)就是性能比較差类浪,打開頁(yè)面較慢载城,特別是在弱網(wǎng)情況下。由于51信用卡業(yè)務(wù)大部分都是靜態(tài)資源請(qǐng)求戚宦,參考業(yè)界做法个曙,我們實(shí)現(xiàn)了動(dòng)態(tài)下發(fā)離線包的方式來提升H5頁(yè)面打開速度。

這里細(xì)節(jié)問題不具體展開受楼。

除了以上提到的實(shí)踐外垦搬,我們還做了很多工作,比如 UI 統(tǒng)一艳汽、Back 鍵攔截猴贰、公共參數(shù)處理、PG 白名單機(jī)制河狐、H5監(jiān)控米绕、PG 方法監(jiān)控等等瑟捣,限于文章篇幅,這里不再一一列出栅干,敬請(qǐng)關(guān)注后續(xù)相關(guān)文章迈套。

Weex 容器實(shí)踐

在 Hybrid 已有配套基礎(chǔ)上,51信用卡選擇了 Weex 作為跨平臺(tái)方案碱鳞,經(jīng)過一年的踩坑填坑過程桑李,目前已經(jīng)有 20+ 個(gè)項(xiàng)目、數(shù)百個(gè) Weex 頁(yè)面在線上穩(wěn)定運(yùn)行窿给,并且贵白,目前 Weex 方案趨于成熟,已經(jīng)作為51信用卡端內(nèi)首選業(yè)務(wù)方案崩泡。

共享插件

由于 Hybrid 良好的面向接口編程特性禁荒,在進(jìn)行 Weex 基礎(chǔ)建設(shè)過程中,很方便的就把已有的插件集成進(jìn)來角撞,并且共享已沉淀的配套設(shè)施呛伴。

public class ENBridgeModule extends WXModule {
    @JSMethod
    public synchronized void send(String method, String args, JSCallback jsCallback) {
        ...
        weexWebView = weexEngine.getWeexVirtualWebView();
        EnNiuJsBridge enNiuJsBridge = weexWebView.getEnNiuJsBridge();
        enNiuJsBridge.notify(pg);
    }
}

注冊(cè) Weex 的 Module,并且每個(gè) Weex Engine 中會(huì)新建出一個(gè)虛擬 webview靴寂,用于橋接 JsBridge 進(jìn)而調(diào)用 PluginManager 進(jìn)行插件邏輯分發(fā)磷蜀。

Weex 容器實(shí)踐在之前的文章中已經(jīng)提到過一部分,具體請(qǐng)看 Weex避坑指南-理論篇 百炬,后續(xù)還將有 Weex 實(shí)踐相關(guān)的文章放出褐隆,這里不做過多篇幅的介紹,敬請(qǐng)關(guān)注后續(xù)相關(guān)文章剖踊。

工程化實(shí)踐

工程化本質(zhì)上是為了提高研發(fā)效率庶弃。51信用卡客戶端團(tuán)隊(duì)自研的大風(fēng)車管理平臺(tái),用于 App 管理德澈、持續(xù)集成歇攻、類庫(kù)管理、發(fā)版管理等梆造,圍繞客戶端研發(fā)上下游流程缴守,建立統(tǒng)一的管理入口。

目前镇辉,51信用卡 iOS 和 Android 共 30 多個(gè)應(yīng)用 App屡穗、 200 多個(gè)類庫(kù)依托大風(fēng)車平臺(tái)進(jìn)行管理。下面主要介紹下類庫(kù)管理相關(guān)內(nèi)容忽肛。

類庫(kù)管理

51信用卡目前有 100 多個(gè) Android 類庫(kù)村砂,每個(gè)類庫(kù)對(duì)應(yīng)一個(gè)獨(dú)立的 Gitlab 倉(cāng)庫(kù)。過多的獨(dú)立組件及獨(dú)立倉(cāng)庫(kù)屹逛,管理起來有些麻煩础废。

依托于大風(fēng)車平臺(tái)汛骂,所有類庫(kù)的名稱、最新版本及標(biāo)簽類型都會(huì)展示在列表頁(yè)评腺,標(biāo)簽類型對(duì)應(yīng)組件化架構(gòu)的層次結(jié)構(gòu)帘瞭,包括:基礎(chǔ)組件、單業(yè)務(wù)功能組件蒿讥、多業(yè)務(wù)功能組件图张。

在類庫(kù)詳情頁(yè),會(huì)有庫(kù)的功能描述诈悍、groupId:artifactId 依賴信息、版本歷史記錄兽埃、分支信息侥钳、README、CHANGELOG柄错、負(fù)責(zé)人等詳情信息舷夺。

所有的類庫(kù)管理工作都可以在大風(fēng)車完成,包括新建類庫(kù)售貌、類庫(kù)發(fā)版给猾、查閱相關(guān)信息等等,這大大提高了基礎(chǔ)組的研發(fā)效率颂跨,降低了團(tuán)隊(duì)間的溝通成本敢伸。

并且 App 工程中,該 App 所依賴的所有類庫(kù)信息一目了然恒削,在多人維護(hù)池颈、多類庫(kù)并行開發(fā)、類庫(kù)頻繁發(fā)版的情況下钓丰,依賴類庫(kù)信息 check 更加便捷躯砰。

版本管理

由于類庫(kù)之間是倉(cāng)庫(kù)隔離,所以它們的依賴關(guān)系是 maven 依賴携丁,所有類庫(kù)的 aar 包都需要發(fā)布到內(nèi)部 maven 服務(wù)器上琢歇,上傳工作由 PublishMavenPlugin 完成。

SNAPSHOT 預(yù)覽版

對(duì)于開發(fā)調(diào)試階段梦鉴,每個(gè)類庫(kù)自帶 DemoApp 工程李茫,所以采用源碼依賴;開發(fā)完成后尚揣,類庫(kù)使用SNAPSHOT版本(比如 1.0.0-SNAPSHOT)發(fā)布到 maven 服務(wù)器涌矢,接入 App 工程后 push 代碼觸發(fā)大風(fēng)車打包,進(jìn)行集成測(cè)試快骗。需要修改類庫(kù)時(shí)娜庇,可以再重復(fù)發(fā)布相同版本的SNAPSHOT版本塔次。

SNAPSHOT版本可以在開發(fā)同學(xué)自己的機(jī)器上進(jìn)行打包發(fā)布。

正式版

對(duì)于發(fā)布階段名秀,類庫(kù)必須使用正式版本發(fā)布励负,由于正式版本不可重復(fù)發(fā)布,這也就要求開發(fā)同學(xué)保證每個(gè)正式版本的版本質(zhì)量匕得,在正式發(fā)布前都應(yīng)達(dá)到發(fā)布標(biāo)準(zhǔn)继榆。

由于類庫(kù)內(nèi)部也存在相互依賴的情況,所以在類庫(kù)正式發(fā)布時(shí)汁掠,不允許依賴包含SNAPSHOT版本的類庫(kù)略吨,DependencyCheck工作也會(huì)在 PublishMavenPlugin 完成。

同時(shí)考阱,正式版本不允許開發(fā)同學(xué)在本機(jī)打包發(fā)布翠忠,PublishMavenPlugin 會(huì)檢測(cè)是否在云端打包環(huán)境。功能分支經(jīng) CodeReview 后合并 master 分支乞榨,然后創(chuàng)建對(duì)應(yīng)版本的 tag秽之,觸發(fā)大風(fēng)車進(jìn)行打包發(fā)布工作,發(fā)布成功后吃既,會(huì)郵件通知 Android 組同學(xué)考榨,并附帶 CHANGELOG。

依賴管理

依賴傳遞

App 工程下采用 compile 依賴鹦倚,compile 會(huì)解析類庫(kù) maven 包中的 pom 文件河质,進(jìn)而間接依賴 pom 文件中聲明的其他類庫(kù),也就是依賴傳遞震叙。正常情況下愤诱,依賴傳遞會(huì)減少不必要的類庫(kù)聲明,當(dāng)出現(xiàn)版本沖突時(shí)會(huì)自動(dòng)處理 merge 操作捐友。

但是淫半,在多人協(xié)同工作、多類庫(kù)并行開發(fā)情況下匣砖,事情變得有些復(fù)雜科吭。考慮一種情況猴鲫,應(yīng)用 A 依賴類庫(kù) B对人,類庫(kù) B 依賴類庫(kù) C,正常情況下拂共,A 中只需要聲明依賴 B 即可牺弄,C 會(huì)被依賴傳遞過去。如果 C 中改變了方法簽名宜狐,并且在應(yīng)用 A 中顯示聲明依賴 C势告,編譯時(shí)和運(yùn)行時(shí)會(huì)分別出現(xiàn)什么情況蛇捌?在編譯時(shí)沒有問題,正常編譯通過咱台;在運(yùn)行時(shí)络拌,當(dāng)運(yùn)行到類庫(kù) B 中使用的類庫(kù) C 中被改變簽名的方法時(shí),App crash回溺。這是因?yàn)榇好常琺aven 在處理類庫(kù)版本 merge 時(shí),會(huì)將 C 升級(jí)到最高版本遗遵,而此時(shí) B 中已經(jīng)編譯好的 class 中使用的還是老版本 C 中的方法萍恕。

為了處理這個(gè)問題,我們使用 APICheckGradlePlugin 在編譯時(shí)進(jìn)行 check 操作车要,當(dāng)發(fā)現(xiàn)被調(diào)用的方法找不到時(shí)雄坪,主動(dòng)報(bào)錯(cuò),將錯(cuò)誤提前暴露在編譯期屯蹦,而非在運(yùn)行時(shí)。同時(shí)內(nèi)部強(qiáng)調(diào) API 接口的向下兼容性绳姨,不用的方法標(biāo)記為廢棄登澜,而非直接修改其方法簽名或刪除方法。

APICheckGradlePlugin 核心代碼如下:

try {
    c.getClassPool().get(callClassName)
    isClassNotFound = false
    m.getMethod()
} catch (NotFoundException e) {
    if (isClassNotFound) {
        dealException(String.format("在%s類中的第%d行是用到的%s類不存在", className, line, callClassName))
    } else {
        dealException(String.format("在%s類中的第%d行是用到的%s類的%s方法不存在", className, line, callClassName, methodName))
    }
}

多module發(fā)布

上文中提到飘庄,在多業(yè)務(wù)組件庫(kù)工程中會(huì)有多個(gè) module脑蠕,一個(gè) api module,一個(gè) imp module跪削,在使用 DemoApp 編譯調(diào)試時(shí)采用源碼依賴, imp module 依賴 api module,App 依賴 imp module传货,這樣在打包上傳 maven 時(shí)衰琐,會(huì)出現(xiàn)無(wú)法一起上傳的問題;并且我們也要確保 api 和 imp 的版本號(hào)一致毫玖。為了解決這個(gè)問題掀虎,需要在上傳時(shí)動(dòng)態(tài)修改他們的 pom 文件,代碼如下:

modifyPom { pom ->
    pom.dependencies.findAll { dep -> dep.groupId == rootProject.name }.collect { dep ->
        dep.groupId = pom.groupId = rootProject.groupId
        dep.version = pom.version = rootProject.sdkVersion
    }
}

一鍵創(chuàng)建項(xiàng)目

模板工程

由于每個(gè)新建組件類庫(kù)的 App 工程需要運(yùn)行時(shí)環(huán)境基本相同付枫,包括網(wǎng)絡(luò)環(huán)境烹玉、調(diào)試環(huán)境、gradle 配置阐滩、通用依賴配置等等二打,這些重復(fù)性的工作最好放在一起統(tǒng)一處理。為此掂榔,我們創(chuàng)建了組件庫(kù)的模板工程继效,只需要 clone 下來模板倉(cāng)庫(kù)症杏,然后修改一些代碼即可開發(fā)需求代碼。

一鍵創(chuàng)建類庫(kù)

但是莲趣,這種方式依然有很多共性的工作鸳慈,比如 clone 代碼、修改類庫(kù)名喧伞、修改 groupId:artifactId走芋、創(chuàng)建新的類庫(kù)倉(cāng)庫(kù)、push 代碼潘鲫、在大風(fēng)車中新建類庫(kù)關(guān)聯(lián)倉(cāng)庫(kù)地址等等操作翁逞。這些共性操作仍然可以用機(jī)器來操作,所以我們?cè)诖箫L(fēng)車新建類庫(kù)這一步中溉仑,把前面所有要做的事情全部做完挖函,只需要在新建類庫(kù)時(shí)填入必要的參數(shù),一鍵就可以創(chuàng)建出可用的類庫(kù)項(xiàng)目浊竟。


?

一鍵創(chuàng)建應(yīng)用

隨著我司 App 越來越多怨喘,新建 App 的配置同樣面臨類庫(kù)剛開始時(shí)的困擾,新建 App 與新建類庫(kù)本質(zhì)上是一樣的振定,只不過所需參數(shù)更多一些必怜,并且這些參數(shù)可能不固定,有些 App 需要有些 App 不需要后频。參考類庫(kù)梳庆,我們提取共性操作,創(chuàng)建了 App 的模板工程卑惜,并且對(duì)接大風(fēng)車膏执,一鍵即可創(chuàng)建出 App 工程,那些可變的參數(shù)留在模板工程中按需手動(dòng)配置露久。

模塊負(fù)責(zé)人

在組件化初步開始時(shí)更米,我們的每個(gè)模塊都有固定的負(fù)責(zé)人,每個(gè)人手上都有固定的若干個(gè)模塊毫痕,責(zé)任人對(duì)自己負(fù)責(zé)的模塊負(fù)責(zé)壳快。

但是隨著組內(nèi)的人員變動(dòng)和業(yè)務(wù)變動(dòng),導(dǎo)致一些模塊頻繁易主镇草,一些模塊的文檔長(zhǎng)期處于不被維護(hù)狀態(tài)眶痰,README 和 CHANGELOG 常年失修。

依賴大風(fēng)車的類庫(kù)管理梯啤,重新為每個(gè)模塊指定負(fù)責(zé)人竖伯,并且梳理現(xiàn)存類庫(kù)哪些缺失文檔,進(jìn)行補(bǔ)全。自從大風(fēng)車自動(dòng)抄送類庫(kù)發(fā)版 CHANGELOG 后七婴,CHANGELOG 不全的情況也大幅改善祟偷,基本每個(gè)新的版本都會(huì)附上該版本所做修改。

同時(shí)打厘,我們也強(qiáng)調(diào) CodeReview 機(jī)制修肠,每個(gè)模塊在提測(cè)前進(jìn)行 CodeReview,強(qiáng)制merge request 必須有人點(diǎn)贊后才能合并 master 分支等等代碼審查機(jī)制户盯。未來嵌施,我們可能會(huì)進(jìn)一步實(shí)踐負(fù)責(zé)人 backup 方案,主副負(fù)責(zé)人相互 review莽鸭,擴(kuò)大大家技術(shù)視野的同時(shí)吗伤,可以進(jìn)一步提高大家的主人翁意識(shí)。

總結(jié)

好的架構(gòu)不是設(shè)計(jì)出來的硫眨,而是演進(jìn)出來的足淆。本文簡(jiǎn)單闡述了51信用卡 Android 架構(gòu)演進(jìn)的一些實(shí)踐經(jīng)驗(yàn),同時(shí)我們堅(jiān)信技術(shù)方案沒有最優(yōu)解礁阁,重要的是要選擇選擇適合自己的巧号。脫離所處環(huán)境和問題本身談技術(shù)方案,都將不能得到適合自身的開發(fā)架構(gòu)姥闭。同時(shí)丹鸿,我們也應(yīng)當(dāng)吸取和借鑒業(yè)界優(yōu)秀的架構(gòu)和設(shè)計(jì)理念,并將其根據(jù)自身適用場(chǎng)景加以改造泣栈,在理論和實(shí)踐中逐漸交替探索演進(jìn)。

當(dāng)然弥姻,我們目前所使用的架構(gòu)依然存在一些問題南片,比如組件拆分不完全、主工程業(yè)務(wù)仍然很多庭敦、CodeReview 機(jī)制不健全疼进、代碼掃描不夠嚴(yán)格、一些組件庫(kù)沒有嚴(yán)格按照 api 工程來改造秧廉、一些老的組件依然沒有 api module等等問題伞广。我們也應(yīng)該看到,正是因?yàn)檫@些實(shí)際的問題在推動(dòng)我們進(jìn)行技術(shù)改造疼电,架構(gòu)升級(jí)嚼锄。同時(shí),我們也要審視行業(yè)內(nèi)大的方向蔽豺,緊跟技術(shù)趨勢(shì)区丑,主動(dòng)擁抱變化,畢竟技術(shù)世界唯一不變的,便是變化沧侥。

原文地址:https://juejin.im/post/6844903712570228743#heading-28

文章每周持續(xù)更新可霎,可以微信搜索「 程序猿養(yǎng)成中心 」第一時(shí)間閱讀和催更(比博客早一到兩篇喲),另外“點(diǎn)擊公眾號(hào)下方面試/更多資料”宴杀,直接免費(fèi)獲取一二線互聯(lián)網(wǎng)企業(yè)Android開發(fā)崗面試題匯總(答案解析)以及Android架構(gòu)知識(shí)點(diǎn)匯總pdf+超清Android進(jìn)階思維腦圖癣朗。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市旺罢,隨后出現(xiàn)的幾起案子旷余,更是在濱河造成了極大的恐慌,老刑警劉巖主经,帶你破解...
    沈念sama閱讀 216,591評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件荣暮,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡罩驻,警方通過查閱死者的電腦和手機(jī)穗酥,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,448評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來惠遏,“玉大人砾跃,你說我怎么就攤上這事〗谒保” “怎么了抽高?”我有些...
    開封第一講書人閱讀 162,823評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)透绩。 經(jīng)常有香客問我翘骂,道長(zhǎng),這世上最難降的妖魔是什么帚豪? 我笑而不...
    開封第一講書人閱讀 58,204評(píng)論 1 292
  • 正文 為了忘掉前任碳竟,我火速辦了婚禮,結(jié)果婚禮上狸臣,老公的妹妹穿的比我還像新娘莹桅。我一直安慰自己,他們只是感情好烛亦,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,228評(píng)論 6 388
  • 文/花漫 我一把揭開白布诈泼。 她就那樣靜靜地躺著,像睡著了一般煤禽。 火紅的嫁衣襯著肌膚如雪铐达。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,190評(píng)論 1 299
  • 那天檬果,我揣著相機(jī)與錄音娶桦,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛衷畦,可吹牛的內(nèi)容都是我干的栗涂。 我是一名探鬼主播,決...
    沈念sama閱讀 40,078評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼祈争,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼斤程!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起菩混,我...
    開封第一講書人閱讀 38,923評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤忿墅,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后沮峡,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體疚脐,經(jīng)...
    沈念sama閱讀 45,334評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,550評(píng)論 2 333
  • 正文 我和宋清朗相戀三年邢疙,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了棍弄。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,727評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡疟游,死狀恐怖呼畸,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情颁虐,我是刑警寧澤蛮原,帶...
    沈念sama閱讀 35,428評(píng)論 5 343
  • 正文 年R本政府宣布,位于F島的核電站另绩,受9級(jí)特大地震影響儒陨,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜笋籽,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,022評(píng)論 3 326
  • 文/蒙蒙 一蹦漠、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧干签,春花似錦津辩、人聲如沸拆撼。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,672評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)闸度。三九已至竭贩,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間莺禁,已是汗流浹背留量。 一陣腳步聲響...
    開封第一講書人閱讀 32,826評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人楼熄。 一個(gè)月前我還...
    沈念sama閱讀 47,734評(píng)論 2 368
  • 正文 我出身青樓忆绰,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親可岂。 傳聞我的和親對(duì)象是個(gè)殘疾皇子错敢,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,619評(píng)論 2 354