51信用卡 Android 架構演進實踐

本文首發於51NB技術公衆號,原文連接 51信用卡Android架構演進實踐前端

隨着業務的快速擴張,本來小做坊式的單個工程的開發模式越來與不能知足實際需求。早在兩年多之前,51信用卡管家就向下沉澱出了單獨的公用基礎庫,一些通用的功能組件和個別獨立的業務被拆分紅 SDK,造成了一套中型項目、多人並行的開發模式,也爲將來組件化拆分作準備。java

image-20181023162224348

這套框架運行了一段時間以後,伴隨着單應用內業務需求的增長、開發人員數量的增多、基礎庫數量的膨脹,致使了一些問題:android

  • 主工程代碼耦合嚴重,牽一髮而動全身
  • 需求測試影響面大,不能聚焦單一業務模塊
  • 主工程代碼愈來愈多,編譯耗時
  • 依賴倒置,業務代碼依賴App工程
  • SDK 界限模糊,基礎庫和業務庫界限不明確
  • 業務模塊間能夠任意依賴調用,依賴規則不明確
  • 類庫愈來愈多,很差管理

除了以上問題,動態化需求也愈來愈強烈,依賴 Hybrid + H5 打開頁面慢的問題也凸顯出來。git

這些問題推進咱們更進一步的升級開發構架。github

組件化 or 插件化

動態化

最近兩年,插件化框架層出不窮,各大廠都放出了自家開源的插件化框架。做爲 Native 動態化與性能兼顧的插件化方案,不少公司選擇插件化做爲動態化技術方案。動態性一般有兩部分的做用:一是動態熱修復;二是動態下發業務插件。對於第一點,咱們有熱修復框架能夠完成這部分工做;對於第二點,咱們使用了 Hybrid 加載H5的方式實現,雖然性能上有所欠缺,但徹底切到 Native 來作有點推倒重來的意思,而且跟業界同窗交流後,對於動態下發業務插件用到的狀況也很少,業務更新主要仍是依靠 App 升級來實現。技術方案沒有最優解,選擇適合本身的纔是最好的。web

因爲插件化也存在一些弊端,好比不可避免的 hook framework、修改 aapt、包裝 Gradle Plugin、代理組件等等很是規操做,平常維護也是一筆不小的開銷,穩定性、兼容性、新版本適配等等問題都須要考慮進去。對於 Android 端是否使用插件化,公司內部作過一些討論,結論是不急着上,邊走邊看,先把業務組件拆分出來再說。sql

現在回過頭看,自從 Android P發佈以來,限制 hook framework 後,插件化逐漸開始式微,後面走向大機率是維護成本愈來愈高,成本收益比逐漸下降,最終棄坑不用。數據庫

除了插件化外,動態化方案近兩年比較火的就是以 ReactNative、Weex 爲表明的大前端方向,結合51信用卡的實際狀況,最終選擇擁抱大前端, Weex 做爲動態化方案,以 Native 爲主, Hybrid 離線化方案爲輔,Weex 逐步迭代的架構開發模式。編程

Weex 的基礎建設和前端同窗合做,歷經大半年時間,目前已經穩定應用在51信用卡各個 App 上,Weex 做爲動態化頁面的首選方案,已經完成了線上數百個頁面的開發需求。配合離線化方案,各項性能指標也都達到要求。api

組件分離

代碼解耦與代碼隔離,最有效的方案是工程隔離。審視咱們最初的方案,每一個 SDK 對應單獨的倉庫,經過 maven 依賴,經過工程分離隔離代碼,這種方案沒有問題,只不過須要往前更近一步,各個業務模塊也須要獨立主工程,拆分紅獨立的業務組件。

同時,劃分清楚代碼邊界,控制依賴關係,梳理清楚層次結構,最終造成以下圖所示的架構。

組件化層次.001

總體架構上提供三種容器:

  • Native 容器,採用組件化架構,用於原生業務開發
  • Hybrid 容器,webview 加載 H5,配合離線化方案
  • Weex 容器,用於編寫常規的頁面,js 動態轉化成 Native 控件,自然具備動態化特性,配合離線化方案,達到頁面秒開的效果,同時共用 Hybrid 沉澱出的比較完善的 PG 方法

同時,Hybrid 和 Weex 依賴於原生提供的方法,經過 JsBridge 進行通訊,目前共有 200 多個 PG 方法供 js 調用。長遠來看,這三種容器並不會互相取代,相反地,它們應該是相互依存、取長補短、長期共存的狀態。

組件化實踐

