1. 程式人生 > >【架構拾集】: Android 移動應用架構設計

【架構拾集】: Android 移動應用架構設計

在這一個多月裡,我工作在一個採用外掛化的原生 Android 應用專案上。隨著新技術的引入,及編寫原生 Android 程式碼的技能不斷提升,我開始思索如何去解鎖移動應用新架構。對,我就是在說 Growth 5.0。

兩星期前,我嘗試使用了 Kotlin + React Native + Dore + WebView 搭建了一個簡單的 Android 移動應用模板。為了嘗試解決 Growth 3.0+ 出現的一系列問題:啟動速度慢、架構複雜等等的問題。

PS:作為 Architecture 練習計劃的一部分,我將採用規範一些的敘述方式來展開。

  1. 業務架構

  2. 技術遠景

  3. 方案對比

  4. 架構設計方案

  5. 持續整合設計

  6. 測試策略

  7. 架構實施

即下圖:

640?wx_fmt=jpeg&wxfrom=5&wx_lazy=1

技術架構設計之路

業務架構

技術是為了解決業務的問題而產生的。

脫離了業務,技術就沒有了存在的前提。脫離了業務的架構不叫 “架構”,而叫刷流氓,又或者是畫大餅。業務由於其本身擁有其特定的技術場景,往往是對技術決策影響最大的部分。

因此,開始之前讓我們先了解一些業務,這裡以 Growth 為例。

Growth 的價值定位是:帶你成為頂尖開發者

複雜一點的說明就是:Growth 提供 程式設計學習服務 使用 Web開發路線 幫助 新手 Web 程式設計師 解決 Web 學習路徑問題

讓我們來看一下,更復雜一些的說明(電梯演講):

對於缺少 Web 體系經驗的程式設計師
他們有書籍、線上教程、論壇、技術問答、練習專案
我們的產品程式設計學習軟體 Growth
是一個移動應用
它可以涵蓋Web開發的流程及技術棧,Web開發的學習路線、成長衡量等各方面。
但他不同於segmentfault、知乎
它的優勢是擁有完整的 Web 開發流程(知識圖譜)、配套完整的電子書、提供練習及學習工具。

在原有的業務架構下,我們擁有 Growth、探索、社群、練習四個核心業務,以及使用者中心的功能。

  • Growth(首頁),即帶有詳細介紹的 Web 應用的生命週期,能幫助開發者理解 Web 應用的構建流程。

  • 探索,以輔助開發者瞭解 Web 應用方方面面的知識,如常用工具、練手專案、技能測驗、讀書路線等等。

  • 練習,通過這些練習專案,來幫助開發者更好的掌握知識。

  • 社群,一個簡易的論壇。

  • 使用者中心,一些使用者的收藏資料、應用相關的設定等等。

這就是業務上的主要架構,接下來讓我們看看技術上的事務。

技術遠景

遠景,即想象中未來的遠大景象。技術遠景,即想象中未來的技術方面的遠大景象。

在上一節中,我們介紹的是專案的業務遠景。而作為一個技術人員,在一個專案裡,我們也已經建立自己的技術遠景。一來,我們可以創建出可持續演進的架構;二來,可以滿足個人的技能需求。

以 Growth 為例,我的最基本的技術需求是:提升自身的能力。然後才是一個跨平臺的技術設施——減少構建時間。

從 Growth 1.0、Growth 2.0 採用的 Ionic,到 Growth 3.0 採用的 React Native,它都優先採用新的技術來幫助自己成長,並使用了跨平臺的移動應用開發框架。而這幾個不同的版本里,也擁有其對應的不同技術問題

  • Growth 1.0 主要是 Angular 1.x 的跳崖式升級,使之變成不可維護的系統。

  • Growth 2.0 則是 Angular 2.x 那龐大的構建體積,帶來了啟動時間慢的問題。

  • Growth 3.0 則是,React Native 生成的 index.android.bundle 檔案有 3.1M,這個體積相當的大,以至於即使在高通的驍龍 835 處理器上,也需要 4~5 秒的開啟時間。

而在 Growth 5.0 的設計構架裡,考慮到 React Native 本身的不加密,其對於應用來說,存在一些安全的風險。我決定引入 Native 的計劃,來從架構上說明,這個系統在某種程度上也是可以加密的。

