一種Android客戶端架構設計分享

RoxMalin 7年前發布 | 28K 次閱讀 軟件架構 Android開發 移動開發

技術發展日新月異,業界各種 Android 客戶端 架構 設計,五花八門,但我們不能簡單地說哪種架構更好,因為脫離業務談架構是沒有任何意義的,適合業務的才是好架構。而架構也不是一成不變的,隨著業務的發展,也許當初設計的架構已不足以支撐目前的業務,那么就需要改變之前的架構。接下來將分享下我們Android客戶端的架構設計,在App的某個業務發展階段或許有一些參考意義。

分層化與模塊化

分層化與模塊化應該是任何軟件開發的共識。

分層化

在Android應用開發中通常可以分為如下幾層:

  • SDK層:主要是Android SDK及第三方的SDK(可能基于Android SDK或為獨立的SDK),這些SDK為上層框架提供核心功能的支持。
  • 基礎框架層:這里所謂的基礎框架,指多數App都必需的基礎功能,是具體業務邏輯實現的基礎。主要有網絡請求功能、圖片加載與緩存功能、SQLite數據庫管理功能、Log管理功能等,當然根據對業務邏輯支持的不同,基礎框架層的功能支持也不一定相同,上述幾個應該是大部分App都要支持的,當然Crash監控與常用工具類也可歸為該層次。
    具體到每個基礎框架的實現則沒有任何限制,如網絡功能可以使用Volley、OkHttp或者自己封裝實現網絡請求邏輯;對于圖片管理功能則可以使用Glide、Fresco、Picasso,亦或自己實現……總之每個基礎框架都要遵循一定的實現原則,保持功能模塊的獨立性,與具體業務解耦并對外提供良好的交互接口。
  • 業務邏輯層:如果把App架構比作高層建筑,那么上述兩層就是地基。地基打好之后,就可以在上面任意發揮了,至于如何發揮,那就必須結合實際的業務需求,不同的應用往往有不同的業務功能模塊。
    另一方面,業務功能模塊也并非完全是并列的級別,有一些業務邏輯也是可以抽象出來的,作為通用的功能模塊,比如登錄、分享、掃描、統計等,其他的業務模塊可能會調用到這些功能。

這里需要注意的是SDK層與基礎框架層并不是一成不變的,但它們的變化周期往往是比較長的,一般來說當基礎功能不能滿足最上層的業務邏輯時,就需要對其做擴展。由于基礎框架層的功能模塊已經是功能級別的粒度劃分,因此擴展往往是模塊級別的擴展,通常是新增基礎功能框架而不是修改原有基礎功能框架,這也符合“開放-閉合”原則。

模塊化

至于模塊化,對于分層化來說則是更細粒度的劃分,即將每一層細分為不同的模塊,各功能模塊盡可能遵循“高內聚、低耦合”的原則,功能模塊之間僅提供必要的交互接口。

對于基礎框架層,由上圖可見,往往是根據功能來劃分。這里的基礎框架層細分為網絡支持功能、圖片庫、日志系統、 數據庫 支持等模塊,如果不足以支撐業務發展,可能會新增其他基礎功能模塊。

而業務邏輯層則主要由業務需求來決定,如分為掃描功能、電商、快遞查詢等模塊。業務邏輯層的模塊化還有一種驅動因素,那就是通用功能的封裝,這一點大家應該都有體會,隨著App業務邏輯的增加,不同業務功能之間可能會用到相同的功能,如用戶登錄、分享功能等,我們不希望在每個需要的地方都復寫一遍相關代碼,于是就需要把通用功能抽取成獨立于具體業務需求的模塊,如登錄模塊、分享模塊,在模塊內部實現通用的業務邏輯,同時對外暴露調用接口,不同的業務只需調用通用模塊即可。

業務數據流程設計

由于業務邏輯、數據處理邏輯或網絡框架的不同,相信各家應用都有自己的一套數據請求流程。最直接的就是從Activity或Fragment中調用網絡請求的方法,然后通過回調將結果返回到Activity或Fragment中,雖然流程最清晰,但這種方式存在幾個嚴重的問題:

  • 網絡數據直接返回到Activity或Fragment中,后續需要對數據進行解析、過濾、轉換、緩存等操作,這些工作將會大大加重Activity或Fragment的負擔。
  • Activity或Fragment的代碼量猛增,邏輯繁雜(不僅包含了View的邏輯還包含了數據處理的邏輯)
  • 從整個應用的角度來看,每個頁面甚至每個接口都需要重復上述相同的冗余工作,完全可以抽象出來。