Native 容器對應上圖中各個層級的定義:

  • 工程 App,各個應用工程,目前已有十多個應用並行開發,51信用卡管家做爲平臺應用,其他應用爲獨立的業務工程應用
  • 業務組件,獨立的業務組件,通常爲複合業務組件,api 與實現分離,相互之間依賴隔離
  • 基礎業務 SDK,獨立的小的單功能模塊,提供基礎功能,目前這一層級中還包含遺留未改造的部分業務組件
  • 基礎 Lib,業務無關的基礎組件

組件化拆分的核心訴求是解耦合,提升組件內聚,因此應該從訴求出發,在沿用當下開發模式,而且不強依賴組件化框架的狀況下,逐漸的進行組件化拆分。

經過工程隔離進而進行組件化拆分後,基本能夠解決上面提到的問題:

  • 高內聚,低耦合,代碼邊界清晰,代碼變更影響面能夠準確評估
  • 提升開發效率,每一個組件能夠獨立打包,單獨調試,最多幾十秒就能夠完成打包過程
  • 每一個組件負責組件內的事情,理論上只要保證組件內部穩定,接入工程 App 後也不會產生新的問題
  • 下降 App 工程編譯時間,最理想的狀況是,App 工程僅僅是一個空殼,用於加載各個組件

解耦,通常須要避免直接依賴,轉爲間接依賴,簡單來講就是依賴隔離。對於組件化而言,每一個組件都是單獨的實現,單個組件對外提供的服務儘量單一,依賴儘量少;同時,依賴其它組件功能或頁面的狀況下,儘量避免直接依賴,最好依賴中間層進行集中式管理,而後再進行邏輯分發。因此咱們通常採用分總分的結構:組件內部分別註冊,編譯時生成彙總代碼、運行時集中式管理,調用時處理邏輯分發。

image-20181026181156315

組件化須要解耦處理的幾個基礎模塊:

  • 頁面路由
  • 模塊間調用
  • 消息總線
  • 數據總線

下面依次介紹。

頁面路由

路由分發本質上是把直接依賴引用轉化爲中心化管理分發的一個過程,因爲組件化拆分後,各個業務組件間不存在直接的依賴關係,因此必然要有一個統一收集頁面跳轉規則進而再分發的過程。

51信用卡在 2017 年就在進行路由化實踐,以應對後面進行的組件化拆分需求,並沉澱出一套自研的路由框架 U51OkDeepLink,它也採用分總分結構,主要原理是組件內註冊路由,編譯時在組件內生成獨立的路由表,並用 AOP 在編譯時作好全部組件內路由表彙總的工做,調用初始化方法時進行路由表彙總,頁面跳轉時再進行管理分發,其用法很簡單:

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

//其他組件喚起頁面
new DeepLinkClient(context).buildRequest("old://app/main?key=value").addQuery("key2", "2").start();
複製代碼

而且支持強大的異步特性,支持跳轉過程當中的中間邏輯處理。

其原理圖以下

router

感興趣的讀者能夠閱讀 Android 組件化 —— 路由設計最佳實踐 獲取更多技術細節。

模塊間調用

組件間層次和邊界模糊問題的產生,根本緣由是各個業務組件間的相互依賴關係混亂,爲了進行業務組件間的隔離,首先要作好組件之間的服務調用解耦。

這裏採用的是 ServiceLoader 的模式,組件工程目錄通常以下所示

image-20181024204942179

每一個組件內通常聲明三個 module:

  • api module,聲明對外暴露的服務接口和對外暴露的實體類及 Event 事件
  • imp module,依賴 api module,是 api module 的具體實現,不對外暴露細節,不容許其餘組件對 imp module 進行直接依賴
  • app module,是工程的殼,能夠直接運行調試,經過 SDKTemplate 建立生成,包含各類運行時所需環境

業務組件之間依賴 api 庫的服務接口,imp 庫做爲實現動態查找。版本發佈時,同時發佈 api 和 imp 兩個庫,而且保證 api 和 imp 具備相同版本號,這個在組件發版時統一管理。

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

//組件內 imp module 接口實現
@ServiceImpl
public class TestServiceImpl implements TestService {
    @Override
    public void sayHello() {
    }
}

//跨組件調用
compile 'com.u51.android:test-lib-api:$version'

CommentService service = ServicesLoader.getInstance().getService(TestService.class);
service.sayHello();
複製代碼

它的實現原理與路由相似,也是採用分總分結構,在編譯時經過 APT 生成彙總代碼,調用時動態查找注入 Service 及其實現類的綁定關係。

與路由初始化彙總路由表不一樣的是,ServiceLoader 在調用時查找,省去了初始化的邏輯,Service 不會像路由這麼多,查找起來不會存在遍歷太慢的問題。

