Android项目架构设计深入浅出
簡介:本文結合個人在架構設計上的思考和理解,介紹如何從0到1設計一個大型Android項目架構。
作者 | 璞珂
來源 | 阿里技術公眾號
前言:本文結合個人在架構設計上的思考和理解,介紹如何從0到1設計一個大型Android項目架構。
一 引導
本文篇幅較長,可結合下表引導快速了解全文主脈絡。
二 項目架構演進
該章節主要對一個Android項目架構從0到1再到N的演進歷程做出總結(由于項目的開發受業務、團隊和排期等各方面因素影響,因此該總結不會嚴格匹配每一步的演進歷程,但足以說明項目發展階段的一般性規律)。
1 單項目階段
對于一個新開啟項目而言,每端的開發人員通常非常有限,往往只有1-2個。這時候比項目的架構設計和各種開發細節更重要的是開發周期,快速將idea進行落地是該階段最重要的目標。現階段項目的架構往往是這樣
此時項目中幾乎所有的代碼都會寫在一個獨立的app模塊中,在時間為王的背景下,最原始的開發模式往往就是最好最高效的。
2 抽象基礎庫階段
隨著項目最小化MVP已經開發完成,接下來打算繼續完善App。此時大概率會遇到以下幾個問題:
基于以上的一種或多種原因,我們往往會把那些相對于整個項目而言,一旦開發完成后就很少再改動的功能進行模塊化封裝。
我們把原本只包含一個應用層的項目,向下抽取了一個包含網絡庫、圖片加載庫和UI庫等眾多原子能力庫的基礎層。這樣做之后,對于協同開發、整包構建和代碼復用都起到了很大的改善作用。
3 拓展核心能力階段
業務初具規模之后,App已經投入到線上并且有持續穩定的DAU。
在這個時候往往非常關鍵,隨著業務增長、客戶使用量增大、迭代需求增多等各方面挑戰。如果項目沒有一套良性的架構設計,開發的人效會隨著團隊規模的擴大而反向降低,之前單位時間內1個人能開發5個需求,現在10個人用同樣的時間甚至連20個需求都開發不完,單純的依靠加人是很難徹底解決這個問題的。這時候著重需要做的兩件事
該層會涉及到很多核心能力的建設,這里不做過多贅述,下文會對以上各個模塊做詳細展開。
注:從全局視角來看,基礎層和核心層也能作為一個整體,共同支撐上層業務。這里將其分為兩層,主要考慮到前者是必選項,是整體架構的必要組成部分;后者是可選項,但同時也是衡量一個App中臺能力的核心指標。
4 模塊化階段
隨著業務規模繼續擴大,App的產品經理(下簡稱PD)會從一個變為多個,每個PD負責獨立的一條業務線,比如App中包含首頁、商品和我的等多個模塊,則每個PD會對應這里的一個模塊。但該調整會帶來一個很嚴重的問題
項目的版本迭代時間是確定的,只有一個PD的時候,每個版本會提一批需求,開發能按時交付就上線,不能交付就把這個迭代適當順延,這樣不會有什么問題;
但如今多個業務線并行,很難在絕對意義上保證各個業務線的需求迭代都能正常交付,就好像你組織一個活動約定了幾點集合,但總會有人會遇到一些特殊的情況不能及時趕到。同理,這種難以完全保持一致的情況在項目開發中也會遇到。在當前的項目架構下,業務上雖然拆分了業務線,但我們工程項目的業務模塊還是一個整體,內部包含著各種錯綜復雜的依賴關系網,即使每個業務線按分支區分,也很難規避這個問題。
這時候我們需要在架構層面做項目的模塊化,使得多業務線不相互依賴,如圖
業務層中,可以按照開發人員或者小組進行更細粒度的劃分,以保證業務間的解耦合和開發職責的界定。
5 跨平臺開發階段
業務規模和用戶體量繼續擴大,為了應對隨之而來的是業務需求暴增,整個端側團隊開始考慮研發成本問題。
為什么每個業務需求都至少需要Android和iOS兩端都實現一遍?有沒有什么方案能夠滿足一份代碼能運行在多個平臺?這樣豈不是既降低了溝通成本,又提升了研發效率。答案當然是肯定的,此時端側部分業務開始進入了跨平臺開發的階段。
至此,一個相對完整的端側系統架構已經初具雛形了。后續業務上會繼續有著更多的迭代,但項目的整體結構基本都不會偏離太多,更多的是針對于當前架構中的某些節點做更深層次的改進和完善。
以上是對Android項目架構迭代過程的總結,接下來我會對最終的架構圖按照自下而上的層級順序進行逐一展開,并對每層中涉及到的核心模塊和可能遇到的問題進行分析和總結。
三 項目架構拆解
1 基礎層
基礎UI模塊
抽取出基礎的UI模塊,主要有兩個目的:
統一App全局基礎樣式
比如App的主色調、普通正文的文字顏色和大小、頁面的內外邊距、網絡加載失敗的默認提示文案、空列表的默認UI等等,尤其是在下文提到項目模塊化之后這些基礎的UI樣式統一會變得非常重要。
復用基礎UI組件
在項目和團隊規模逐漸發展擴大時,為了提高上層業務的開發效率,秉承DRY的開發原則,我們有必要對一些高頻UI組件進行統一封裝,以供給業務上層調用;另外一個角度來看,必要的抽象封裝還能夠降低最終構建的安裝包大小,以免一份語義的資源文件在多處出現。
基礎UI組件通常包含內部開發和外部引用兩部分,內部開發無可厚非,根據業務需求進行開發和封裝即可;外部引用要著重強調一下,Github上有大量可復用、經過很多項目驗證過的優秀UI組件庫,如果是為了快速滿足業務開發訴求,這些都將不失為一種很不錯的選擇。
選擇一個合適的UI庫,會給整個開發進程帶來很大的加速,自己手動去實現也許沒問題,但會非常花費時間和精力,如果不是為了研究實現原理或深度定制,建議優先選擇成熟的UI庫。
網絡模塊
絕大多數的App應用都需要聯網,網絡模塊幾乎成為了所有App必不可少的部分。
框架選擇
基礎框架的選擇往往參考幾個大原則:
這里不做具體展開,如果不是基礎層對網絡層有自己額外的定制,則推薦直接使用Retrofit2作為網絡庫首選,上層Java Interface風格的Api,面向開發者非常友好;下層依賴功能強大的Okhttp框架也幾乎能夠滿足絕大多數場景的業務訴求。官網的用例參考
用例中對Retorfit聲明式接口的優勢做了很好的展現,不需要手動實現接口,聲明即可使用,其背后的原理是基于Java的動態代理來做的。
統一攔截處理
無論上一步選擇的是什么網絡庫,都需要考慮到該網絡庫對于統一攔截的能力支持。比如我們想在App的整個運行過程中,打印所有請求的日志,就需要有一個支持配置類似Interceptor這樣的全局攔截器。
舉一個具體的例子,在現如今服務端很多分布式部署的場景,傳統的session方式已經無法滿足對客戶端狀態記錄的訴求。有一個比較公認的解決方案是JWT(JSON WEB TOKEN),它需要客戶端側在登錄認證之后,把包含用戶狀態的請求頭信息傳遞給服務端,此時就需要在網絡層做類似于下面的統一攔截處理。
Retrofit retrofit = new Retrofit.Builder().baseUrl("https://xxx.xxxxxx.xxx").client(new OkHttpClient.Builder().addInterceptor(new Interceptor() {@NonNull@Overridepublic Response intercept(@NonNull Chain chain) throws IOException {// 添加統一請求頭Request newRequest = chain.request().newBuilder().addHeader("Authorization", "Bearer " + token).build();return chain.proceed(newRequest);}}).build()).build();此外還有一點需要額外說明,如果應用中有一些跟業務強相關的信息,也建議根據實際業務情況考慮直接通過請求頭進行統一傳遞。比如社區App的社區Id、門店App的門店Id等,這類參數有個普遍性特點,一旦切換過來之后,接下來的很多業務網絡請求都會需要該參數信息,而如果每個接口都手動傳入將會降低開發效率,也更容易引發一些不必要的人為錯誤。
圖片模塊
圖片庫和網絡庫不同的是,目前行業里比較流行的幾個庫差異性并沒有那么大,這里建議根據個人喜好和熟悉度自行選擇。以下是我從各個圖片庫官網整理出來的使用示例。
Picasso
Picasso.get().load("http://i.imgur.com/DvpvklR.png").into(imageView);Fresco
Uri uri = Uri.parse("https://raw.githubusercontent.com/facebook/fresco/main/docs/static/logo.png"); SimpleDraweeView draweeView = (SimpleDraweeView) findViewById(R.id.my_image_view); draweeView.setImageURI(uri);Glide
Glide.with(fragment).load(myUrl).into(imageView);另外,這里附上各個庫在Github上的star,供參考。
圖片庫的選型比較靈活,但是它的基礎原理我們需要弄清楚,以便在圖片庫出問題時有足夠的應對解決策略。
另外需要著重提出來的是,對于圖片庫最核心的是對圖片緩存的設計,有關該部分的延伸可以參考下文的「核心原理總結」章節。
異步模塊
在Android開發中異步會使用的非常之多,同時其中也包含很多知識點,因此這里將該部分單獨抽出來講解。
1)Android中的異步定理
總結下來一句話就是,主線程處理UI操作,子線程處理耗時任務操作。如果反其道而行之就會出現以下問題:
2)子線程調用主線程
如果當前在子線程,想要調用主線程的方法,一般有以下幾種方式
1.通過主線程Handler的post方法
private static final Handler UI_HANDLER = new Handler(Looper.getMainLooper());@WorkerThread private void doTask() throws Throwable {Thread.sleep(3000);UI_HANDLER.post(new Runnable() {@Overridepublic void run() {refreshUI();}}); }2.通過主線程Handler的sendMessage方法
private final Handler UI_HANDLER = new Handler(Looper.getMainLooper()) {@Overridepublic void handleMessage(@NonNull Message msg) {if (msg.what == MSG_REFRESH_UI) {refreshUI();}} };@WorkerThread private void doTask() throws Throwable {Thread.sleep(3000);UI_HANDLER.sendEmptyMessage(MSG_REFRESH_UI); }3.通過Activity的runOnUiThread方法
public class MainActivity extends Activity {// ...@WorkerThreadprivate void doTask() throws Throwable {Thread.sleep(3000);runOnUiThread(new Runnable() {@Overridepublic void run() {refreshUI();}});} }4.通過View的post方法
private View view;@WorkerThread private void doTask() throws Throwable {Thread.sleep(3000);view.post(new Runnable() {@Overridepublic void run() {refreshUI();}}); }3)主線程調用子線程
如果當前在子線程,想要調用主線程的方法,一般也對應幾種方式,如下
1.通過新開線程
@UiThread private void startTask() {new Thread() {@Overridepublic void run() {doTask();}}.start(); }2.通過ThreadPoolExecutor
private final Executor executor = Executors.newFixedThreadPool(10);@UiThread private void startTask() {executor.execute(new Runnable() {@Overridepublic void run() {doTask();}}); }3.通過AsyncTask
@UiThread private void startTask() {new AsyncTask< Void, Void, Void>() {@Overrideprotected Void doInBackground(Void... voids) {doTask();return null;}}.execute(); }異步編程痛點
Android開發使用的是Java和Kotlin這兩種語言,如果我們的項目中引入了Kotlin當然是最好,對于異步調用時只需要按照如下方式進行調用即可。
Kotlin方案
val one = async { doSomethingUsefulOne() } val two = async { doSomethingUsefulTwo() } println("The answer is ${one.await() + two.await()}")這里適當延伸一下,類似于async + await的異步調用方式,在其他很多語言都已經得到了支持,如下
Dart方案
Future< String> fetchUserOrder() =>Future.delayed(const Duration(seconds: 2), () => 'Large Latte');Future< String> createOrderMessage() async {var order = await fetchUserOrder();return 'Your order is: $order'; }JavaScript方案
function resolveAfter2Seconds(x) {return new Promise(resolve => {setTimeout(() => { resolve(x); }, 2000);}); }async function f1() {var x = await resolveAfter2Seconds(10);console.log(x); // 10 } f1();但是如果我們的項目中還是純Java項目,在復雜的業務交互場景下,常常會遇到串行異步的業務邏輯,此時我們的代碼可讀性會變得很差,一種可選的應對方案是通過引入RxJava來解決,參考如下
RxJava方案
source.operator1().operator2().operator3().subscribe(consumer)2 核心層
動態配置
業務開關、ABTest
對于線上功能的動態配置
背景
基于以上幾點,就決定了我們在Android開發過程中,對代碼邏輯有動態配置的訴求。
基于這個最基本的模型單元,業務上可以演化出非常豐富的玩法,比如配置啟動頁停留時長、配置商品中是否展示大圖、配置每頁加載多少條數據、配置要不要是否允許用戶進入某個頁面等等。
分析
客戶端獲取配置信息通常有兩種方案,分別對應推和拉。
推是指通過建立客戶端與服務端的長連接,服務端一旦有配置發生變化,就將變化的數據推到客戶端以進行更新;
拉是指客戶端每次通過主動請求來讀取最新配置;
基于這兩種模式,還會演化出推拉結合的方式,其本質就是兩種方式都使用,技術層面沒有新變化,這里不做贅述。下面將推拉兩種方式進行對比
綜合來看,如果業務上對時效性要求沒有非常高的情況下,我個人還是傾向于選擇拉的方式,主要原因更改配置是低頻事件,為了這個低頻事件去做C-S的長連接,會有種牛刀殺雞的感覺。
實現
推配置的實現思考相對清晰,有配置下發客戶端更新即可,但需要做好長連接斷開后的重連邏輯。
拉配置的實現,這里有些需要我們思考的地方,這里總結以下幾點:
全局攔截
背景
App與用戶聯系最緊密的就是交互,它是我們的App產品與用戶之間溝通的橋梁。
用戶點擊一個按鈕之后要執行什么動作,進入一個頁面之后要展示什么內容,某個操作之后要執行什么請求,請求之后要執行什么提示,這些都是用戶最直觀能看到的東西。全局攔截就是針對于這些用戶能接觸到的最高頻的交互邏輯做出可支持通過前面動態配置來進行定制的技術方案。
交互結構化
具體的交互響應(如彈出一個Toast或Dialog,跳轉到某個頁面)是需要通過代碼邏輯來控制的,但該部分要做到的就是在App發布之后還能實現這些交互,因此我們需要將一些基礎常見的交互進行結構化處理,然后在App中提前做出通用的預埋邏輯。
我們可以做出以下約定,定義出Action的概念,每個Action就對應著App中能夠識別的一個具體交互行為,比如
1.彈出Toast
{"type": "toast","content": "您好,歡迎來到XXX","gravity": "< 這里填寫toast要展示的位置, 可選項為(center|top|bottom), 默認值為center>" }2.彈出Dialog
這里值得注意的是,Dialog的Action中嵌套了Toast的邏輯,多種Action的靈活組合能給我們提供豐富的交互能力。
{"type": "dialog","title": "提示","message": "確定退出當前頁面嗎?","confirmText": "確定","cancelText": "取消","confirmAction": {"type": "toast","content": "您點擊了確定"} }3.關閉當前頁面
{"type": "finish" }4.跳轉到某個頁面
{"type": "route","url": "https://www.xxx.com/goods/detail?id=xxx" }5.執行某個網絡請求 同2,這里也做了多Action的嵌套組合。
{"type": "request","url": "https://www.xxx.com/goods/detail","method": "post","params": {"id": "xxx"},"response": {"successAction": {"type": "toast","content": "當前商品的價格為${response.data.priceDesc}元"},"errorAction": {"type": "dialog","title": "提示","message": "查詢失敗, 即將退出當前頁面","confirmText": "確定","confirmAction": {"type": "finish"}}} }統一攔截
交互結構化的數據協議約定了每個Action對應的具體事件,客戶端對結構化數據的解析和封裝,進而能夠將數據協議轉化為與用戶的產品交互,接下來要考慮的就是如何讓一個交互信息生效。參考如下邏輯
1.提供根據頁面和事件標識來獲取服務端下發的Action的能力,這里用到的DynamicConfig即為前面提到的動態配置。
@Nullable private static Action getClickActionIfExists(String page, String event) {// 根據當前頁面和事件確定動作標識String actionId = String.format("hook/click/%s/%s", page, event);// 解析動態配置中, 是否有需要下發的ActionString value = DynamicConfig.getValue(actionId, null);if (TextUtils.isEmpty(value)) {return null;}try {// 將下發Action解析為結構化數據return JSON.parseObject(value, Action.class);} catch (JSONException ignored) {// 格式錯誤時不做處理 (供參考)}return null; }2.提供包裝點擊事件的處理邏輯(performAction為對具體Action的解析邏輯,功能比較簡單,這里不做展開)
/*** 包裝點擊事件的處理邏輯** @param page 當前頁面標識* @param event 當前事件標識* @param clickListener 點擊事件的處理邏輯*/ public static View.OnClickListener handleClick(String page, String event, View.OnClickListener clickListener) {// 這里返回一個OnClickListener對象, 降低上層業務方的理解成本和代碼改動難度return new View.OnClickListener() {@Overridepublic void onClick(View v) {// 取出當前事件的下發配置Action action = getClickActionIfExists(page, event);if (action != null) {// 有配置, 則走配置邏輯performAction(action);} else if (clickListener != null) {// 無配置, 則走默認處理邏輯clickListener.onClick(v);}}}; }有了上面的基礎,我們便能夠快速實現支持遠端動態改變App交互行為的功能,下面對比一下上層業務方在該能力前后的代碼差異。
// 之前 addGoodsButton.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {Router.open("https://www.xxx.com/goods/add");} });// 之后 addGoodsButton.setOnClickListener(ActionManager.handleClick("goods-manager", "add-goods", new View.OnClickListener() {@Overridepublic void onClick(View v) {Router.open("https://www.xxx.com/goods/add");}}));可以看到,業務側多透傳一些對于當前上下文的標識參數,除此之外沒有其他更多的改動。
截止目前,我們對于addGoodsButton這一按鈕的點擊事件就已經完成了遠端的hook能力,如果現在突然出現了一些原因導致添加商品頁不可用,則只需要在遠端動態配置里添加如下配置即可。
{"hook/click/goods-manager/add-goods": {"type": "dialog","title": "提示","message": "由于XX原因,添加商品頁面暫不可用","confirmText": "確定","confirmAction": {"type": "finish"}} }此時用戶再點擊添加商品按鈕,就會出現如上的提示信息。
上面介紹了對于點擊事件的遠端攔截思路,與點擊事件對應的,還有跳轉頁面、執行網絡請求等常見的交互,它們的原理都是一樣的,不再一一枚舉。
本地配置
在App開發測試階段,通常需要添加一些本地化配置,從而實現一次編譯構建允許兼容多種邏輯。比如,在與服務端接口聯調過程中,App需要做出常見的幾種環境切換(日常、預發和線上)。
理論上,基于前面提到的動態配置也能實現這個訴求,但動態配置主要面向的是線上用戶,而如果選擇產研階段使用該種能力,無疑會增加線上配置的復雜度,而且還會依賴網絡請求的結果才能實現。
因此,我們需要抽象出一套支持本地化配置的方案,該套方案需要盡可能滿足以下能力
版本管理
在移動客戶端中,Android應用不同于iOS只能在AppStore進行發布,Android構建的產物.apk文件支持直接安裝,這就給App靜默升級提供了可能。基于該特性,我們可以實現用戶在不通過應用市場即可直接檢測和升級新版本的訴求,縮短了用戶App升級的路徑,進而能夠提升新版本發布時的覆蓋率。
我們需要在應用中考慮抽象出來版本檢測和升級的能力支持,這里需要服務端提供檢測和獲取新版本App的接口。客戶端基于某種策略,如每次剛進入App、或手動點擊新版本檢測時,調用服務端的版本檢測接口,以判斷當前App是否為最新版本。如果當前是新版本,則提供給App側最新版本的apk文件下載鏈接,客戶端在后臺進行版本下載。下面總結出核心步驟的流程圖
日志監控
環境隔離、本地持久化、日志上報
客戶端的日志監控主要用來排查用戶在使用App過程中出現的Crash等異常問題,對于日志部分總結幾項值得注意的點
這里推薦兩個開源的日志框架:
logger
timber
埋點統計
服務端能查詢到的是客戶端接口調用的次數和頻率,但無法感知到用戶具體的操作路徑。為了能夠更加清晰的了解用戶,進而分析分析產品的優劣勢和瓶頸點,我們可以將用戶在App上的核心操作路徑進行收集和上報。
比如,下面是一個電商App的用戶成交漏斗圖,通過客戶端的埋點統計能夠獲取到漏斗各層的數據,然后再通過數據制作做出可視化報表。
分析以下漏斗,我們可以很明顯的看出成交流失的關鍵節點是在「進入商品頁」和「購買」之間,因此接下來就需要思考為什么「進入商品頁」的用戶購買意愿降低?是因為商品本身問題還是商品頁的產品交互問題?會不會是因為購買按鈕比較難點擊?還是因為商品頁圖片太大導致商品介紹沒有展示?這些流失流量的頁面停留時長又是怎樣的?對于這些問題的思考,會進一步促使我們去在商品頁添加更多的ABTest和更細粒度的埋點統計分析。總結下來,埋點統計為用戶行為分析和產品優化提供了很重要的指引意義。
在技術側,對于該部分做出以下關鍵點總結
熱修復
熱修復(Hotfix)是一種對已發布上線的App在不進行應用升級的情況下進行動態更新原代碼邏輯的技術方案。主要同于以下場景
有關熱修復相關的技術方案探究,可以延展出很大篇幅,本文的定位是Android項目整體的架構,因此不做詳細展開。
3 應用層
抽象和封裝
對于抽象和封裝,主要取決于我們日常Coding過程中對一些痛點和冗余編碼的感知和思考能力。
比如,下面是一段Android開發過程中常寫的列表頁面的標準實現邏輯
public class GoodsListActivity extends Activity {private final List< GoodsModel> dataList = new ArrayList<>();private Adapter adapter;@Overrideprotected void onCreate(@Nullable Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_goods_list);RecyclerView recyclerView = findViewById(R.id.goods_recycler_view);recyclerView.setLayoutManager(new LinearLayoutManager(this));adapter = new Adapter();recyclerView.setAdapter(adapter);// 加載數據dataList.addAll(...);adapter.notifyDataSetChanged();}private class Adapter extends RecyclerView.Adapter< ViewHolder> {@NonNull@Overridepublic ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int position) {LayoutInflater inflater = LayoutInflater.from(parent.getContext());View view = inflater.inflate(R.layout.item_goods, parent, false);return new ViewHolder(view);}@Overridepublic void onBindViewHolder(@NonNull ViewHolder holder, int position) {GoodsModel model = dataList.get(position);holder.title.setText(model.title);holder.price.setText(String.format("%.2f", model.price / 100f));}@Overridepublic int getItemCount() {return dataList.size();}}private static class ViewHolder extends RecyclerView.ViewHolder {private final TextView title;private final TextView price;public ViewHolder(View itemView) {super(itemView);title = itemView.findViewById(R.id.item_title);price = itemView.findViewById(R.id.item_price);}} }這段代碼看上去沒有邏輯問題,能夠滿足一個列表頁的功能訴求。
面向RecyclerView框架層,為了提供框架的靈活和拓展能力,所以把API設計到足夠原子化,以支撐開發者千差萬別的開發訴求。比如,RecyclerView要做對多itemType的支持,所以內部要做根據itemType開分組緩存vitemView的邏輯。
但實際業務開發過程中,就會拋開很多特殊性,我們頁面要展示的絕大多數列表都是單itemType的,在連續寫很多個這種單itemType的列表之后,我們就開始去思考一些問題:
對于以上問題的思考最終引導我封裝了RecyclerViewHelper的輔助類,相對于標準實現而言,使用方可以省去繁瑣的Adapter和ViewGolder聲明,省去一些高頻且必需的代碼邏輯,只需要關注最核心的功能實現,如下
public class GoodsListActivity extends Activity {private RecyclerViewHelper< GoodsModel> recyclerViewHelper;@Overrideprotected void onCreate(@Nullable Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_goods_list);RecyclerView recyclerView = findViewById(R.id.goods_recycler_view);recyclerViewHelper = RecyclerViewHelper.of(recyclerView, R.layout.item_goods,(holder, model, position, itemCount) -> {TextView title = holder.getView(R.id.item_title);TextView price = holder.getView(R.id.item_price);title.setText(model.title);price.setText(String.format("%.2f", model.price / 100f));});// 加載數據recyclerViewHelper.addData(...);} }上面只是一個引子,實際開發過程中我們會遇到非常多類似的情況,還有一些常見的封裝。比如,封裝全局統一的BaseActivity和BaseFragment,包含但不限于以下能力
模塊化
背景
這里提到的模塊化是指,基于App的業務功能對項目工程進行模塊化拆分,主要為了解決大型復雜業務項目的協同開發困難問題。
在項目結構的改造如上圖,將原來承載所有業務的 app 模塊拆分為 home、goods、mine等多個業務模塊。
通用能力下沉
前面「抽象和封裝」章節提到的 BaseActivity、BaseFragment 等通用業務能力在項目模塊化之后,也需要同步做改造,要下沉到業務層中單獨的一個 base 模塊中,以便提供給其他業務模塊引用。
隱式路由改造
模塊化之后,各模塊間沒有相互依賴關系,此時跨模塊進行頁面跳轉時不能直接引用其他模塊的類。
比如,在首頁展示某一個商品推薦,點擊之后要跳轉到商品詳情頁,在模塊化之前的寫法是
但在模塊化之后,在首頁模塊無法引用 GoodsActivity 類,因此頁面跳轉不能再繼續之前的方式,需要對頁面進行隱式路由改造,如下
1.注冊 Activity 標識,在 AndroidManifest.xml 中注冊 Activity 的地方添加 action 標識
2.替換跳轉邏輯,代碼中根據上一步注冊的 Activity 標識進行隱式跳轉
基于這兩步的改造, 便能夠達到模塊化之后仍能正常跳轉業務頁面的目的。
更進一步,我們將隱式跳轉的邏輯進行抽象和封裝,提煉出一個專門提供隱式路由能力的靜態方法,參考如下代碼
public class Router {/*** 根據url跳轉到目標頁面** @param context 當前頁面上下文* @param url 目標頁面url*/public static void open(Context context, String url) {// 解析為Uri對象Uri uri = Uri.parse(url);// 獲取不帶參數的urlString urlWithoutParam = String.format("%s://%s%s", uri.getScheme(), uri.getHost(), uri.getPath());Intent intent = new Intent(urlWithoutParam);// 解析url中的參數, 并通過Intent傳遞至下個頁面for (String paramKey : uri.getQueryParameterNames()) {String paramValue = uri.getQueryParameter(paramKey);intent.putExtra(paramKey, paramValue);}// 執行跳轉操作context.startActivity(intent);} }此時外部頁面跳轉時,只需要通過如下一句調用即可
Router.open(this, "https://www.xxx.com/goods/detail?goodsId=" + model.goodsId);這次封裝可以
模塊通信
模塊化后另一個需要解決是模塊通信問題,沒有直接依賴關系的模塊間是拿不到任何對方的 API 進行直接調用的。對于該問題往往會按照如下類別進行分析和處理
1、通知式通信,只需要將事件告知對方,并不關注對方的響應結果。對于該種通信,一般采用如下方式實現
- 借助 framework 層提供的通過 Intent + BroadcastReceiver (或 LocalBroadcastManager)發送事件;
- 借助框架 EventBus 發送事件;
- 基于觀察者模式自實現消息轉發器來發送事件;
2、調用式通信,將事件告知對方,同時還關注對方的事件響應結果。對于該種通信,一般采用如下方式實現
- 定義出 biz-service 模塊,將業務接口 interface 文件收口到該模塊,再由各接口對應語義的業務模塊進行接口的實現,然后再基于某種機制(手動注冊或動態掃描)完成實現類的注冊;
-
抽象出 Request => Response 的通信協議,協議層負責完成
- 先將通過調用方傳遞的 Request 路由到被調用方的協議實現層;
- 再將實現層返回結果轉化為泛化的 Response對象;
- 最后將 Response 返回給調用方;
相對于 biz-service,該方案的中間層不包含任何業務語義,只定義泛化調用所需要的關鍵參數。
4 跨平臺層
跨平臺層,主要是為了提高開發人效,一套代碼能夠在多平臺運行。
跨平臺一般有兩個接入的時機,一個是在最開始的前期項目調研階段,直接技術選型為純跨平臺技術方案;另一個是在已有Native工程上需要集成跨平臺能力的階段,此時App屬于混合開發的模式,即Native + 跨平臺相結合。
有關更多跨平臺的選型和細節不在本文范疇內,具體可以參考《移動跨平臺開發框架解析與選型》,文中對于整個跨平臺技術的發展、各框架原理及優劣勢講得很詳細。參跨平臺技術演進圖
對于目前主流方案的對比可參考下表
上面對項目架構中各層的主要模塊進行了逐一的拆解和剖析,接下來會重點對架構設計和實際開發中用到的一些非常核心的原理進行總結和梳理。
四 核心原理總結
在Android開發中,我們會接觸到的框架不計其數,并且這些框架還還在不斷的更新迭代,因此我們很難對每個框架都能了如指掌。
但這并不影響我們對Android中核心技術學習和研究,如果你有嘗試過深入剖析這些框架的底層原理,就會發現它們中很多原理都是相通的。一旦我們掌握了這些核心原理,就會發現絕大多數框架只不過是利用這些原理,再結合框架要解決的核心問題,進而包裝出來的通用技術解決方案。
下面我把在SDK框架和實際開發中一些高頻率使用的核心原理進行梳理和總結。
1 雙緩存
雙緩存是指在通過網絡獲取一些資源時,為提高獲取速度而在內存和磁盤添加雙層緩存的技術方案。該方案最開始主要用于上文「圖片模塊」提到的圖片庫中,圖片庫利用雙緩存來極大程度上提高了圖片的加載速度。一個標準雙緩存方案如下圖示
雙緩存方案的核心思想就是,對時效性低或更改較少的網絡資源,盡可能采取用空間換時間的方式。我們知道一般的數據獲取效率:內存 > 磁盤 > 網絡,因此該方案的本質就是將獲取效率低的渠道向效率高的取到進行資源拷貝。
基于該方案,我們在實際開發中還能拓展另一個場景,對于業務上一些時效性低或更改較少的接口數據,為了提高它們的加載效率,也可以結合該思路進行封裝,這樣就將一個依賴網絡請求頁面的首幀渲染時長從一般的幾百ms降到幾十ms以內,優化效果相當明顯。
2 線程池
線程池在Android開發中使用到的頻率非常高,比如
如此多的場景會用到線程池,如果我們希望對項目的全局觀把握的更加清晰,熟悉線程池的一些核心能力和內部原理是尤為重要的。
就其直接暴露出來的API而言,最核心的方法就兩個,分別是線程池構造方法和執行子任務的方法。
// 構造線程池 ThreadPoolExecutor executor = new ThreadPoolExecutor(corePoolSize, maximumPoolSize,keepAliveTime, keepAliveTimeUnit, workQueue, threadFactory, rejectedExecutionHandler);// 提交子任務 executor.execute(new Runnable() {@Overridepublic void run() {// 這里做子任務操作} });其中,提交子任務就是傳入一個 Runnable 類型的對象實例不做贅述,需要重點說明也是線程池中最核心的是構造方法中的幾個參數。
// 核心線程數 int corePoolSize = 5; // 最大線程數 int maximumPoolSize = 10; // 閑置線程保活時長 int keepAliveTime = 1; // 保活時長單位 TimeUnit keepAliveTimeUnit = TimeUnit.MINUTES; // 阻塞隊列 BlockingDeque< Runnable> workQueue = new LinkedBlockingDeque<>(50); // 線程工廠 ThreadFactory threadFactory = new ThreadFactory() {@Overridepublic Thread newThread(Runnable r) {return new Thread(r);} }; // 任務溢出的處理策略 RejectedExecutionHandler rejectedExecutionHandler = new ThreadPoolExecutor.AbortPolicy();網上有關線程池的文章和教程有很多,這里不對每個具體參數做重復表述;但我下面要把對理解線程池內部原理至關重要的——子任務提交后的扭轉機制進行單獨說明。
上圖表明的是往線程池中不斷提交子任務且任務來不及執行時線程池內部對任務的處理機制,該圖對理解線程池內部原理和配置線程池參數尤為重要。
3 反射和注解
反射和注解都是Java語言里一種官方提供的技術能力,前者用來在程序運行期間動態讀寫對象實例(或靜態)屬性、執行對象(靜態)方法;后者用來在代碼處往類、方法、方法入參、類成員變量和局部變量等指定域添加標注信息。
通過反射和注解技術,再結合對代碼的抽象和封裝思維,我們可以非常靈活的實現很多泛化調用的訴求,比如
反射和注解在開發中適用的場景有哪些?下面列舉幾點
依賴注入場景
普通方式
public class DataManager {private UserHelper userHelper = new UserHelper();private GoodsHelper goodsHelper = new GoodsHelper();private OrderHelper orderHelper = new OrderHelper(); }注入方式
public class DataManager {@Injectprivate UserHelper userHelper;@Injectprivate GoodsHelper goodsHelper;@Injectprivate OrderHelper orderHelper;public DataManager() {// 注入對象實例 (內部通過反射+注解實現)InjectManager.inject(this);} }注入方式的優勢是,對使用方屏蔽依賴對象的實例化過程,這樣方便對依賴對象進行統一管理。
調用私有或隱藏API場景
有個包含私有方法的類。
public class Manager {private void doSomething(String name) {// ...} }我們拿到 Manager 的對象實例后,希望調用到 doSomething 這個私有方法,按照一般的調用方式如果不更改方法為 public 就是無解的。但利用反射可以做到
try {Class< ?> managerType = manager.getClass();Method doSomethingMethod = managerType.getMethod("doSomething", String.class);doSomethingMethod.setAccessible(true);doSomethingMethod.invoke(manager, "< name參數>"); } catch (Exception e) {e.printStackTrace(); }諸如此類的場景在開發中會有很多,可以說熟練掌握反射和注解技術,既是掌握 Java 高階語言特性的表現,也能夠讓我們在對一些通用能力進行抽象封裝時提高認知和視角。
4 動態代理
動態代理是一種能夠在程序運行期間為指定接口提供代理能力的技術方案。
在使用動態代理時,通常都會伴隨著反射和注解的應用,但相對于反射和注解而言,動態代理的作用相對會比較晦澀難懂。下面結合一個具體的場景來看動態代理的作用。
背景
項目開發過程中,需要調用到服務端接口,因此客戶端封裝一個網絡請求的通用方法。
public class HttpUtil {/*** 執行網絡請求** @param relativePath url相對路徑* @param params 請求參數* @param callback 回調函數* @param < T> 響應結果類型*/public static < T> void request(String relativePath, Map< String, Object> params, Callback< T> callback) {// 實現略..} }由于業務上有多個頁面都需要查詢商品列表數據,因此需要封裝一個 GoodsApi 的接口。
public interface GoodsApi {/*** 分頁查詢商品列表** @param pageNum 頁面索引* @param pageSize 每頁數據量* @param callback 回調函數*/void getPage(int pageNum, int pageSize, Callback< Page< Goods>> callback); }并針對于該接口添加 GoodsApiImpl 實現類。
public class GoodsApiImpl implements GoodsApi {@Overridepublic void getPage(int pageNum, int pageSize, Callback< Page< Goods>> callback) {Map< String, Object> params = new HashMap<>();params.put("pageNum", pageNum);params.put("pageSize", pageSize);HttpUtil.request("goods/page", params, callback);} }基于當前封裝,業務便能夠直接調用。
問題
業務需要再添加如下的查詢商品詳情接口。
我們需要在實現類添加實現邏輯。
緊接著,又需要添加 create 和 update 接口,我們繼續實現。
不僅如此,接下來還要加 OrderApi、ContentApi、UserApi 等等,并且每個類都需要這些列表。我們會發現業務每次需要添加新接口時,都得寫一遍對 HttpUtil#request 方法的調用,并且這段調用代碼非常機械化。
分析
前面提到接口實現代碼的機械化,接下來我們嘗試著將這段機械化的代碼,抽象出一個偽代碼的調用模板,然后進行分析。
透過每個方法內部代碼實現的現象看其核心的本質,我們可以抽象歸納為以上的“模板”邏輯。
有沒有一種技術可以讓我們只需要寫網絡請求所必需的請求協議相關參數,而不需要每次都要做以下幾步重復瑣碎的編碼?
此時動態代理便能解決這個問題。
封裝
分別定義路徑和參數注解。
@Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface Path {/*** @return 接口路徑*/String value(); } @Target({ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) public @interface Param {/*** @return 參數名稱*/String value(); }基于這兩個注解,便能封裝動態代理實現(以下代碼為了演示核心鏈路,忽略參數校驗和邊界處理邏輯)。
@SuppressWarnings("unchecked") public static < T> T getApi(Class< T> apiType) {return (T) Proxy.newProxyInstance(apiType.getClassLoader(), new Class[]{apiType}, new InvocationHandler() {@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {// 解析接口路徑String path = method.getAnnotation(Path.class).value();// 解析接口參數Map< String, Object> params = new HashMap<>();Parameter[] parameters = method.getParameters();// 注: 此處多偏移一位, 為了跳過最后一項callback參數for (int i = 0; i < method.getParameterCount() - 1; i++) {Parameter parameter = parameters[i];Param param = parameter.getAnnotation(Param.class);params.put(param.value(), args[i]);}// 取最后一項參數為回調函數Callback< ?> callback = (Callback< ?>) args[args.length - 1];// 執行網絡請求HttpUtil.request(path, params, callback);return null;}}); }效果
此時需要通過注解在接口聲明處添加網絡請求所需要的必要信息。
public interface GoodsApi {@Path("goods/page")void getPage(@Param("pageNum") int pageNum, @Param("pageNum") int pageSize, Callback< Page< Goods>> callback);@Path("goods/detail")void getDetail(@Param("id") long id, Callback< Goods> callback);@Path("goods/create")void create(@Param("goods") Goods goods, Callback< Goods> callback);@Path("goods/update")void update(@Param("goods") Goods goods, Callback< Void> callback); }外部通過 ApiProxy 獲取接口實例。
// 之前 GoodsApi goodsApi = new GoodsApiImpl();// 現在 GoodsApi goodsApi = ApiProxy.getApi(GoodsApi.class);相比之前,上層的調用方式只有極小的調整;但內部的實現卻有了很大的改進,直接省略了所有的接口實現邏輯,參考如下代碼對比圖。
前面講了架構設計過程中涉及到的核心框架原理,接下來會講到架構設計里的通用設計方案。
五 通用設計方案
我們進行架構設計的場景下通常是不同的,但有些問題的底層設計方案是相通的,這一章節會對這些相通的設計方案進行總結。
通信設計
一句話概括,通信的本質就是解決 A 和 B 之間如何調用的問題,下面按抽象出來的 AB 模型依賴關系進行逐一分析。
直接依賴關系
關系范式:A => B
這是最常見的關聯關系,A 類中直接依賴 B,只需要通過最基本的方法調用和設置回調即可完成。
場景
頁面 Activity (A)與按鈕 Button (B)的關系。
參考代碼
間接依賴關系
關系范式:A => C => B
通信方式同直接依賴,但需要添加中間層進行透傳。
場景
頁面 Activity(A)中有商品卡片視圖 GoodsCardView(C),商品卡片中包含關注按鈕 Button(B)。
參考代碼C 與 B 通信
public class GoodsCardView extends FrameLayout {private final Button button;private OnFollowListener followListener;public GoodsCardView(Context context, AttributeSet attrs) {super(context, attrs);// 略...button.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {if (followListener != null) {// C回調BfollowListener.onFollowClick();}}});}public void setFollowText(String followText) {// C調用Bbutton.setText(followText);}public void setOnFollowClickListener(OnFollowListener followListener) {this.followListener = followListener;} }A 與 C 通信
public class MainActivity extends Activity {private GoodsCardView goodsCard;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);// 略...// A調用CgoodsCard.setFollowText("點擊商品即可關注");goodsCard.setOnFollowClickListener(new OnFollowListener() {@Overridepublic void onFollowClick() {// C回調A}});} }組合關系
關系范式:A <= C => B
通信方式和間接依賴類似,但其中一方的調用順序需要倒置。
場景
頁面 Activity(C)中包含列表 RecyclerView(A)和置頂圖標 ImageView(B),點擊置頂時,列表需要滾動到頂部。
參考代碼
public class MainActivity extends Activity {private RecyclerView recyclerView;private ImageView topIcon;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);// 略...topIcon.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {// B回調ConTopIconClick();}});recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {@Overridepublic void onScrollStateChanged(RecyclerView recyclerView, int newState) {// A回調Cif (newState == RecyclerView.SCROLL_STATE_IDLE) {LinearLayoutManager layoutManager = (LinearLayoutManager) recyclerView.getLayoutManager();onFirstItemVisibleChanged(layoutManager.findFirstVisibleItemPosition() == 0);}}});}private void onFirstItemVisibleChanged(boolean visible) {// C調用BtopIcon.setVisibility(visible ? View.GONE : View.VISIBLE);}private void onTopIconClick() {// C調用ArecyclerView.scrollToPosition(0);// C調用BtopIcon.setVisibility(View.GONE);} }深依賴/組合關系
關系范式:A => C => ··· => B、A <= C => ··· => B
當依賴關系隔了多層時,直接使用普通的調用和設置回調這種通信方式,代碼會變得非常冗余,中間層大多都是做信息透傳邏輯。此時采取另一種方式,通過事件管理器進行事件的分發。
場景
頁面組件化之后,組件 A 需要通知組件 B 某個事件。
參考代碼
事件管理器
public class EventManager extends Observable< EventManager.OnEventListener> {public interface OnEventListener {void onEvent(String action, Object... args);}public void dispatch(String action, Object... args) {synchronized (mObservers) {for (OnEventListener observer : mObservers) {observer.onEvent(action, args);}}} }A 調用 X
public class AComponent {public static final String ACTION_SOMETHING = "a_do_something";private final EventManager eventManager;public AComponent(EventManager eventManager) {this.eventManager = eventManager;}public void sendMessage() {// A調用XeventManager.dispatch(ACTION_SOMETHING);} }X 分發 B
public class BComponent {private final EventManager eventManager;public BComponent(EventManager eventManager) {this.eventManager = eventManager;eventManager.registerObserver(new EventManager.OnEventListener() {@Overridepublic void onEvent(String action, Object... args) {if (AComponent.ACTION_SOMETHING.equals(action)) {// X分發B}}});} }無關系
關系范式:A、B
這里指的是狹義概念的無關系,因為廣義概念上如果兩者之間沒有任何關聯關系,那它們是永遠無法通信的。
該種關系的通信也是借助于事件管理器,唯一不同點是對于 EventManager 對象實例的獲取方式不同了,不再是直接由當前上下文獲取到的,而是來源于全局唯一的實例對象,比如從單例中獲取到。
可拓展回調函數設計
背景
當我們封裝一個SDK,需要對外部添加回調函數,如下。
回調函數
public interface Callback {void onCall1(); }SDK核心類
public class SDKManager {private Callback callback;public void setCallback(Callback callback) {this.callback = callback;}private void doSomething1() {// 略...if (callback != null) {callback.onCall1();}} }外部客戶調用
SDKManager sdkManager = new SDKManager(); sdkManager.setCallback(new Callback() {@Overridepublic void onCall1() {} });問題
以上是很常見的一種回調設置方式,如果僅僅是做業務開發,這種寫法沒有任何問題,但如果是做成給外部客戶使用的SDK,這種做法就會存在瑕疵。
按照這種寫法,假如SDK已經提供出去給外部客戶使用了,此時需要增加一些回調給外面。
public interface Callback {void onCall1();void onCall2(); }如果這樣添加回調,外部升級時就無法做到無感知升級,下面代碼就會報錯需要添加額外實現。
sdkManager.setCallback(new Callback() {@Overridepublic void onCall1() {} });不想讓外部感知,另一個方案就是新建一個接口。
public interface Callback2 {void onCall2(); }然后在SDK中添加對該方法的支持。
public class SDKManager {// 略..private Callback2 callback2;public void setCallback2(Callback2 callback2) {this.callback2 = callback2;}private void doSomething2() {// 略...if (callback2 != null) {callback2.onCall2();}} }對應的,外部調用時需要添加回調函數的設置。
sdkManager.setCallback2(new Callback2() {@Overridepublic void onCall2() {} });這種方案確實能解決外部無法靜默升級SDK的問題,但卻會帶來另外的問題,隨著每次接口升級,外部設置回調函數的代碼將會越來越多。
對外優化
對于該問題,我們可以設置一個空的回調函數基類。
public interface Callback { }SDK回調函數都繼承它。
public interface Callback1 extends Callback {void onCall1();}public interface Callback2 extends Callback {void onCall2(); }SDK內部設置回調時接收基類回調函數,回調時根據類型判斷。
public class SDKManager {private Callback callback;public void setCallback(Callback callback) {this.callback = callback;}private void doSomething1() {// 略...if ((callback instanceof Callback1)) {((Callback1) callback).onCall1();}}private void doSomething2() {// 略...if ((callback instanceof Callback2)) {((Callback2) callback).onCall2();}} }再向外部提供一個回調函數的空實現類。
public class SimpleCallback implements Callback1, Callback2 {@Overridepublic void onCall1() {}@Overridepublic void onCall2() {} }此時,外部可以選擇通過單接口、組合接口和空實現類等多種方式設置回調函數。
// 單接口方式設置回調 sdkManager.setCallback(new Callback1() {@Overridepublic void onCall1() {// ..} });// 組合接口方式設置回調 interface CombineCallback extends Callback1, Callback2 { } sdkManager.setCallback(new CombineCallback() {@Overridepublic void onCall1() {// ..}@Overridepublic void onCall2() {// ...} });// 空實現類方式設置回調 sdkManager.setCallback(new SimpleCallback() {@Overridepublic void onCall1() {// ..}@Overridepublic void onCall2() {//..} });現在如果SDK再拓展回調,只需要添加新回調接口。
public interface Callback3 extends Callback {void onCall3(); }內部添加新回調邏輯。
private void doSomething3() {// 略...if ((callback instanceof Callback3)) {((Callback3) callback).onCall3();} }這時候再升級SDK,對外部客戶之前的調用邏輯沒有任何影響,能夠很好的做到向前兼容。
對內優化
經過前面的優化,外部已經做到不感知SDK變化了;但是內部有些代碼還比較冗余,如下。
private void doSomething1() {// 略...if ((callback instanceof Callback1)) {((Callback1) callback).onCall1();} }SDK每次對外部回調的時候都要添加這種判斷著實麻煩,我們接下來將這段判斷邏輯單獨封裝起來。
public class CallbackProxy implements Callback1, Callback2, Callback3 {private Callback callback;public void setCallback(Callback callback) {this.callback = callback;}@Overridepublic void onCall1() {if (callback instanceof Callback1) {((Callback1) callback).onCall1();}}@Overridepublic void onCall2() {if (callback instanceof Callback2) {((Callback2) callback).onCall2();}}@Overridepublic void onCall3() {if (callback instanceof Callback3) {((Callback3) callback).onCall3();}} }接下來SDK內部就可以直接調用對應方法而不需要各種冗余的判斷邏輯了。
public class SDKManager {private final CallbackProxy callbackProxy = new CallbackProxy();public void setCallback(Callback callback) {callbackProxy.setCallback(callback);}private void doSomething1() {// 略...callbackProxy.onCall1();}private void doSomething2() {// 略...callbackProxy.onCall2();}private void doSomething3() {// 略...callbackProxy.onCall3();} }六 總結
做好項目的架構設計需要我們考慮到技術選型、業務現狀、團隊成員、未來規劃等很多方面,并且伴隨著業務的發展,還需要在項目不同階段對工程和代碼進行持續化重構。
業務領域千差萬別,可能是電商項目,可能是社交項目,還有可能是金融項目;開發技術也一直在快速迭代,也許在用純 Native 開發模式,也許在用 Flutter、RN 開發模式,也許在用混合開發模式;但無論如何,這些項目在架構設計方面的底層原理和設計思路都是萬變不離其宗的,這些東西也正是我們真正要學習和掌握的核心能力。
原文鏈接
本文為阿里云原創內容,未經允許不得轉載。?
總結
以上是生活随笔為你收集整理的Android项目架构设计深入浅出的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 媒体声音 | 云数据库,谁才是领导者?
- 下一篇: Android网络性能监控方案