Android项目架构设计深入浅出
簡介:本文結(jié)合個人在架構(gòu)設(shè)計上的思考和理解,介紹如何從0到1設(shè)計一個大型Android項目架構(gòu)。
作者 | 璞珂
來源 | 阿里技術(shù)公眾號
前言:本文結(jié)合個人在架構(gòu)設(shè)計上的思考和理解,介紹如何從0到1設(shè)計一個大型Android項目架構(gòu)。
一 引導
本文篇幅較長,可結(jié)合下表引導快速了解全文主脈絡(luò)。
二 項目架構(gòu)演進
該章節(jié)主要對一個Android項目架構(gòu)從0到1再到N的演進歷程做出總結(jié)(由于項目的開發(fā)受業(yè)務、團隊和排期等各方面因素影響,因此該總結(jié)不會嚴格匹配每一步的演進歷程,但足以說明項目發(fā)展階段的一般性規(guī)律)。
1 單項目階段
對于一個新開啟項目而言,每端的開發(fā)人員通常非常有限,往往只有1-2個。這時候比項目的架構(gòu)設(shè)計和各種開發(fā)細節(jié)更重要的是開發(fā)周期,快速將idea進行落地是該階段最重要的目標。現(xiàn)階段項目的架構(gòu)往往是這樣
此時項目中幾乎所有的代碼都會寫在一個獨立的app模塊中,在時間為王的背景下,最原始的開發(fā)模式往往就是最好最高效的。
2 抽象基礎(chǔ)庫階段
隨著項目最小化MVP已經(jīng)開發(fā)完成,接下來打算繼續(xù)完善App。此時大概率會遇到以下幾個問題:
基于以上的一種或多種原因,我們往往會把那些相對于整個項目而言,一旦開發(fā)完成后就很少再改動的功能進行模塊化封裝。
我們把原本只包含一個應用層的項目,向下抽取了一個包含網(wǎng)絡(luò)庫、圖片加載庫和UI庫等眾多原子能力庫的基礎(chǔ)層。這樣做之后,對于協(xié)同開發(fā)、整包構(gòu)建和代碼復用都起到了很大的改善作用。
3 拓展核心能力階段
業(yè)務初具規(guī)模之后,App已經(jīng)投入到線上并且有持續(xù)穩(wěn)定的DAU。
在這個時候往往非常關(guān)鍵,隨著業(yè)務增長、客戶使用量增大、迭代需求增多等各方面挑戰(zhàn)。如果項目沒有一套良性的架構(gòu)設(shè)計,開發(fā)的人效會隨著團隊規(guī)模的擴大而反向降低,之前單位時間內(nèi)1個人能開發(fā)5個需求,現(xiàn)在10個人用同樣的時間甚至連20個需求都開發(fā)不完,單純的依靠加人是很難徹底解決這個問題的。這時候著重需要做的兩件事
該層會涉及到很多核心能力的建設(shè),這里不做過多贅述,下文會對以上各個模塊做詳細展開。
注:從全局視角來看,基礎(chǔ)層和核心層也能作為一個整體,共同支撐上層業(yè)務。這里將其分為兩層,主要考慮到前者是必選項,是整體架構(gòu)的必要組成部分;后者是可選項,但同時也是衡量一個App中臺能力的核心指標。
4 模塊化階段
隨著業(yè)務規(guī)模繼續(xù)擴大,App的產(chǎn)品經(jīng)理(下簡稱PD)會從一個變?yōu)槎鄠€,每個PD負責獨立的一條業(yè)務線,比如App中包含首頁、商品和我的等多個模塊,則每個PD會對應這里的一個模塊。但該調(diào)整會帶來一個很嚴重的問題
項目的版本迭代時間是確定的,只有一個PD的時候,每個版本會提一批需求,開發(fā)能按時交付就上線,不能交付就把這個迭代適當順延,這樣不會有什么問題;
但如今多個業(yè)務線并行,很難在絕對意義上保證各個業(yè)務線的需求迭代都能正常交付,就好像你組織一個活動約定了幾點集合,但總會有人會遇到一些特殊的情況不能及時趕到。同理,這種難以完全保持一致的情況在項目開發(fā)中也會遇到。在當前的項目架構(gòu)下,業(yè)務上雖然拆分了業(yè)務線,但我們工程項目的業(yè)務模塊還是一個整體,內(nèi)部包含著各種錯綜復雜的依賴關(guān)系網(wǎng),即使每個業(yè)務線按分支區(qū)分,也很難規(guī)避這個問題。
這時候我們需要在架構(gòu)層面做項目的模塊化,使得多業(yè)務線不相互依賴,如圖
業(yè)務層中,可以按照開發(fā)人員或者小組進行更細粒度的劃分,以保證業(yè)務間的解耦合和開發(fā)職責的界定。
5 跨平臺開發(fā)階段
業(yè)務規(guī)模和用戶體量繼續(xù)擴大,為了應對隨之而來的是業(yè)務需求暴增,整個端側(cè)團隊開始考慮研發(fā)成本問題。
為什么每個業(yè)務需求都至少需要Android和iOS兩端都實現(xiàn)一遍?有沒有什么方案能夠滿足一份代碼能運行在多個平臺?這樣豈不是既降低了溝通成本,又提升了研發(fā)效率。答案當然是肯定的,此時端側(cè)部分業(yè)務開始進入了跨平臺開發(fā)的階段。
至此,一個相對完整的端側(cè)系統(tǒng)架構(gòu)已經(jīng)初具雛形了。后續(xù)業(yè)務上會繼續(xù)有著更多的迭代,但項目的整體結(jié)構(gòu)基本都不會偏離太多,更多的是針對于當前架構(gòu)中的某些節(jié)點做更深層次的改進和完善。
以上是對Android項目架構(gòu)迭代過程的總結(jié),接下來我會對最終的架構(gòu)圖按照自下而上的層級順序進行逐一展開,并對每層中涉及到的核心模塊和可能遇到的問題進行分析和總結(jié)。
三 項目架構(gòu)拆解
1 基礎(chǔ)層
基礎(chǔ)UI模塊
抽取出基礎(chǔ)的UI模塊,主要有兩個目的:
統(tǒng)一App全局基礎(chǔ)樣式
比如App的主色調(diào)、普通正文的文字顏色和大小、頁面的內(nèi)外邊距、網(wǎng)絡(luò)加載失敗的默認提示文案、空列表的默認UI等等,尤其是在下文提到項目模塊化之后這些基礎(chǔ)的UI樣式統(tǒng)一會變得非常重要。
復用基礎(chǔ)UI組件
在項目和團隊規(guī)模逐漸發(fā)展擴大時,為了提高上層業(yè)務的開發(fā)效率,秉承DRY的開發(fā)原則,我們有必要對一些高頻UI組件進行統(tǒng)一封裝,以供給業(yè)務上層調(diào)用;另外一個角度來看,必要的抽象封裝還能夠降低最終構(gòu)建的安裝包大小,以免一份語義的資源文件在多處出現(xiàn)。
基礎(chǔ)UI組件通常包含內(nèi)部開發(fā)和外部引用兩部分,內(nèi)部開發(fā)無可厚非,根據(jù)業(yè)務需求進行開發(fā)和封裝即可;外部引用要著重強調(diào)一下,Github上有大量可復用、經(jīng)過很多項目驗證過的優(yōu)秀UI組件庫,如果是為了快速滿足業(yè)務開發(fā)訴求,這些都將不失為一種很不錯的選擇。
選擇一個合適的UI庫,會給整個開發(fā)進程帶來很大的加速,自己手動去實現(xiàn)也許沒問題,但會非常花費時間和精力,如果不是為了研究實現(xiàn)原理或深度定制,建議優(yōu)先選擇成熟的UI庫。
網(wǎng)絡(luò)模塊
絕大多數(shù)的App應用都需要聯(lián)網(wǎng),網(wǎng)絡(luò)模塊幾乎成為了所有App必不可少的部分。
框架選擇
基礎(chǔ)框架的選擇往往參考幾個大原則:
這里不做具體展開,如果不是基礎(chǔ)層對網(wǎng)絡(luò)層有自己額外的定制,則推薦直接使用Retrofit2作為網(wǎng)絡(luò)庫首選,上層Java Interface風格的Api,面向開發(fā)者非常友好;下層依賴功能強大的Okhttp框架也幾乎能夠滿足絕大多數(shù)場景的業(yè)務訴求。官網(wǎng)的用例參考
用例中對Retorfit聲明式接口的優(yōu)勢做了很好的展現(xiàn),不需要手動實現(xiàn)接口,聲明即可使用,其背后的原理是基于Java的動態(tài)代理來做的。
統(tǒng)一攔截處理
無論上一步選擇的是什么網(wǎng)絡(luò)庫,都需要考慮到該網(wǎng)絡(luò)庫對于統(tǒng)一攔截的能力支持。比如我們想在App的整個運行過程中,打印所有請求的日志,就需要有一個支持配置類似Interceptor這樣的全局攔截器。
舉一個具體的例子,在現(xiàn)如今服務端很多分布式部署的場景,傳統(tǒng)的session方式已經(jīng)無法滿足對客戶端狀態(tài)記錄的訴求。有一個比較公認的解決方案是JWT(JSON WEB TOKEN),它需要客戶端側(cè)在登錄認證之后,把包含用戶狀態(tài)的請求頭信息傳遞給服務端,此時就需要在網(wǎng)絡(luò)層做類似于下面的統(tǒng)一攔截處理。
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 {// 添加統(tǒng)一請求頭Request newRequest = chain.request().newBuilder().addHeader("Authorization", "Bearer " + token).build();return chain.proceed(newRequest);}}).build()).build();此外還有一點需要額外說明,如果應用中有一些跟業(yè)務強相關(guān)的信息,也建議根據(jù)實際業(yè)務情況考慮直接通過請求頭進行統(tǒng)一傳遞。比如社區(qū)App的社區(qū)Id、門店App的門店Id等,這類參數(shù)有個普遍性特點,一旦切換過來之后,接下來的很多業(yè)務網(wǎng)絡(luò)請求都會需要該參數(shù)信息,而如果每個接口都手動傳入將會降低開發(fā)效率,也更容易引發(fā)一些不必要的人為錯誤。
圖片模塊
圖片庫和網(wǎng)絡(luò)庫不同的是,目前行業(yè)里比較流行的幾個庫差異性并沒有那么大,這里建議根據(jù)個人喜好和熟悉度自行選擇。以下是我從各個圖片庫官網(wǎng)整理出來的使用示例。
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,供參考。
圖片庫的選型比較靈活,但是它的基礎(chǔ)原理我們需要弄清楚,以便在圖片庫出問題時有足夠的應對解決策略。
另外需要著重提出來的是,對于圖片庫最核心的是對圖片緩存的設(shè)計,有關(guān)該部分的延伸可以參考下文的「核心原理總結(jié)」章節(jié)。
異步模塊
在Android開發(fā)中異步會使用的非常之多,同時其中也包含很多知識點,因此這里將該部分單獨抽出來講解。
1)Android中的異步定理
總結(jié)下來一句話就是,主線程處理UI操作,子線程處理耗時任務操作。如果反其道而行之就會出現(xiàn)以下問題:
2)子線程調(diào)用主線程
如果當前在子線程,想要調(diào)用主線程的方法,一般有以下幾種方式
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)主線程調(diào)用子線程
如果當前在子線程,想要調(diào)用主線程的方法,一般也對應幾種方式,如下
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開發(fā)使用的是Java和Kotlin這兩種語言,如果我們的項目中引入了Kotlin當然是最好,對于異步調(diào)用時只需要按照如下方式進行調(diào)用即可。
Kotlin方案
val one = async { doSomethingUsefulOne() } val two = async { doSomethingUsefulTwo() } println("The answer is ${one.await() + two.await()}")這里適當延伸一下,類似于async + await的異步調(diào)用方式,在其他很多語言都已經(jīng)得到了支持,如下
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項目,在復雜的業(yè)務交互場景下,常常會遇到串行異步的業(yè)務邏輯,此時我們的代碼可讀性會變得很差,一種可選的應對方案是通過引入RxJava來解決,參考如下
RxJava方案
source.operator1().operator2().operator3().subscribe(consumer)2 核心層
動態(tài)配置
業(yè)務開關(guān)、ABTest
對于線上功能的動態(tài)配置
背景
基于以上幾點,就決定了我們在Android開發(fā)過程中,對代碼邏輯有動態(tài)配置的訴求。
基于這個最基本的模型單元,業(yè)務上可以演化出非常豐富的玩法,比如配置啟動頁停留時長、配置商品中是否展示大圖、配置每頁加載多少條數(shù)據(jù)、配置要不要是否允許用戶進入某個頁面等等。
分析
客戶端獲取配置信息通常有兩種方案,分別對應推和拉。
推是指通過建立客戶端與服務端的長連接,服務端一旦有配置發(fā)生變化,就將變化的數(shù)據(jù)推到客戶端以進行更新;
拉是指客戶端每次通過主動請求來讀取最新配置;
基于這兩種模式,還會演化出推拉結(jié)合的方式,其本質(zhì)就是兩種方式都使用,技術(shù)層面沒有新變化,這里不做贅述。下面將推拉兩種方式進行對比
綜合來看,如果業(yè)務上對時效性要求沒有非常高的情況下,我個人還是傾向于選擇拉的方式,主要原因更改配置是低頻事件,為了這個低頻事件去做C-S的長連接,會有種牛刀殺雞的感覺。
實現(xiàn)
推配置的實現(xiàn)思考相對清晰,有配置下發(fā)客戶端更新即可,但需要做好長連接斷開后的重連邏輯。
拉配置的實現(xiàn),這里有些需要我們思考的地方,這里總結(jié)以下幾點:
全局攔截
背景
App與用戶聯(lián)系最緊密的就是交互,它是我們的App產(chǎn)品與用戶之間溝通的橋梁。
用戶點擊一個按鈕之后要執(zhí)行什么動作,進入一個頁面之后要展示什么內(nèi)容,某個操作之后要執(zhí)行什么請求,請求之后要執(zhí)行什么提示,這些都是用戶最直觀能看到的東西。全局攔截就是針對于這些用戶能接觸到的最高頻的交互邏輯做出可支持通過前面動態(tài)配置來進行定制的技術(shù)方案。
交互結(jié)構(gòu)化
具體的交互響應(如彈出一個Toast或Dialog,跳轉(zhuǎn)到某個頁面)是需要通過代碼邏輯來控制的,但該部分要做到的就是在App發(fā)布之后還能實現(xiàn)這些交互,因此我們需要將一些基礎(chǔ)常見的交互進行結(jié)構(gòu)化處理,然后在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.關(guān)閉當前頁面
{"type": "finish" }4.跳轉(zhuǎn)到某個頁面
{"type": "route","url": "https://www.xxx.com/goods/detail?id=xxx" }5.執(zhí)行某個網(wǎng)絡(luò)請求 同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"}}} }統(tǒng)一攔截
交互結(jié)構(gòu)化的數(shù)據(jù)協(xié)議約定了每個Action對應的具體事件,客戶端對結(jié)構(gòu)化數(shù)據(jù)的解析和封裝,進而能夠?qū)?shù)據(jù)協(xié)議轉(zhuǎn)化為與用戶的產(chǎn)品交互,接下來要考慮的就是如何讓一個交互信息生效。參考如下邏輯
1.提供根據(jù)頁面和事件標識來獲取服務端下發(fā)的Action的能力,這里用到的DynamicConfig即為前面提到的動態(tài)配置。
@Nullable private static Action getClickActionIfExists(String page, String event) {// 根據(jù)當前頁面和事件確定動作標識String actionId = String.format("hook/click/%s/%s", page, event);// 解析動態(tài)配置中, 是否有需要下發(fā)的ActionString value = DynamicConfig.getValue(actionId, null);if (TextUtils.isEmpty(value)) {return null;}try {// 將下發(fā)Action解析為結(jié)構(gòu)化數(shù)據(jù)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對象, 降低上層業(yè)務方的理解成本和代碼改動難度return new View.OnClickListener() {@Overridepublic void onClick(View v) {// 取出當前事件的下發(fā)配置Action action = getClickActionIfExists(page, event);if (action != null) {// 有配置, 則走配置邏輯performAction(action);} else if (clickListener != null) {// 無配置, 則走默認處理邏輯clickListener.onClick(v);}}}; }有了上面的基礎(chǔ),我們便能夠快速實現(xiàn)支持遠端動態(tài)改變App交互行為的功能,下面對比一下上層業(yè)務方在該能力前后的代碼差異。
// 之前 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");}}));可以看到,業(yè)務側(cè)多透傳一些對于當前上下文的標識參數(shù),除此之外沒有其他更多的改動。
截止目前,我們對于addGoodsButton這一按鈕的點擊事件就已經(jīng)完成了遠端的hook能力,如果現(xiàn)在突然出現(xiàn)了一些原因?qū)е绿砑由唐讽摬豢捎?#xff0c;則只需要在遠端動態(tài)配置里添加如下配置即可。
{"hook/click/goods-manager/add-goods": {"type": "dialog","title": "提示","message": "由于XX原因,添加商品頁面暫不可用","confirmText": "確定","confirmAction": {"type": "finish"}} }此時用戶再點擊添加商品按鈕,就會出現(xiàn)如上的提示信息。
上面介紹了對于點擊事件的遠端攔截思路,與點擊事件對應的,還有跳轉(zhuǎn)頁面、執(zhí)行網(wǎng)絡(luò)請求等常見的交互,它們的原理都是一樣的,不再一一枚舉。
本地配置
在App開發(fā)測試階段,通常需要添加一些本地化配置,從而實現(xiàn)一次編譯構(gòu)建允許兼容多種邏輯。比如,在與服務端接口聯(lián)調(diào)過程中,App需要做出常見的幾種環(huán)境切換(日常、預發(fā)和線上)。
理論上,基于前面提到的動態(tài)配置也能實現(xiàn)這個訴求,但動態(tài)配置主要面向的是線上用戶,而如果選擇產(chǎn)研階段使用該種能力,無疑會增加線上配置的復雜度,而且還會依賴網(wǎng)絡(luò)請求的結(jié)果才能實現(xiàn)。
因此,我們需要抽象出一套支持本地化配置的方案,該套方案需要盡可能滿足以下能力
版本管理
在移動客戶端中,Android應用不同于iOS只能在AppStore進行發(fā)布,Android構(gòu)建的產(chǎn)物.apk文件支持直接安裝,這就給App靜默升級提供了可能。基于該特性,我們可以實現(xiàn)用戶在不通過應用市場即可直接檢測和升級新版本的訴求,縮短了用戶App升級的路徑,進而能夠提升新版本發(fā)布時的覆蓋率。
我們需要在應用中考慮抽象出來版本檢測和升級的能力支持,這里需要服務端提供檢測和獲取新版本App的接口。客戶端基于某種策略,如每次剛進入App、或手動點擊新版本檢測時,調(diào)用服務端的版本檢測接口,以判斷當前App是否為最新版本。如果當前是新版本,則提供給App側(cè)最新版本的apk文件下載鏈接,客戶端在后臺進行版本下載。下面總結(jié)出核心步驟的流程圖
日志監(jiān)控
環(huán)境隔離、本地持久化、日志上報
客戶端的日志監(jiān)控主要用來排查用戶在使用App過程中出現(xiàn)的Crash等異常問題,對于日志部分總結(jié)幾項值得注意的點
這里推薦兩個開源的日志框架:
logger
timber
埋點統(tǒng)計
服務端能查詢到的是客戶端接口調(diào)用的次數(shù)和頻率,但無法感知到用戶具體的操作路徑。為了能夠更加清晰的了解用戶,進而分析分析產(chǎn)品的優(yōu)劣勢和瓶頸點,我們可以將用戶在App上的核心操作路徑進行收集和上報。
比如,下面是一個電商App的用戶成交漏斗圖,通過客戶端的埋點統(tǒng)計能夠獲取到漏斗各層的數(shù)據(jù),然后再通過數(shù)據(jù)制作做出可視化報表。
分析以下漏斗,我們可以很明顯的看出成交流失的關(guān)鍵節(jié)點是在「進入商品頁」和「購買」之間,因此接下來就需要思考為什么「進入商品頁」的用戶購買意愿降低?是因為商品本身問題還是商品頁的產(chǎn)品交互問題?會不會是因為購買按鈕比較難點擊?還是因為商品頁圖片太大導致商品介紹沒有展示?這些流失流量的頁面停留時長又是怎樣的?對于這些問題的思考,會進一步促使我們?nèi)ピ谏唐讽撎砑痈嗟腁BTest和更細粒度的埋點統(tǒng)計分析。總結(jié)下來,埋點統(tǒng)計為用戶行為分析和產(chǎn)品優(yōu)化提供了很重要的指引意義。
在技術(shù)側(cè),對于該部分做出以下關(guān)鍵點總結(jié)
熱修復
熱修復(Hotfix)是一種對已發(fā)布上線的App在不進行應用升級的情況下進行動態(tài)更新原代碼邏輯的技術(shù)方案。主要同于以下場景
有關(guān)熱修復相關(guān)的技術(shù)方案探究,可以延展出很大篇幅,本文的定位是Android項目整體的架構(gòu),因此不做詳細展開。
3 應用層
抽象和封裝
對于抽象和封裝,主要取決于我們?nèi)粘oding過程中對一些痛點和冗余編碼的感知和思考能力。
比如,下面是一段Android開發(fā)過程中常寫的列表頁面的標準實現(xiàn)邏輯
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);// 加載數(shù)據(jù)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設(shè)計到足夠原子化,以支撐開發(fā)者千差萬別的開發(fā)訴求。比如,RecyclerView要做對多itemType的支持,所以內(nèi)部要做根據(jù)itemType開分組緩存vitemView的邏輯。
但實際業(yè)務開發(fā)過程中,就會拋開很多特殊性,我們頁面要展示的絕大多數(shù)列表都是單itemType的,在連續(xù)寫很多個這種單itemType的列表之后,我們就開始去思考一些問題:
對于以上問題的思考最終引導我封裝了RecyclerViewHelper的輔助類,相對于標準實現(xiàn)而言,使用方可以省去繁瑣的Adapter和ViewGolder聲明,省去一些高頻且必需的代碼邏輯,只需要關(guān)注最核心的功能實現(xiàn),如下
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));});// 加載數(shù)據(jù)recyclerViewHelper.addData(...);} }上面只是一個引子,實際開發(fā)過程中我們會遇到非常多類似的情況,還有一些常見的封裝。比如,封裝全局統(tǒng)一的BaseActivity和BaseFragment,包含但不限于以下能力
模塊化
背景
這里提到的模塊化是指,基于App的業(yè)務功能對項目工程進行模塊化拆分,主要為了解決大型復雜業(yè)務項目的協(xié)同開發(fā)困難問題。
在項目結(jié)構(gòu)的改造如上圖,將原來承載所有業(yè)務的 app 模塊拆分為 home、goods、mine等多個業(yè)務模塊。
通用能力下沉
前面「抽象和封裝」章節(jié)提到的 BaseActivity、BaseFragment 等通用業(yè)務能力在項目模塊化之后,也需要同步做改造,要下沉到業(yè)務層中單獨的一個 base 模塊中,以便提供給其他業(yè)務模塊引用。
隱式路由改造
模塊化之后,各模塊間沒有相互依賴關(guān)系,此時跨模塊進行頁面跳轉(zhuǎn)時不能直接引用其他模塊的類。
比如,在首頁展示某一個商品推薦,點擊之后要跳轉(zhuǎn)到商品詳情頁,在模塊化之前的寫法是
但在模塊化之后,在首頁模塊無法引用 GoodsActivity 類,因此頁面跳轉(zhuǎn)不能再繼續(xù)之前的方式,需要對頁面進行隱式路由改造,如下
1.注冊 Activity 標識,在 AndroidManifest.xml 中注冊 Activity 的地方添加 action 標識
2.替換跳轉(zhuǎn)邏輯,代碼中根據(jù)上一步注冊的 Activity 標識進行隱式跳轉(zhuǎn)
基于這兩步的改造, 便能夠達到模塊化之后仍能正常跳轉(zhuǎn)業(yè)務頁面的目的。
更進一步,我們將隱式跳轉(zhuǎn)的邏輯進行抽象和封裝,提煉出一個專門提供隱式路由能力的靜態(tài)方法,參考如下代碼
public class Router {/*** 根據(jù)url跳轉(zhuǎn)到目標頁面** @param context 當前頁面上下文* @param url 目標頁面url*/public static void open(Context context, String url) {// 解析為Uri對象Uri uri = Uri.parse(url);// 獲取不帶參數(shù)的urlString urlWithoutParam = String.format("%s://%s%s", uri.getScheme(), uri.getHost(), uri.getPath());Intent intent = new Intent(urlWithoutParam);// 解析url中的參數(shù), 并通過Intent傳遞至下個頁面for (String paramKey : uri.getQueryParameterNames()) {String paramValue = uri.getQueryParameter(paramKey);intent.putExtra(paramKey, paramValue);}// 執(zhí)行跳轉(zhuǎn)操作context.startActivity(intent);} }此時外部頁面跳轉(zhuǎn)時,只需要通過如下一句調(diào)用即可
Router.open(this, "https://www.xxx.com/goods/detail?goodsId=" + model.goodsId);這次封裝可以
模塊通信
模塊化后另一個需要解決是模塊通信問題,沒有直接依賴關(guān)系的模塊間是拿不到任何對方的 API 進行直接調(diào)用的。對于該問題往往會按照如下類別進行分析和處理
1、通知式通信,只需要將事件告知對方,并不關(guān)注對方的響應結(jié)果。對于該種通信,一般采用如下方式實現(xiàn)
- 借助 framework 層提供的通過 Intent + BroadcastReceiver (或 LocalBroadcastManager)發(fā)送事件;
- 借助框架 EventBus 發(fā)送事件;
- 基于觀察者模式自實現(xiàn)消息轉(zhuǎn)發(fā)器來發(fā)送事件;
2、調(diào)用式通信,將事件告知對方,同時還關(guān)注對方的事件響應結(jié)果。對于該種通信,一般采用如下方式實現(xiàn)
- 定義出 biz-service 模塊,將業(yè)務接口 interface 文件收口到該模塊,再由各接口對應語義的業(yè)務模塊進行接口的實現(xiàn),然后再基于某種機制(手動注冊或動態(tài)掃描)完成實現(xiàn)類的注冊;
-
抽象出 Request => Response 的通信協(xié)議,協(xié)議層負責完成
- 先將通過調(diào)用方傳遞的 Request 路由到被調(diào)用方的協(xié)議實現(xiàn)層;
- 再將實現(xiàn)層返回結(jié)果轉(zhuǎn)化為泛化的 Response對象;
- 最后將 Response 返回給調(diào)用方;
相對于 biz-service,該方案的中間層不包含任何業(yè)務語義,只定義泛化調(diào)用所需要的關(guān)鍵參數(shù)。
4 跨平臺層
跨平臺層,主要是為了提高開發(fā)人效,一套代碼能夠在多平臺運行。
跨平臺一般有兩個接入的時機,一個是在最開始的前期項目調(diào)研階段,直接技術(shù)選型為純跨平臺技術(shù)方案;另一個是在已有Native工程上需要集成跨平臺能力的階段,此時App屬于混合開發(fā)的模式,即Native + 跨平臺相結(jié)合。
有關(guān)更多跨平臺的選型和細節(jié)不在本文范疇內(nèi),具體可以參考《移動跨平臺開發(fā)框架解析與選型》,文中對于整個跨平臺技術(shù)的發(fā)展、各框架原理及優(yōu)劣勢講得很詳細。參跨平臺技術(shù)演進圖
對于目前主流方案的對比可參考下表
上面對項目架構(gòu)中各層的主要模塊進行了逐一的拆解和剖析,接下來會重點對架構(gòu)設(shè)計和實際開發(fā)中用到的一些非常核心的原理進行總結(jié)和梳理。
四 核心原理總結(jié)
在Android開發(fā)中,我們會接觸到的框架不計其數(shù),并且這些框架還還在不斷的更新迭代,因此我們很難對每個框架都能了如指掌。
但這并不影響我們對Android中核心技術(shù)學習和研究,如果你有嘗試過深入剖析這些框架的底層原理,就會發(fā)現(xiàn)它們中很多原理都是相通的。一旦我們掌握了這些核心原理,就會發(fā)現(xiàn)絕大多數(shù)框架只不過是利用這些原理,再結(jié)合框架要解決的核心問題,進而包裝出來的通用技術(shù)解決方案。
下面我把在SDK框架和實際開發(fā)中一些高頻率使用的核心原理進行梳理和總結(jié)。
1 雙緩存
雙緩存是指在通過網(wǎng)絡(luò)獲取一些資源時,為提高獲取速度而在內(nèi)存和磁盤添加雙層緩存的技術(shù)方案。該方案最開始主要用于上文「圖片模塊」提到的圖片庫中,圖片庫利用雙緩存來極大程度上提高了圖片的加載速度。一個標準雙緩存方案如下圖示
雙緩存方案的核心思想就是,對時效性低或更改較少的網(wǎng)絡(luò)資源,盡可能采取用空間換時間的方式。我們知道一般的數(shù)據(jù)獲取效率:內(nèi)存 > 磁盤 > 網(wǎng)絡(luò),因此該方案的本質(zhì)就是將獲取效率低的渠道向效率高的取到進行資源拷貝。
基于該方案,我們在實際開發(fā)中還能拓展另一個場景,對于業(yè)務上一些時效性低或更改較少的接口數(shù)據(jù),為了提高它們的加載效率,也可以結(jié)合該思路進行封裝,這樣就將一個依賴網(wǎng)絡(luò)請求頁面的首幀渲染時長從一般的幾百ms降到幾十ms以內(nèi),優(yōu)化效果相當明顯。
2 線程池
線程池在Android開發(fā)中使用到的頻率非常高,比如
如此多的場景會用到線程池,如果我們希望對項目的全局觀把握的更加清晰,熟悉線程池的一些核心能力和內(nèi)部原理是尤為重要的。
就其直接暴露出來的API而言,最核心的方法就兩個,分別是線程池構(gòu)造方法和執(zhí)行子任務的方法。
// 構(gòu)造線程池 ThreadPoolExecutor executor = new ThreadPoolExecutor(corePoolSize, maximumPoolSize,keepAliveTime, keepAliveTimeUnit, workQueue, threadFactory, rejectedExecutionHandler);// 提交子任務 executor.execute(new Runnable() {@Overridepublic void run() {// 這里做子任務操作} });其中,提交子任務就是傳入一個 Runnable 類型的對象實例不做贅述,需要重點說明也是線程池中最核心的是構(gòu)造方法中的幾個參數(shù)。
// 核心線程數(shù) int corePoolSize = 5; // 最大線程數(shù) 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();網(wǎng)上有關(guān)線程池的文章和教程有很多,這里不對每個具體參數(shù)做重復表述;但我下面要把對理解線程池內(nèi)部原理至關(guān)重要的——子任務提交后的扭轉(zhuǎn)機制進行單獨說明。
上圖表明的是往線程池中不斷提交子任務且任務來不及執(zhí)行時線程池內(nèi)部對任務的處理機制,該圖對理解線程池內(nèi)部原理和配置線程池參數(shù)尤為重要。
3 反射和注解
反射和注解都是Java語言里一種官方提供的技術(shù)能力,前者用來在程序運行期間動態(tài)讀寫對象實例(或靜態(tài))屬性、執(zhí)行對象(靜態(tài))方法;后者用來在代碼處往類、方法、方法入?yún)ⅰ㈩惓蓡T變量和局部變量等指定域添加標注信息。
通過反射和注解技術(shù),再結(jié)合對代碼的抽象和封裝思維,我們可以非常靈活的實現(xiàn)很多泛化調(diào)用的訴求,比如
反射和注解在開發(fā)中適用的場景有哪些?下面列舉幾點
依賴注入場景
普通方式
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() {// 注入對象實例 (內(nèi)部通過反射+注解實現(xiàn))InjectManager.inject(this);} }注入方式的優(yōu)勢是,對使用方屏蔽依賴對象的實例化過程,這樣方便對依賴對象進行統(tǒng)一管理。
調(diào)用私有或隱藏API場景
有個包含私有方法的類。
public class Manager {private void doSomething(String name) {// ...} }我們拿到 Manager 的對象實例后,希望調(diào)用到 doSomething 這個私有方法,按照一般的調(diào)用方式如果不更改方法為 public 就是無解的。但利用反射可以做到
try {Class< ?> managerType = manager.getClass();Method doSomethingMethod = managerType.getMethod("doSomething", String.class);doSomethingMethod.setAccessible(true);doSomethingMethod.invoke(manager, "< name參數(shù)>"); } catch (Exception e) {e.printStackTrace(); }諸如此類的場景在開發(fā)中會有很多,可以說熟練掌握反射和注解技術(shù),既是掌握 Java 高階語言特性的表現(xiàn),也能夠讓我們在對一些通用能力進行抽象封裝時提高認知和視角。
4 動態(tài)代理
動態(tài)代理是一種能夠在程序運行期間為指定接口提供代理能力的技術(shù)方案。
在使用動態(tài)代理時,通常都會伴隨著反射和注解的應用,但相對于反射和注解而言,動態(tài)代理的作用相對會比較晦澀難懂。下面結(jié)合一個具體的場景來看動態(tài)代理的作用。
背景
項目開發(fā)過程中,需要調(diào)用到服務端接口,因此客戶端封裝一個網(wǎng)絡(luò)請求的通用方法。
public class HttpUtil {/*** 執(zhí)行網(wǎng)絡(luò)請求** @param relativePath url相對路徑* @param params 請求參數(shù)* @param callback 回調(diào)函數(shù)* @param < T> 響應結(jié)果類型*/public static < T> void request(String relativePath, Map< String, Object> params, Callback< T> callback) {// 實現(xiàn)略..} }由于業(yè)務上有多個頁面都需要查詢商品列表數(shù)據(jù),因此需要封裝一個 GoodsApi 的接口。
public interface GoodsApi {/*** 分頁查詢商品列表** @param pageNum 頁面索引* @param pageSize 每頁數(shù)據(jù)量* @param callback 回調(diào)函數(shù)*/void getPage(int pageNum, int pageSize, Callback< Page< Goods>> callback); }并針對于該接口添加 GoodsApiImpl 實現(xiàn)類。
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);} }基于當前封裝,業(yè)務便能夠直接調(diào)用。
問題
業(yè)務需要再添加如下的查詢商品詳情接口。
我們需要在實現(xiàn)類添加實現(xiàn)邏輯。
緊接著,又需要添加 create 和 update 接口,我們繼續(xù)實現(xiàn)。
不僅如此,接下來還要加 OrderApi、ContentApi、UserApi 等等,并且每個類都需要這些列表。我們會發(fā)現(xiàn)業(yè)務每次需要添加新接口時,都得寫一遍對 HttpUtil#request 方法的調(diào)用,并且這段調(diào)用代碼非常機械化。
分析
前面提到接口實現(xiàn)代碼的機械化,接下來我們嘗試著將這段機械化的代碼,抽象出一個偽代碼的調(diào)用模板,然后進行分析。
透過每個方法內(nèi)部代碼實現(xiàn)的現(xiàn)象看其核心的本質(zhì),我們可以抽象歸納為以上的“模板”邏輯。
有沒有一種技術(shù)可以讓我們只需要寫網(wǎng)絡(luò)請求所必需的請求協(xié)議相關(guān)參數(shù),而不需要每次都要做以下幾步重復瑣碎的編碼?
此時動態(tài)代理便能解決這個問題。
封裝
分別定義路徑和參數(shù)注解。
@Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface Path {/*** @return 接口路徑*/String value(); } @Target({ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) public @interface Param {/*** @return 參數(shù)名稱*/String value(); }基于這兩個注解,便能封裝動態(tài)代理實現(xiàn)(以下代碼為了演示核心鏈路,忽略參數(shù)校驗和邊界處理邏輯)。
@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();// 解析接口參數(shù)Map< String, Object> params = new HashMap<>();Parameter[] parameters = method.getParameters();// 注: 此處多偏移一位, 為了跳過最后一項callback參數(shù)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]);}// 取最后一項參數(shù)為回調(diào)函數(shù)Callback< ?> callback = (Callback< ?>) args[args.length - 1];// 執(zhí)行網(wǎng)絡(luò)請求HttpUtil.request(path, params, callback);return null;}}); }效果
此時需要通過注解在接口聲明處添加網(wǎng)絡(luò)請求所需要的必要信息。
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();// 現(xiàn)在 GoodsApi goodsApi = ApiProxy.getApi(GoodsApi.class);相比之前,上層的調(diào)用方式只有極小的調(diào)整;但內(nèi)部的實現(xiàn)卻有了很大的改進,直接省略了所有的接口實現(xiàn)邏輯,參考如下代碼對比圖。
前面講了架構(gòu)設(shè)計過程中涉及到的核心框架原理,接下來會講到架構(gòu)設(shè)計里的通用設(shè)計方案。
五 通用設(shè)計方案
我們進行架構(gòu)設(shè)計的場景下通常是不同的,但有些問題的底層設(shè)計方案是相通的,這一章節(jié)會對這些相通的設(shè)計方案進行總結(jié)。
通信設(shè)計
一句話概括,通信的本質(zhì)就是解決 A 和 B 之間如何調(diào)用的問題,下面按抽象出來的 AB 模型依賴關(guān)系進行逐一分析。
直接依賴關(guān)系
關(guān)系范式:A => B
這是最常見的關(guān)聯(lián)關(guān)系,A 類中直接依賴 B,只需要通過最基本的方法調(diào)用和設(shè)置回調(diào)即可完成。
場景
頁面 Activity (A)與按鈕 Button (B)的關(guān)系。
參考代碼
間接依賴關(guān)系
關(guān)系范式:A => C => B
通信方式同直接依賴,但需要添加中間層進行透傳。
場景
頁面 Activity(A)中有商品卡片視圖 GoodsCardView(C),商品卡片中包含關(guān)注按鈕 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回調(diào)BfollowListener.onFollowClick();}}});}public void setFollowText(String followText) {// C調(diào)用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調(diào)用CgoodsCard.setFollowText("點擊商品即可關(guān)注");goodsCard.setOnFollowClickListener(new OnFollowListener() {@Overridepublic void onFollowClick() {// C回調(diào)A}});} }組合關(guān)系
關(guān)系范式:A <= C => B
通信方式和間接依賴類似,但其中一方的調(diào)用順序需要倒置。
場景
頁面 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回調(diào)ConTopIconClick();}});recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {@Overridepublic void onScrollStateChanged(RecyclerView recyclerView, int newState) {// A回調(diào)Cif (newState == RecyclerView.SCROLL_STATE_IDLE) {LinearLayoutManager layoutManager = (LinearLayoutManager) recyclerView.getLayoutManager();onFirstItemVisibleChanged(layoutManager.findFirstVisibleItemPosition() == 0);}}});}private void onFirstItemVisibleChanged(boolean visible) {// C調(diào)用BtopIcon.setVisibility(visible ? View.GONE : View.VISIBLE);}private void onTopIconClick() {// C調(diào)用ArecyclerView.scrollToPosition(0);// C調(diào)用BtopIcon.setVisibility(View.GONE);} }深依賴/組合關(guān)系
關(guān)系范式:A => C => ··· => B、A <= C => ··· => B
當依賴關(guān)系隔了多層時,直接使用普通的調(diào)用和設(shè)置回調(diào)這種通信方式,代碼會變得非常冗余,中間層大多都是做信息透傳邏輯。此時采取另一種方式,通過事件管理器進行事件的分發(fā)。
場景
頁面組件化之后,組件 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 調(diào)用 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調(diào)用XeventManager.dispatch(ACTION_SOMETHING);} }X 分發(fā) 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分發(fā)B}}});} }無關(guān)系
關(guān)系范式:A、B
這里指的是狹義概念的無關(guān)系,因為廣義概念上如果兩者之間沒有任何關(guān)聯(lián)關(guān)系,那它們是永遠無法通信的。
該種關(guān)系的通信也是借助于事件管理器,唯一不同點是對于 EventManager 對象實例的獲取方式不同了,不再是直接由當前上下文獲取到的,而是來源于全局唯一的實例對象,比如從單例中獲取到。
可拓展回調(diào)函數(shù)設(shè)計
背景
當我們封裝一個SDK,需要對外部添加回調(diào)函數(shù),如下。
回調(diào)函數(shù)
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();}} }外部客戶調(diào)用
SDKManager sdkManager = new SDKManager(); sdkManager.setCallback(new Callback() {@Overridepublic void onCall1() {} });問題
以上是很常見的一種回調(diào)設(shè)置方式,如果僅僅是做業(yè)務開發(fā),這種寫法沒有任何問題,但如果是做成給外部客戶使用的SDK,這種做法就會存在瑕疵。
按照這種寫法,假如SDK已經(jīng)提供出去給外部客戶使用了,此時需要增加一些回調(diào)給外面。
public interface Callback {void onCall1();void onCall2(); }如果這樣添加回調(diào),外部升級時就無法做到無感知升級,下面代碼就會報錯需要添加額外實現(xiàn)。
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();}} }對應的,外部調(diào)用時需要添加回調(diào)函數(shù)的設(shè)置。
sdkManager.setCallback2(new Callback2() {@Overridepublic void onCall2() {} });這種方案確實能解決外部無法靜默升級SDK的問題,但卻會帶來另外的問題,隨著每次接口升級,外部設(shè)置回調(diào)函數(shù)的代碼將會越來越多。
對外優(yōu)化
對于該問題,我們可以設(shè)置一個空的回調(diào)函數(shù)基類。
public interface Callback { }SDK回調(diào)函數(shù)都繼承它。
public interface Callback1 extends Callback {void onCall1();}public interface Callback2 extends Callback {void onCall2(); }SDK內(nèi)部設(shè)置回調(diào)時接收基類回調(diào)函數(shù),回調(diào)時根據(jù)類型判斷。
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();}} }再向外部提供一個回調(diào)函數(shù)的空實現(xiàn)類。
public class SimpleCallback implements Callback1, Callback2 {@Overridepublic void onCall1() {}@Overridepublic void onCall2() {} }此時,外部可以選擇通過單接口、組合接口和空實現(xiàn)類等多種方式設(shè)置回調(diào)函數(shù)。
// 單接口方式設(shè)置回調(diào) sdkManager.setCallback(new Callback1() {@Overridepublic void onCall1() {// ..} });// 組合接口方式設(shè)置回調(diào) interface CombineCallback extends Callback1, Callback2 { } sdkManager.setCallback(new CombineCallback() {@Overridepublic void onCall1() {// ..}@Overridepublic void onCall2() {// ...} });// 空實現(xiàn)類方式設(shè)置回調(diào) sdkManager.setCallback(new SimpleCallback() {@Overridepublic void onCall1() {// ..}@Overridepublic void onCall2() {//..} });現(xiàn)在如果SDK再拓展回調(diào),只需要添加新回調(diào)接口。
public interface Callback3 extends Callback {void onCall3(); }內(nèi)部添加新回調(diào)邏輯。
private void doSomething3() {// 略...if ((callback instanceof Callback3)) {((Callback3) callback).onCall3();} }這時候再升級SDK,對外部客戶之前的調(diào)用邏輯沒有任何影響,能夠很好的做到向前兼容。
對內(nèi)優(yōu)化
經(jīng)過前面的優(yōu)化,外部已經(jīng)做到不感知SDK變化了;但是內(nèi)部有些代碼還比較冗余,如下。
private void doSomething1() {// 略...if ((callback instanceof Callback1)) {((Callback1) callback).onCall1();} }SDK每次對外部回調(diào)的時候都要添加這種判斷著實麻煩,我們接下來將這段判斷邏輯單獨封裝起來。
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內(nèi)部就可以直接調(diào)用對應方法而不需要各種冗余的判斷邏輯了。
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();} }六 總結(jié)
做好項目的架構(gòu)設(shè)計需要我們考慮到技術(shù)選型、業(yè)務現(xiàn)狀、團隊成員、未來規(guī)劃等很多方面,并且伴隨著業(yè)務的發(fā)展,還需要在項目不同階段對工程和代碼進行持續(xù)化重構(gòu)。
業(yè)務領(lǐng)域千差萬別,可能是電商項目,可能是社交項目,還有可能是金融項目;開發(fā)技術(shù)也一直在快速迭代,也許在用純 Native 開發(fā)模式,也許在用 Flutter、RN 開發(fā)模式,也許在用混合開發(fā)模式;但無論如何,這些項目在架構(gòu)設(shè)計方面的底層原理和設(shè)計思路都是萬變不離其宗的,這些東西也正是我們真正要學習和掌握的核心能力。
原文鏈接
本文為阿里云原創(chuàng)內(nèi)容,未經(jīng)允許不得轉(zhuǎn)載。?
總結(jié)
以上是生活随笔為你收集整理的Android项目架构设计深入浅出的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 媒体声音 | 云数据库,谁才是领导者?
- 下一篇: 自己动手写符合自己业务需求的eslint