消息總線

消息總線是基於 EventBus 實現的跨三端(Native、Hybrid、Weex)事件管理分發組件 U51EventBus。跨三端是指在任意一端註冊監聽後,在事件觸發時均可以獲得響應。

對於原生開發來講,EventBus 自己能夠知足需求,雖然有點事件滿天飛的缺點,可是還在可接受範圍以內。對於業務組件來講,其 Event 類須要放在 api module 中進行暴露。

對於 Hybrid 和 Weex 來講,通常的 bridge 都是 callback 形式獲得異步響應,對於全局事件通知支持不太友好。經過 bridge 通道鏈接 U51EventBus 消息總線,打通跨三端全局的事件監聽及分發,得以實現任意事件能夠在 Native、H五、Weex 之間相互發送和監聽。好比,相似登錄、登出操做在 Native 發出後,全局已打開的 H5 或 Weex 頁面能夠當即獲得感知。

其實現原理也是採用分總分結構,在編譯時對 EventBus 進行了定製封裝,事件分發仍是使用的原有的 EventBus 分發邏輯。

數據總線

數據存儲採用基於 Room 實現的統一 KV 存儲框架,底層數據庫依然是 sqlite,性能這塊沒有作特別強調,強制其在子線程中進行操做,用於支持平常開發中配置和業務數據的存取操做。

另外,數據總線支持按模塊進行存取,每一個業務組件均可以定義自有 tag,避免字段衝突問題。

跨平臺混合開發實踐

不管從早期的 PhoneGap、Cordova,仍是近年來比較火的 ReactNative、Weex,到最近兩年崛起的 Flutter,跨平臺混合開發一直深受衆多開發青睞。究其緣由,仍是其跨平臺和動態化是原生開發所不具有的特性。

Hybrid 容器實踐

Native 和 H5 混合開發通常是比較常見的混合開發模式,H5 開發效率高、迭代快速、不依賴 App 發版,51信用卡衆多 App 產品中,有不少頁面都是用 H5 來開發,嵌入原生 App 中使用 webview 進行加載顯示。

早期 H5 容器在各個 App 中分別獨立實現,沒有統一的架構和規範,致使對 H5 的支持效率較低,PG 方法(來源於 PhoneGap)的開發、測試和維護都至關的混亂,重複性工做太多。

Native 層提供一套通用性強、功能豐富、穩定性高的 H5 容器對業務的高速發展相當重要。

image-20181105173903973

插件管理

因爲 H5 不具有直接調用原生方法,因此原生殼要提供一套通用的通訊方式,通常爲 JsBridge,在 Android 端,實現 JsBridge 通訊的通道通常有如下幾種:

  • shouldOverrideUrlLoading
  • addJavascriptInterface
  • onJsPrompt/onJsAlert

而通道不是關鍵,怎樣管理和維護 PG 方法調用纔是核心。爲此,咱們把每一個方法定義爲一個 Plugin,用插件的形式管理 PG 方法,這樣能夠作到每一個插件獨立運行,互不干擾。插件管理也是採用分總分結構,在各個業務組件中分別註冊,編譯是經過 APT 生成彙總代碼,運行時進行插件彙總,最後調用經過 PluginManager 查找分發邏輯。

插件註冊代碼以下,其中 onExecute() 方法在 js 調用該方法時觸發,執行結果經過 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 生命週期感知能力,插件的生命週期:

image-20181105191433330

配套設施

插件統一經過插件管理平臺進行維護管理,目前已有200+插件。PG 插件做爲基礎通用功能,採起集中式管理機制,任何人在新增、修改插件都須要進行相關負責人審覈,以免出現 Android、iOS 兩端實現不統一,版本間實現不統一等問題。

image-20181105191949792

插件調試經過調試平臺進行操做,瀏覽器中打開調試地址,App 端經過調試工具掃碼創建鏈接,便可進行插件調試。

image-20181105192429943

離線加載

Hybrid 混合開發的一大劣勢就是性能比較差,打開頁面較慢,特別是在弱網狀況下。因爲51信用卡業務大部分都是靜態資源請求,參考業界作法,咱們實現了動態下發離線包的方式來提高H5頁面打開速度。

lixianbao

這裏細節問題不具體展開。

除了以上提到的實踐外,咱們還作了不少工做,好比 UI 統1、Back 鍵攔截、公共參數處理、PG 白名單機制、H5監控、PG 方法監控等等,限於文章篇幅,這裏再也不一一列出,敬請關注後續相關文章。

Weex 容器實踐