因此,對於我而言,從技術上的期望就是:

  • 使用新技術帶來成長

  • 讓應用長期可維護

  • 擁有跨平臺的基礎設施

  • 外掛化方案

方案對比

對於普通的應用來說,其需要從不同的方案中選擇一個合適的方案。其選擇的核心,取決於專案所依賴的關鍵點。如在 Growth 有兩個關鍵點:程式碼複用程度、應用效能。

這個時候就需要羅列出不同系統的優缺點,並從中選擇合適自己的一部分。

如下資料(純屬個人使用體驗總結,沒有任何的資料基礎):

原 生React NativeNativeScript混合應用
開發效率2435
跨平臺程度0334
效能5442
成熟度5435
安全性5342
總計17181718

PS:NativeScript 在安全性上比 React Native 好一點點的原因是,使用 NativeScript 的人相對少一點,所以技術成本就高一些。畢竟,macOS 和 Android 手機上也是有病毒的。

考慮到我打算結合不同的幾個框架,所以這裡就不需要選擇了。

技術方案

在定下了基本的技術方案後,就差不多是時候進行架構設計了。

現今的很多應用裡,也是採用多種技術棧結合的架構,如淘寶的 Android 原生 + Weex + WebView,或者支付寶(不確定有沒有 Weex)。但是,可以肯定的是幾乎每個大型應用,都會在應用裡嵌入 WebView。WebView 畢竟是可以輕鬆地進行遠端動態更新,也需要原生程式碼那樣的後臺更新策略。

在 Growth 3.0 裡,我們選擇了使用 React Native + WebView 的構建方式,其原因主要是 WebView 的生態圈比較成熟,有相當多的功能已經用 WebView 實現了。而在新版本的設計中,則系統變得稍微複雜一些:

0?wx_fmt=jpeg

架構圖 2

從設計上來說,它擁有更好的擴充套件性,畢竟在安全上也更容易操作。然而,從技術棧上來說,它變得更加複雜。

Growth 技術方案

原生部分

系統在底層將採用原生的程式碼作為基礎框架,而不再是 React Native 作為基礎。再考慮到專案上正在實施的 Android 外掛化方案,我打算在 Android 的 Native 部分使用 RePlugin 來引入一些更靈活的地特性。因此,從架構上來說,能滿足個人的成長需求了。

畢竟原生 Android 有些架構還是相當有意思的:

0?wx_fmt=jpeg

原生 Android 架構

React Native

React Native 從程式碼上的變化比較大,架構設計上從程式碼上切分出幾個不同的頁面。它可能可以在某種程度上 Bundle 檔案過大,帶來的載入速度慢的問題。因而,在某種程度上,可能帶來更快的啟動速度。

WebView

總體上來說,WebView 變化不會太大。除了,可能從 React Native 的 WebView 遷移到原生部分的 WebView 之外。

持續整合設計

之前我們提到持續整合的時候,多數是指持續整合的實施。而今天我們談到持續整合的時候,則是在討論如何去設計。

程式碼策略

首先,就是程式碼策略,即程式碼管理策略。程式碼管理,指的就是決定採用哪種 git 工作流。會影響到程式碼管理的因素有:

  • 上線功能。如某次釋出要上線哪些功能,肯定會影響到正常的開發流程。

  • 程式碼整合。當我們採用模組化、外掛化來設計系統架構時,就需要將幾個不同的的專案整合到一起。

  • 程式碼合併。在有的專案裡,人們會使用 PR 來提交程式碼,有的則是直接在 master 上提交,也有的採用 feature branch。

  • 分支策略。什麼時候,決定拉出新的分支?

  • 修復 bug。當我們拉到一條新的分支時,我們要怎麼去應對一個 bug 的出現呢?

對於 Growth 而言,則仍然是 master 分支,採用多個 GitHub 專案的整合方式。

工具箱

作為一個有經驗的程式設計師,我們應該在設計的初期考慮到我們所需要的工具:

  • 基礎設施,諸如 React Native 需要的 Node.js、Android 及 Java 需要的構建工具 Gradle

  • 文件工具,諸如架構決策記錄工具 ADR,

  • 開發工具,編寫 Android 應用需要的 Android Studio、編寫 React Native 的 Intellij IDEA

  • 依賴庫,這些工具是我們

  • 持續整合,在持續整合上可以採用 Travis CI

  • 應用釋出,APP 仍然使用 GitHub 和 pgyer.com 來進行測試版釋出。至於後臺 API,是否從 GitHub、Coding 上遷出,仍然有待商榷。

