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