上述設計思路是需要摒棄的,結合自身業務及架構演化,我們沒有跟風MVP、MVVM,而是設計了下面一套業務數據請求流程:

首先,視圖層通常表現為Activity或Fragment,并由視圖層發起數據請求,與上述不同,視圖層并不直接跟網絡框架打交道,而是先將數據請求發送到數據代理層DataAgent。需要注意到是,視圖層與數據代理層之間沒有采用直接通信的方式,而是插入了一個消息調度器MessageScheduler中轉。這樣做的好處是將視圖層與數據代理層解耦,視圖層無需關注數據代理層的具體實現,有了MessageScheduler,視圖層所要做的就是發出一個數據請求的消息而已,然后就可以靜靜等待一個回復消息,該回復消息會附帶最終需要的數據對象,這樣在視圖層就免除了數據處理的邏輯,拿到結果直接展示到UI上即可。使用這種方式,一般來講Activity或Fragment三五百行代碼即可搞定,UI邏輯或接口邏輯(如一個頁面有多個接口)比較復雜的代碼量基本也能控制在1000行左右,邏輯非常清爽。

消息調度器將視圖層的請求消息轉發到數據代理層后,DataAgent解析出數據請求類型DataType(該類型對應著具體數據對象模型)、必要參數(接口參數、是否需要緩存結果、分頁頁碼等),然后再執行具體的操作:

  • 如果要取緩存的數據,則DataAgent直接向緩存模塊發送請求。緩存的數據可以是初始JSON數據,也可以是解析處理后得到的數據對象Model,可根據具體需求配置。如果從緩存中取到的是JSON,則DataAgent先要解析處理得到對應Model;如果從緩存中取到的是Model,則不做處理,然后將Model封裝發回到消息調度器,再由MessageScheduler分發給具體的請求者,如Activity或Fragment。
  • 由于Android的數據來源有多種,如果數據來自持久化存儲,如SQLite或File等,仍然統一由DataAgent來跟它們通信,獲取數據并加工后通過MessageScheduler發回視圖層。
  • 最常見的是從服務器獲取數據,此種場景下,DataAgent將與網絡框架交互,將從MessageScheduler中獲取的參數提供給網絡框架構造請求url。至于網絡框架使用Volley或OkHttp或者其他都沒關系,網絡框架負責向Server請求數據,數據通常以JSON格式返回。DataAgent收到返回的JSON數據后,根據DataType將JSON數據校驗后拋給解析器,解析器會將JSON解析為視圖層需要的Model。當然數據解析過程可能伴隨數據的過濾、轉換等邏輯。另外需要注意的是,還需要根據視圖層需求對數據進行是否緩存的操作,可選擇緩存JSON還是Model。經過一系列操作,得到最終Model后,DataAgent將其通過MessageScheduler發回視圖層。

當然,由于數據請求流程是耗時的,因此上述步驟都是走的線程池,這點上圖中并未注明。

數據代理層

DataAgent在上文中已簡單提及,它的主要作用是對數據的一系列操作,包括實際的數據請求、數據解析處理、數據緩存等邏輯。下圖為從服務端接口獲取JSON數據并處理的流程:

從上圖可知,DataAgent的大致工作流程為:

  1. DataAgent將真正的數據請求發送給各數據源,數據源可能為緩存、SQLite或文件,但通常是從服務端獲取數據,因此DataAgent會將數據請求發到網絡框架層,然后等待數據返回。
  2. 由于數據源不同,返回數據也可能不同,這里簡化為兩種:原始JSON或Model。
  3. DataAgent拿到數據后,則開始數據處理流程。以從網絡請求的JSON數據為例,先對返回的JSON進行數據校驗,檢查數據的有效性與正確性,如果數據校驗通過,接下來根據需求來決定要不要寫入緩存,然后再進行數據加工(如精度處理、數據拼接、數據裁剪等),最后進行數據解析得到視圖層需要的Model。如果數據校驗沒有通過,則嘗試從緩存中讀取,從緩存中讀取后也需要校驗(檢查數據的時效性、有效性、正確性),校驗通過后同樣進行數據處理、解析等流程。如果緩存中讀取得到的就是Model,那么則可以省略數據處理和解析的流程。得到最終的Model后,DataAgent將其包裝發送給MessageScheduler。另外DataAgent還要具有一定的容錯功能,因為任何數據源都無法保證能夠返回合法的數據,如果不對數據錯誤進行容錯處理,那么就可能無法解析為對應的Model,從而導致視圖層無數據甚至異常。如果接口及緩存都無法返回正確的數據,DataAgent需要做特殊處理,以保證視圖層能給用戶以反饋。