在 Hybrid 已有配套基礎上,51信用卡選擇了 Weex 做爲跨平臺方案,通過一年的踩坑填坑過程,目前已經有 20+ 個項目、數百個 Weex 頁面在線上穩定運行,而且,目前 Weex 方案趨於成熟,已經做爲51信用卡端內首選業務方案。

共享插件

因爲 Hybrid 良好的面向接口編程特性,在進行 Weex 基礎建設過程當中,很方便的就把已有的插件集成進來,而且共享已沉澱的配套設施。

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);
    }
}
複製代碼

註冊 Weex 的 Module,而且每一個 Weex Engine 中會新建出一個虛擬 webview,用於橋接 JsBridge 進而調用 PluginManager 進行插件邏輯分發。

Weex 容器實踐在以前的文章中已經提到過一部分,具體請看 Weex避坑指南-理論篇 ,後續還將有 Weex 實踐相關的文章放出,這裏不作過多篇幅的介紹,敬請關注後續相關文章。

工程化實踐

工程化本質上是爲了提升研發效率。51信用卡客戶端團隊自研的大風車管理平臺,用於 App 管理、持續集成、類庫管理、發版管理等,圍繞客戶端研發上下游流程,創建統一的管理入口。

目前,51信用卡 iOS 和 Android 共 30 多個應用 App、 200 多個類庫依託大風車平臺進行管理。下面主要介紹下類庫管理相關內容。

類庫管理

51信用卡目前有 100 多個 Android 類庫,每一個類庫對應一個獨立的 Gitlab 倉庫。過多的獨立組件及獨立倉庫,管理起來有些麻煩。

image-20181030193630574

依託於大風車平臺,全部類庫的名稱、最新版本及標籤類型都會展現在列表頁,標籤類型對應組件化架構的層次結構,包括:基礎組件、單業務功能組件、多業務功能組件。

在類庫詳情頁,會有庫的功能描述、groupId:artifactId 依賴信息、版本歷史記錄、分支信息、README、CHANGELOG、負責人等詳情信息。

全部的類庫管理工做均可以在大風車完成,包括新建類庫、類庫發版、查閱相關信息等等,這大大提升了基礎組的研發效率,下降了團隊間的溝通成本。

而且 App 工程中,該 App 所依賴的全部類庫信息一目瞭然,在多人維護、多類庫並行開發、類庫頻繁發版的狀況下,依賴類庫信息 check 更加便捷。

image-20181031160854953

版本管理

因爲類庫之間是倉庫隔離,因此它們的依賴關係是 maven 依賴,全部類庫的 aar 包都須要發佈到內部 maven 服務器上,上傳工做由 PublishMavenPlugin 完成。

SNAPSHOT 預覽版

對於開發調試階段,每一個類庫自帶 DemoApp 工程,因此採用源碼依賴;開發完成後,類庫使用SNAPSHOT版本(好比 1.0.0-SNAPSHOT)發佈到 maven 服務器,接入 App 工程後 push 代碼觸發大風車打包,進行集成測試。須要修改類庫時,能夠再重複發佈相同版本的SNAPSHOT版本。

SNAPSHOT版本能夠在開發同窗本身的機器上進行打包發佈。

正式版

對於發佈階段,類庫必須使用正式版本發佈,因爲正式版本不可重複發佈,這也就要求開發同窗保證每一個正式版本的版本質量,在正式發佈前都應達到發佈標準。

因爲類庫內部也存在相互依賴的狀況,因此在類庫正式發佈時,不容許依賴包含SNAPSHOT版本的類庫,DependencyCheck工做也會在 PublishMavenPlugin 完成。

同時,正式版本不容許開發同窗在本機打包發佈,PublishMavenPlugin 會檢測是否在雲端打包環境。功能分支經 CodeReview 後合併 master 分支,而後建立對應版本的 tag,觸發大風車進行打包發佈工做,發佈成功後,會郵件通知 Android 組同窗,並附帶 CHANGELOG。

image-20181105204154472

依賴管理

依賴傳遞

App 工程下采用 compile 依賴,compile 會解析類庫 maven 包中的 pom 文件,進而間接依賴 pom 文件中聲明的其餘類庫,也就是依賴傳遞。正常狀況下,依賴傳遞會減小沒必要要的類庫聲明,當出現版本衝突時會自動處理 merge 操做。