這些也仍是我們在設計架構的過程中,需要考慮的一些因素。

測試策略

一般情況下,我們要會採用測試金字塔:

0?wx_fmt=png

測試金字塔

在這裡,引用《全棧應用開發:精益實踐》對於測試金字塔的分析:

從圖中我們可以發現:單元測試應該要是最多的,也是最底層的。其次才是服務測試,最後才是 UI 測試。大量的單元測試可以保證我們的基礎函式是正常、正確工作的。而服務測試則是一門很有學問的測試,不僅僅只在測試我們自己提供的服務,也會測試我們依賴第三方提供的服務。在測試第三方提供的服務時,這就會變成一件有意思的事了。除此還有對功能和 UI 的測試,寫這些測試可以減輕測試人員的工作量——畢竟這些工作量轉向了開發人員來完成。

而如果是架構混搭的應用來說,其的測試成本相當的大。因為要測試的部分是 3 + 1,即:

  • 原生部分,採用原先程式碼的測試策略,如 JUnit

  • React Native 部分,繼續之前的 react-test-renderer 測試渲染、 jest 和 chai 測試業務邏輯

  • WebView 部分,採用框架本身推薦的框架

  • 組合部分,對於這部分來說,UI 上的測試會更加可靠,如在 Growth 3.0 中採用的 appium 就是一個不錯的選擇。

架構實施

最後,讓我們來看看我在兩個星期前,搭的一個架子,用於作技術驗證功能。一共由三部分元件:

  • 使用 Kotlin 編寫的原生程式碼

  • 使用 React Native 編寫的 Fragment

  • 使用 Ionic 編寫的 WebView 應用

接下來看兩個簡單的程式碼示例:

建立 React Native 的 Fragement

如下是一個使用 React Native 編寫的 Fragement 示例,它可以直接在原生的 Activity 上使用:

  1. classArcheReactFragment:ReactFragment(){

  2. override val mainComponentName:String

  3. get()="RNArche"

  4. privatevar mReactRootView:ReactRootView?=null

  5. privatevar mReactInstanceManager:ReactInstanceManager?=null

  6. @Nullable

  7. override fun onCreateView(inflater:LayoutInflater?,group:ViewGroup?, savedInstanceState:Bundle?):ReactRootView?{

  8.        mReactRootView =ReactRootView(activity)

  9.        mReactInstanceManager =ReactInstanceManager.builder()

  10. .setApplication(activity.application)

  11. .setBundleAssetName("index.android.bundle")

  12. .setJSMainModulePath("index")

  13. .addPackage(MainReactPackage())

  14. .setUseDeveloperSupport(BuildConfig.DEBUG)

  15. .setInitialLifecycleState(LifecycleState.RESUMED)

  16. .build()

  17.        mReactRootView!!.startReactApplication(mReactInstanceManager,"RNArche",null)

  18. return mReactRootView

  19. }

  20. }

除了將 React Native 切分成不同的幾個子模組。對於一個 React Native 應用來說,它可以註冊多個 Component

  1. AppRegistry.registerComponent('RNArche',()=>App);

  2. AppRegistry.registerComponent('RNArche2',()=>App2);

這樣一來說,可以在一個 React Native 應用裡被原生部分多次呼叫不同的元件。

簡單的 WebView

對於那些不需要原生元件的元件來說,可以直接由原生應用來對 WebView 處理。從邏輯上來說,這樣的效能會更好一些:

  1. @SuppressLint("SetJavaScriptEnabled")

  2. @Nullable

  3. override fun onCreateView(inflater:LayoutInflater?, container:ViewGroup?, savedInstanceState:Bundle?):View?{

  4.    val view = inflater?.inflate(R.layout.fragment_webview, container,false)

  5.    mWebView = view?.findViewById(R.id.webview)

  6.    mWebView!!.loadUrl("file:///android_asset/www/index.html")

  7.    val webSettings = mWebView!!.settings

  8.    webSettings.javaScriptEnabled =true

  9.    mWebView!!.webViewClient =WebViewClient()

  10. return view

  11. }

對,就是這麼簡單。

結論

So,嘗試去做這樣的設計吧。

0?wx_fmt=jpeg

架構設計
分享、關注就是最好的讚賞~~

0?wx_fmt=jpeg

0?wx_fmt=jpeg