業務視圖邏輯

雖然不同的業務頁面有不同的視圖邏輯,這里以一個應用中最常見的頁面為例來說明,假設該頁面有一個列表。大家都知道ListView(此處為泛指,可能大家都在用RecyclerView了)的工作方式,它需要ViewHolder來填充視圖,需要Adapter來填充數據,如果每個需要ListView的界面都維護各自的一套ViewHolder及Adapter,那么頁面邏輯又將變得臃腫。

我們在實踐中是這樣做的:

  • 封裝一個Adapter公共處理類,提供多種構造函數,其中有一個type參數,用來標明需要使用哪個ViewHolder。
  • 封裝一個ViewHolder抽象類,定義數據設置的邏輯,并交由具體的ViewHolder實現。
  • 構建一個叫做ViewHolderFactory的類,顧名思義該類主要作用是用來構建ViewHolder,它主要提供兩個方法createViewHolder()與createConvertView(),其中createConvertView()是個中間方法,用于生成ViewHolder。
  • 在Adapter的getView方法中,根據上述type參數,獲取具體的ViewHolder實現,調用設置數據的邏輯。

經過上述封裝之后,視圖層只需要向Adapter公共處理類傳入一個type參數即可得到對應的Adapter;等數據返回到視圖層后,再將數據傳給Adapter公共處理類,其他什么都不用管,就可以展示列表數據了。原本需要很多代碼實現的邏輯從視圖層抽離之后,視圖層只需要幾行代碼就能夠完成一個列表展示了。

Hybrid框架

自Android誕生以來,就有Native App與Web App之爭,這兩種開發方式雖然各有優缺點,但Native App一直占據上風。近一兩年來,移動應用中的Web頁面越來越多,而純Native的應用則相對越來越少。但是純Web App由于其渲染效率、性能問題、對硬件的調用限制導致其也并未廣泛地應用。于是一種折中的方案成為主流,即Hybrid App。

所謂Hybrid App,即混合開發方式,部分功能使用Native開發,部分功能使用H5開發。為了充分利用Web開發的優點并避開其缺點,并非所有業務功能都適合使用Web方式來開發。在我們的應用中,主要將H5用于以下方面:

  • 節日活動或游戲頁、秒殺或團購頁等具有時效性的頁面。
  • 使用說明、公告等偏展示、少交互的頁面。
  • 經常更新、交互較少且不涉及硬件調用的頁面或模塊,如電商商品首頁展示、積分兌換模塊。

截止到目前,我們App中的Web頁所占比重是上升的,大概占到所有功能的25%左右。使用Web開發的優勢非常明顯,可以支持多變的UI視圖效果、節省開發人力(Android、 iOS 共用)、Bug的在線修復而不用App發版等。

為了滿足App的Web頁面需求,于是我們在基礎框架層擴展了一個Hybrid功能模塊。該框架主要是自行封裝了Android原生的WebView控件,且分為不同層級的封裝,可根據需要靈活使用,核心功能及特性如下:

  • 支持完整的Web頁面,即整個頁面的內容全部是H5實現,外部容器為Activity或Fragment。
  • 支持局部的Web頁面,即部分頁面的內容是H5實現,可單獨使用自定義的WebView或者嵌入Fragment使用。
  • 定義了一套較為完整的交互協議,支持Native與JS的互相調用,典型的場景如H5頁面點擊跳轉Native功能頁面(支持傳參)、JS喚起Native對話框或Toast等,同時Java也能調用JS函數。基于此套交互協議,基本能夠滿足日常App中Web開發需求。
  • 避免了JS注入漏洞。
  • 支持同一個Web頁面中Http與Https混合的場景。
  • 向業務邏輯層暴露接口,可根據需求定制WebViewClient與WebChromeClient。
  • 對外提供接口,可根據需求控制縮放、Cookie管理、緩存管理、硬件加速等。
  • 經過試驗與摸索,兼容多種Android設備及版本。