可是,在多人協同工做、多類庫並行開發狀況下,事情變得有些複雜。考慮一種狀況,應用 A 依賴類庫 B,類庫 B 依賴類庫 C,正常狀況下,A 中只須要聲明依賴 B 便可,C 會被依賴傳遞過去。若是 C 中改變了方法簽名,而且在應用 A 中顯示聲明依賴 C,編譯時和運行時會分別出現什麼狀況?在編譯時沒有問題,正常編譯經過;在運行時,當運行到類庫 B 中使用的類庫 C 中被改變簽名的方法時,App crash。這是由於,maven 在處理類庫版本 merge 時,會將 C 升級到最高版本,而此時 B 中已經編譯好的 class 中使用的仍是老版本 C 中的方法。

爲了處理這個問題,咱們使用 APICheckGradlePlugin 在編譯時進行 check 操做,當發現被調用的方法找不到時,主動報錯,將錯誤提早暴露在編譯期,而非在運行時。同時內部強調 API 接口的向下兼容性,不用的方法標記爲廢棄,而非直接修改其方法簽名或刪除方法。

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發佈

上文中提到,在多業務組件庫工程中會有多個 module,一個 api module,一個 imp module,在使用 DemoApp 編譯調試時採用源碼依賴, imp module 依賴 api module,App 依賴 imp module,這樣在打包上傳 maven 時,會出現沒法一塊兒上傳的問題;而且咱們也要確保 api 和 imp 的版本號一致。爲了解決這個問題,須要在上傳時動態修改他們的 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
    }
}
複製代碼

一鍵建立項目

模板工程

因爲每一個新建組件類庫的 App 工程須要運行時環境基本相同,包括網絡環境、調試環境、gradle 配置、通用依賴配置等等,這些重複性的工做最好放在一塊兒統一處理。爲此,咱們建立了組件庫的模板工程,只須要 clone 下來模板倉庫,而後修改一些代碼便可開發需求代碼。

一鍵建立類庫

可是,這種方式依然有不少共性的工做,好比 clone 代碼、修改類庫名、修改 groupId:artifactId、建立新的類庫倉庫、push 代碼、在大風車中新建類庫關聯倉庫地址等等操做。這些共性操做仍然能夠用機器來操做,因此咱們在大風車新建類庫這一步中,把前面全部要作的事情所有作完,只須要在新建類庫時填入必要的參數,一鍵就能夠建立出可用的類庫項目。

image-20181031201425732

一鍵建立應用

隨着我司 App 愈來愈多,新建 App 的配置一樣面臨類庫剛開始時的困擾,新建 App 與新建類庫本質上是同樣的,只不過所需參數更多一些,而且這些參數可能不固定,有些 App 須要有些 App 不須要。參考類庫,咱們提取共性操做,建立了 App 的模板工程,而且對接大風車,一鍵便可建立出 App 工程,那些可變的參數留在模板工程中按需手動配置。

模塊負責人

在組件化初步開始時,咱們的每一個模塊都有固定的負責人,每一個人手上都有固定的若干個模塊,責任人對本身負責的模塊負責。

可是隨着組內的人員變更和業務變更,致使一些模塊頻繁易主,一些模塊的文檔長期處於不被維護狀態,README 和 CHANGELOG 常年失修。

依賴大風車的類庫管理,從新爲每一個模塊指定負責人,而且梳理現存類庫哪些缺失文檔,進行補全。自從大風車自動抄送類庫發版 CHANGELOG 後,CHANGELOG 不全的狀況也大幅改善,基本每一個新的版本都會附上該版本所作修改。

同時,咱們也強調 CodeReview 機制,每一個模塊在提測前進行 CodeReview,強制merge request 必須有人點贊後才能合併 master 分支等等代碼審查機制。將來,咱們可能會進一步實踐負責人 backup 方案,主副負責人相互 review,擴大你們技術視野的同時,能夠進一步提升你們的主人翁意識。

總結

好的架構不是設計出來的,而是演進出來的。本文簡單闡述了51信用卡 Android 架構演進的一些實踐經驗,同時咱們堅信技術方案沒有最優解,重要的是要選擇選擇適合本身的。脫離所處環境和問題自己談技術方案,都將不能獲得適合自身的開發架構。同時,咱們也應當吸收和借鑑業界優秀的架構和設計理念,並將其根據自身適用場景加以改造,在理論和實踐中逐漸交替探索演進。

固然,咱們目前所使用的架構依然存在一些問題,好比組件拆分不徹底、主工程業務仍然不少、CodeReview 機制不健全、代碼掃描不夠嚴格、一些組件庫沒有嚴格按照 api 工程來改造、一些老的組件依然沒有 api module等等問題。咱們也應該看到,正是由於這些實際的問題在推進咱們進行技術改造,架構升級。同時,咱們也要審視行業內大的方向,緊跟技術趨勢,主動擁抱變化,畢竟技術世界惟一不變的,即是變化。

相關文章
相關標籤/搜索