51信用卡 Android 架構演進實踐

w4lle發表於2018-11-13

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

隨著業務的快速擴張,原本小作坊式的單個工程的開發模式越來與不能滿足實際需求。早在兩年多以前,51信用卡管家就向下沉澱出了單獨的公用基礎庫,一些通用的功能元件和個別獨立的業務被拆分成 SDK,形成了一套中型專案、多人並行的開發模式,也為未來元件化拆分做準備。

image-20181023162224348

這套框架執行了一段時間之後,伴隨著單應用內業務需求的增加、開發人員數量的增多、基礎庫數量的膨脹,導致了一些問題:

  • 主工程程式碼耦合嚴重,牽一髮而動全身
  • 需求測試影響面大,不能聚焦單一業務模組
  • 主工程程式碼越來越多,編譯耗時
  • 依賴倒置,業務程式碼依賴App工程
  • SDK 界限模糊,基礎庫和業務庫界限不明確
  • 業務模組間可以任意依賴呼叫,依賴規則不明確
  • 類庫越來越多,不好管理

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

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

元件化 or 外掛化

動態化

最近兩年,外掛化框架層出不窮,各大廠都放出了自家開源的外掛化框架。作為 Native 動態化與效能兼顧的外掛化方案,很多公司選擇外掛化作為動態化技術方案。動態性通常有兩部分的作用:一是動態熱修復;二是動態下發業務外掛。對於第一點,我們有熱修復框架可以完成這部分工作;對於第二點,我們使用了 Hybrid 載入H5的方式實現,雖然效能上有所欠缺,但完全切到 Native 來做有點推倒重來的意思,並且跟業界同學交流後,對於動態下發業務外掛用到的情況也不多,業務更新主要還是依靠 App 升級來實現。技術方案沒有最優解,選擇適合自己的才是最好的。

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

如今回過頭看,自從 Android P釋出以來,限制 hook framework 後,外掛化逐漸開始式微,後面走向大概率是維護成本越來越高,成本收益比逐漸降低,最終棄坑不用。

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

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

元件分離

程式碼解耦與程式碼隔離,最有效的方案是工程隔離。審視我們最初的方案,每個 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、H5、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 統一、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等等問題。我們也應該看到,正是因為這些實際的問題在推動我們進行技術改造,架構升級。同時,我們也要審視行業內大的方向,緊跟技術趨勢,主動擁抱變化,畢竟技術世界唯一不變的,便是變化。

相關文章