雖然后來出現了 React Native ,但由于學習成本及其Android版本的局限性,結合我們自己團隊的人力資源原因,我們尚未在應用中正式使用。目前仍然以Hybrid開發為主,且其在整個應用中的比重越來越大,因此Hybrid框架是我們架構中重要的一個組成部分。

消息調度中心

前面業務數據流程的設計中,在視圖層與數據代理層之間插入了一個消息調度器——MessageScheduler,MessageScheduler主要功能就是管理消息及消息調度。

MessageScheduler核心原理是維護了一個哈希表,當收到視圖層的數據請求時就使用唯一的key將發起者保存到哈希表中,以便稍后收到DataAgent的返回數據后,能夠找到發起者。存儲好消息發起者的信息后,即向DataAgent發送數據請求,多個數據請求是可以并行的,主要在于線程池的線程數控制機制。DataAgent返回數據之后,MessageScheduler根據唯一key找到初始的請求者,同樣利用消息機制將請求結果返回給視圖層,同時在哈希表中清除該元素。其示意圖如下:

消息分發器

既然有了消息調度機制,就需要消息分發器MessageDispatcher,來負責發送消息。

MessageDispatcher本質上是利用了Android的消息機制來對業務需求進行封裝和擴展。看過Android Framework層源碼就會發現其實Android框架本身就有很多地方使用了消息機制來進行通信,Android消息機制可以在模塊頁面間、線程間通信,甚至可以在進程間使用Messenger通信(Messenger方式是利用了消息機制,當然還有其他進程間通信方式)。

MessageDispatcher功能比較簡單,支持兩種方式:

  • 點對點的通信,如兩個頁面之間,通信目標唯一,如上文提到的從視圖層發送數據請求消息到消息調度器。
  • 點對面的通信,類似于廣播,也有點像EventBus,一條消息發出,凡是注冊(或叫訂閱)過的頁面都能收到通知;也可以進一步通過Tag控制達到一對一發送。

其示意圖如下:

模塊路由中心

一個完整的應用中,免不了模塊之間、功能頁面之間的跳轉。當然在需要的地方通過Intent可以實現跳轉,但這不是一個好的方案,很明顯不同模塊或頁面之間的耦合度增加了。而我們的原則是模塊和頁面之間盡可能解耦,于是設計了一個模塊路由(Module Routing)中心,App中所有的頁面跳轉均由其控制。

模塊路由的核心原理是給功能頁面進行唯一編碼,編碼的邏輯可以跟隨產品版本定義到應用中,并保證兼容之前版本。這樣就可以在應用的任何地方只需要向模塊路由中心發送對應模塊頁面的編碼即可,由模塊路由負責打開目標頁面。

以下幾點需要注意:

  • 整個應用中的功能頁編碼都必須保證唯一
  • 如打開某些功能頁面除了具體編碼外,還可能需要額外參數。如打開商品詳情頁,除了知道商品詳情頁的編碼外,還需要商品ID,模塊路由需要對附加參數提供支持。
  • 模塊路由支持打開Web頁面,即Hybrid頁面也支持上述特定編碼,所以在Web頁面上點擊跳轉Native頁面使用的協議也是由模塊路由支持的。

使用模塊路由的好處有:

  • 大量減少應用中的跳轉Intent
  • 模塊之間、頁面之間解耦
  • 適配變化,統一管理,修改方便

其他

日志系統

在開發過程中,甚至運行過程中,日志都是很重要的一部分。當然Android提供了Log相關的API,但不建議這一行那一行地零星使用,否則如果想統一控制Tag或關閉Log時非常麻煩。建議對Log API進行簡單封裝或者使用現有第三方Log庫,將Log功能獨立出來,提供統一的調用接口、級別控制、開關控制,這樣既方便調試也方便管理,同時也能為整個應用代碼的清晰做出一點貢獻。

線上崩潰監控

對線上應用的Crash監控是提高應用穩定性、優化應用性能的一個重要方法。我們構建了一個小型的全局監控系統,主要由以下功能特性:

  • 對用戶不可見,用戶無感知
  • 全局注冊即可開啟監控
  • 捕捉線上崩潰,保存到本地文件
  • 線上崩潰信息按一定策略上傳服務器,上傳后同時刪除本地文件
  • 崩潰信息主要包括Android設備信息(如手機型號、系統版本等)、App版本號、異常信息等

服務器收到上傳的線上崩潰信息后,也按一定策略通過郵件方式通知到開發者,以便開發者及時修復異常。線上崩潰監測系統雖然小而簡單,但作用非常重要,利用線上崩潰反饋可以有效地提高應用的穩定性,建議在應用設計中務必給它留出一個位置。

統計系統

相信大部分應用都有統計分析后臺,可以統計應用的日活、PV、UV或其他用戶行為,也可能有一部分應用是使用的第三方統計功能,如友盟等。結合公司BI部門的統計需求,我們客戶端自行設計了一套統計方案,用于Android與iOS兩個客戶端。之所以不用第三方統計,主要是因為我們無法根據需求自由定制且數據不在自家服務器,另一方面也有些許數據泄露的風險。

基于客戶端的統計系統主要包括三個方面的功能:

  • 數據采集
  • 數據存儲
  • 數據上傳

對于數據采集,主要針對統計部門的需求,如采集設備信息、定位信息、App啟動時間次數、PV、UV、甚至用戶行為,如點擊、切換Tab、頁面流向跟蹤等。

為了避免每次采集完數據后就即時上傳,因此需要數據存儲,將采集的統計數據暫存到本地,一般使用SQLite。然后采用一定策略進行上傳,如數據累積到50條或者應用切換到后臺時進行上傳。

對于數據上傳,除了上傳時機的選擇策略外,還要遵循一定的結構字段,該結構可以根據數據統計部門的需求來定義。數據上傳的流程同樣可以使用之前的數據請求框架,只不過返回值可能為一個成功提示而已。

基于上述功能,我們自定義的統計功能模塊提供了方便的調用接口,并支持靈活擴展,目前可以完美支持日常的統計需求,調用也非常簡單,只需要在需要統計的地方插入一行代碼即可。

域名劫持應對策略

最近遇到域名劫持的問題,真是頭疼,另一方面也說明我們的流量引起運營商注意了。目前主流的有幾下幾種方案:

  • 向運營商投訴。此方法非常被動且效果不佳,完全掌控在運營商手中。
  • 使用httpDNS。此方法使用http的方式直接獲取最優IP,繞過localDNS的解析,可謂徹底解決了域名劫持。
  • 先使用域名嘗試,域名失敗后再使用IP嘗試。此方案屬于容災方案,并不能避免域名劫持。

理論上講第二種是最佳方案,但由于httpDNS為第三方服務,也無法保證效果,外加上付費及接入成本等因素,我們暫時采用了第三種容災方案,主要實施邏輯如下:

  1. 應用預先內置IP。
  2. 每次啟動應用時獲取最新IP,并保存到應用本地。
  3. 請求數據時,先使用域名走正常的邏輯,一旦遇到疑似劫持的問題后,使用本地的IP進行直連嘗試。

上述步驟其實是有漏洞的,比如啟動時獲取最新IP的接口如果被劫持了,那么就無法獲取最新IP,假如剛好同時服務器IP也改變了,因此預先內置的IP已經失效,此時就徹底沒辦法了。不過上述兩個條件同時滿足的概率比較小,因此可以使用該方案解決很大一部分域名劫持問題。另外從服務端獲取的IP,如果有多個的話,還需要增加一些策略,即考慮到負載均衡、訪問速度、穩定性、網絡運營商等因素,如何確定客戶端拿到的哪一個是最優IP,當然這點可以優化,但首先能保證用戶看到頁面數據或許更加重要。

上述應對域名劫持的策略本身并不能獨立成一個模塊,我們把它集成為網絡框架的擴展。

總結

上文提到的是我們Android應用架構中的核心部分,可能你發現并沒有什么花哨的、潮流的玩意兒,沒有MVP,沒有RxAndroid,沒有插件化,也沒有熱修復……但就是這樣它仍然支撐起了上億的用戶量。世上沒有完美的架構,只有符合自身業務的架構,上述架構還有很多缺點,我們也在有選擇、有步驟地重構,而隨著業務需求的擴展,架構也會不斷演化,最后希望本文能給大家帶來一點參考意義。

 

 

 

來自:http://www.androidchina.net/6607.html

 

 本文由用戶 RoxMalin 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!