日韩av黄I国产麻豆传媒I国产91av视频在线观看I日韩一区二区三区在线看I美女国产在线I麻豆视频国产在线观看I成人黄色短片

歡迎訪問(wèn) 生活随笔!

生活随笔

當(dāng)前位置: 首頁(yè) > 运维知识 > Android >内容正文

Android

Android项目架构设计深入浅出

發(fā)布時(shí)間:2024/8/23 Android 48 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Android项目架构设计深入浅出 小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

簡(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)題:

  • 代碼的版本控制問(wèn)題,為保證項(xiàng)目加快迭代,團(tuán)隊(duì)新招1-3名開發(fā)同學(xué),多人同時(shí)在一個(gè)項(xiàng)目上開發(fā)時(shí),Git代碼合并總會(huì)出現(xiàn)沖突,非常影響開發(fā)效率;
  • 項(xiàng)目的編譯構(gòu)建問(wèn)題,隨著項(xiàng)目代碼量逐漸增多,運(yùn)行App都是基于源碼編譯,以至于首次整包編譯構(gòu)建的速度逐漸變慢,甚至?xí)霈F(xiàn)為了驗(yàn)證一行代碼的改動(dòng)而需要等待大幾分鐘或者更久時(shí)間的現(xiàn)象;
  • 多應(yīng)用的代碼復(fù)用問(wèn)題,公司可能同時(shí)在進(jìn)行多個(gè)App的開發(fā),同樣的代碼總是需要通過(guò)復(fù)制粘貼的方式進(jìn)行復(fù)用,維持同一個(gè)功能在多個(gè)App之間的邏輯一致性也會(huì)存在問(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í)候著重需要做的兩件事

  • 開發(fā)職責(zé)分離,團(tuán)隊(duì)成員需要分為兩部分,分別對(duì)應(yīng)業(yè)務(wù)開發(fā)和基礎(chǔ)架構(gòu)。業(yè)務(wù)開發(fā)組負(fù)責(zé)完成日常業(yè)務(wù)迭代的支撐,以業(yè)務(wù)交付為目標(biāo);基礎(chǔ)架構(gòu)組負(fù)責(zé)底層基礎(chǔ)核心能力建設(shè),以提升效率、性能和核心能力拓展為目標(biāo);
  • 項(xiàng)目架構(gòu)優(yōu)化,基于1,要在應(yīng)用層和基礎(chǔ)層之間,抽象出核心架構(gòu)層,并將其連帶基礎(chǔ)層一起交由基礎(chǔ)架構(gòu)組負(fù)責(zé),如圖;
  • 該層會(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è)大原則:

  • 維護(hù)團(tuán)隊(duì)和社區(qū)比較大,遇到問(wèn)題后能夠有足夠多的自助解決空間;
  • 底層功能強(qiáng)大,支撐盡可能多的上層應(yīng)用場(chǎng)景;
  • 拓展能力靈活,支持在框架基礎(chǔ)上做能力拓展和AOP處理;
  • Api側(cè)友好,降低上層的理解和使用成本;
  • 這里不做具體展開,如果不是基礎(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)題:

  • 主線程做網(wǎng)絡(luò)請(qǐng)求,會(huì)出現(xiàn)NetworkOnMainThreadException異常;
  • 主線程做耗時(shí)任務(wù),很可能會(huì)出現(xiàn)ANR(全稱Application Not Responding,指應(yīng)用無(wú)響應(yīng));
  • 子線程做UI操作,會(huì)出現(xiàn)CalledFromWrongThreadException異常(這里只做一般性討論,實(shí)際上在滿足某些條件下子線程也能更新UI,參《Android 中子線程真的不能更新 UI 嗎?》,本文不討論該情況);
  • 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)配置

    背景

  • Android(Native開發(fā))不同于Web能夠隨時(shí)發(fā)布上線,Android發(fā)布幾乎都要走應(yīng)用平臺(tái)的審核;
  • 業(yè)務(wù)上很多時(shí)候需要做AB測(cè)試或一些配置開關(guān),以滿足業(yè)務(wù)的多樣性;
  • 基于以上幾點(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):

  • 按照namespace進(jìn)行多模塊劃分配置,避免全局一個(gè)大而全的配置;
  • 每個(gè)namespace在初始化和每次改動(dòng)時(shí)都會(huì)有個(gè)flag標(biāo)識(shí),以標(biāo)識(shí)當(dāng)前版本;
  • 客戶端每個(gè)業(yè)務(wù)請(qǐng)求都在請(qǐng)求頭處統(tǒng)一拉上各flag或他們共同組合的md5等標(biāo)識(shí),為了在服務(wù)端統(tǒng)一攔截時(shí)進(jìn)行flag時(shí)效性校驗(yàn);
  • 服務(wù)端時(shí)效性檢驗(yàn)結(jié)果通過(guò)統(tǒng)一響應(yīng)頭下發(fā),與業(yè)務(wù)接口隔離,上層業(yè)務(wù)方不感知;
  • 客戶端收到時(shí)效性不一致結(jié)果時(shí),再根據(jù)具體的namespace進(jì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)。

    因此,我們需要抽象出一套支持本地化配置的方案,該套方案需要盡可能滿足以下能力

  • 對(duì)本地配置提供默認(rèn)值支持,未做任何配置時(shí),配置返回默認(rèn)值。比如,默認(rèn)環(huán)境為線上,如果沒(méi)有更改配置,就不會(huì)讀取到日常和預(yù)發(fā)環(huán)境。
  • 簡(jiǎn)化配置的讀寫接口,讓上層業(yè)務(wù)方盡可能少感知實(shí)現(xiàn)細(xì)節(jié)。比如,我們不需要讓上層感知到本地配置的持久化信息寫入的是SharedPreferences還是SQLite,而只需提供一個(gè)寫入的API即可。
  • 向上層暴露進(jìn)入本地配置頁(yè)的API方式,以滿足上層選擇性進(jìn)入的空間。比如,上層通過(guò)我們暴露出去的API可以選擇實(shí)際使用App過(guò)程中,是通過(guò)點(diǎn)擊頁(yè)面的某個(gè)操作按鈕、還是手機(jī)的音量鍵或者是搖一搖來(lái)進(jìn)入配置頁(yè)面。
  • 對(duì)于App中是否擁有本地配置能力的控制,盡可能放到編譯構(gòu)建級(jí)別,保證線上用戶不會(huì)進(jìn)入到配置頁(yè)面。比如,如果線上用戶能夠進(jìn)入到預(yù)發(fā)環(huán)境,那很可能會(huì)醞釀著一場(chǎng)安全事故即將發(fā)生。
  • 版本管理

    在移動(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)

  • 環(huán)境隔離,release包禁止log輸出;
  • 本地持久化,對(duì)于關(guān)鍵重要日志(如某個(gè)位置錯(cuò)誤引起的Crash)要做本地持久化保存;
  • 日志上報(bào),在用戶授權(quán)允許的情況下,將暫存用戶本地的日志進(jì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é)

  • 客戶端埋點(diǎn)一般分為P點(diǎn)(頁(yè)面級(jí)別)、E點(diǎn)(事件級(jí)別)和C點(diǎn)(自定義點(diǎn));
  • 埋點(diǎn)分為收集和上報(bào)兩個(gè)步驟,用戶量級(jí)較大時(shí)要注意對(duì)上報(bào)埋點(diǎn)進(jìn)行合并、壓縮等優(yōu)化處理;
  • 埋點(diǎn)邏輯是輔邏輯,產(chǎn)品業(yè)務(wù)是主邏輯,客戶端資源緊張時(shí)要做好資源分配的權(quán)衡;
  • 熱修復(fù)

    熱修復(fù)(Hotfix)是一種對(duì)已發(fā)布上線的App在不進(jìn)行應(yīng)用升級(jí)的情況下進(jìn)行動(dòng)態(tài)更新原代碼邏輯的技術(shù)方案。主要同于以下場(chǎng)景

  • 應(yīng)用出現(xiàn)重大缺陷并嚴(yán)重影響到用戶使用時(shí),比如,在某些系統(tǒng)定制化較強(qiáng)的機(jī)型(如小米系列)上一旦進(jìn)入商品詳情頁(yè)就出現(xiàn)應(yīng)用Crash;
  • 應(yīng)用出現(xiàn)明顯阻塞問(wèn)題并影響到用戶正常交互時(shí),比如,在某些極端場(chǎng)景下,用戶無(wú)法關(guān)閉頁(yè)面對(duì)話框;
  • 應(yīng)用出現(xiàn)資損、客訴、輿論風(fēng)波等產(chǎn)品形態(tài)問(wèn)題,比如將價(jià)格單位“元”誤顯示為“分”;
  • 有關(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)題:

  • 為什么每個(gè)列表都要寫一個(gè)ViewHolder?
  • 為什么每個(gè)列表都要寫一個(gè)Adapter?
  • 為什么Adapter中對(duì)itemView的創(chuàng)建和數(shù)據(jù)綁定要在onCreateViewHolder和onBindViewHolder兩個(gè)方法中分開進(jìn)行?
  • 為什么Adapter每次設(shè)置數(shù)據(jù)之后,都要調(diào)用對(duì)應(yīng)的notifyXXX方法?
  • 為什么Android上實(shí)現(xiàn)一個(gè)簡(jiǎn)單的列表需要大幾十行代碼量?這其中有多少是必需的,又有多少是可以抽象封裝起來(lái)的?
  • 對(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,包含但不限于以下能力

  • 頁(yè)面埋點(diǎn),基于前面提到的埋點(diǎn)統(tǒng)計(jì),在用戶進(jìn)入、離開頁(yè)面等時(shí)機(jī),將用戶頁(yè)面的交互情況進(jìn)行收集上報(bào);
  • 公共UI,頁(yè)面頂部狀態(tài)欄和ActionBar、頁(yè)面常用的下拉刷新能力實(shí)現(xiàn)、頁(yè)面耗時(shí)操作時(shí)的加載進(jìn)度條;
  • 權(quán)限處理,進(jìn)入當(dāng)前頁(yè)面所需要的權(quán)限申請(qǐng),用戶授權(quán)后的回調(diào)邏輯和拒絕后的異常處理邏輯;
  • 統(tǒng)一攔截,結(jié)合前面提到的統(tǒng)一攔截對(duì)進(jìn)入頁(yè)面后添加支持動(dòng)態(tài)配置交互的定制能力;
  • 模塊化

    背景

    這里提到的模塊化是指,基于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);

    這次封裝可以

  • 抽象統(tǒng)一方法,降低外部編碼成本;
  • 統(tǒng)一收口路由邏輯,便于結(jié)合前面的「動(dòng)態(tài)配置」和「統(tǒng)一攔截」章節(jié)進(jìn)行線上App路由邏輯的動(dòng)態(tài)更改;
  • 標(biāo)準(zhǔn)化 Android 端頁(yè)面跳轉(zhuǎn)參數(shù)格式,統(tǒng)一使用 String 類型,解除目標(biāo)頁(yè)面解析參數(shù)時(shí)的類型判斷歧義;
  • 對(duì)跳轉(zhuǎn)頁(yè)面所需要的數(shù)據(jù)做了標(biāo)準(zhǔn)化支持,iOS 端再配合同步改造后,頁(yè)面跳轉(zhuǎn)邏輯將支持完全由業(yè)務(wù)服務(wù)端進(jìn)行下發(fā);
  • 模塊通信

    模塊化后另一個(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ā)中使用到的頻率非常高,比如

  • 在開發(fā)框架中,網(wǎng)絡(luò)庫(kù)和圖片庫(kù)獲取網(wǎng)絡(luò)資源需用到線程池;
  • 在項(xiàng)目開發(fā)中,讀寫SQLite和本地磁盤文件等IO操作需要用到線程池;
  • 在類似于AsyncTask這種提供任務(wù)調(diào)度的API中,其底層也是依賴線程池來(lái)完成的;
  • 如此多的場(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ù)」章節(jié),基于ClassLoader 的方案,其內(nèi)部實(shí)現(xiàn)幾乎全是通過(guò)反射進(jìn)行 dex 更改的;
  • 前面「網(wǎng)絡(luò)模塊」章節(jié),Retorfit 只需要聲明一個(gè)接口再添加注解即可使用,其底層也是利用了反射注解和下面要介紹的動(dòng)態(tài)代理技術(shù);
  • 依賴注入框架 dagger 和 androidannotations 利用 Java 的 APT 預(yù)編譯技術(shù)再結(jié)合編譯時(shí)注解做注入代碼生成;
  • 如果了解 Java 服務(wù)端開發(fā),主流的開發(fā)框架 SpringBoot 其內(nèi)部大量運(yùn)用了注射和注解的技術(shù);
  • 反射和注解在開發(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ù)瑣碎的編碼?

  • 手動(dòng)寫一個(gè) Map;
  • 往 Map 中塞參數(shù)鍵值對(duì);
  • 調(diào)用 HttpUtil#request 執(zhí)行網(wǎng)絡(luò)請(qǐng)求;
  • 此時(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)題。

    如果覺(jué)得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。

    91成人网页版| 中文字幕在线观看一区二区 | 999热线在线观看 | 在线观看中文字幕亚洲 | 久热这里有精品 | 久久美女免费视频 | 国产成人在线免费观看 | 国产中文字幕网 | 国产精品成人国产乱一区 | 午夜丁香网 | 亚洲欧美国产日韩在线观看 | 夜夜操天天操 | 天天爱天天插 | 国产色在线 | 2024av | 日韩欧美高清一区二区三区 | 亚洲在线综合 | 日韩激情片在线观看 | 免费的成人av | 91免费网 | 久久国产精品一区二区三区四区 | 日韩丝袜 | 不卡av在线播放 | 欧美一级视频一区 | 精品久久久久久久久久国产 | 九九综合久久 | www.99在线观看 | 日韩大片在线观看 | 日韩精品久久一区二区 | 亚洲综合最新在线 | 日韩视频精品在线 | 91精品伦理 | 91在线小视频 | 婷婷伊人五月天 | 免费看色网站 | 国产成人精品一区二区三区网站观看 | 久久久久久久久毛片精品 | 欧美孕交vivoestv另类 | av动图| 日韩,中文字幕 | 18性欧美xxxⅹ性满足 | 在线观看黄色大片 | 婷婷六月天在线 | 日韩精品视频免费在线观看 | 成人永久免费 | 亚洲天堂毛片 | 免费黄色在线播放 | 精品久久久久久久久久久久久久久久久久 | 国产午夜麻豆影院在线观看 | 蜜臀av.com| 免费观看国产成人 | 久久国产成人午夜av影院潦草 | 欧美大香线蕉线伊人久久 | 免费视频 三区 | 免费在线a | 成人欧美一区二区三区在线观看 | 国产一级不卡毛片 | 欧美一级性 | 全黄色一级片 | 在线观看av免费 | 韩国av一区二区三区在线观看 | 精品一区二三区 | 永久免费av在线播放 | 天天操天天舔天天爽 | 日韩欧美综合视频 | 日本黄色大片免费看 | 日韩二级毛片 | 欧美一区二区三区在线观看 | 久一在线| 婷婷在线不卡 | 成人在线观看影院 | 日韩在线观看视频免费 | 亚洲国产欧美在线人成大黄瓜 | 久草精品免费 | 999一区二区三区 | 国产精品原创在线 | 久草在线免 | 久久精品高清 | 欧美日一级片 | 中文字幕在线日亚洲9 | 亚洲特级毛片 | 国产日韩av在线 | 五月花婷婷 | 免费观看视频的网站 | www国产亚洲精品久久网站 | 国产无遮挡猛进猛出免费软件 | 欧美一二三区在线观看 | 色噜噜日韩精品欧美一区二区 | 天天干 夜夜操 | 国产一区在线不卡 | 永久免费毛片在线观看 | 免费看国产曰批40分钟 | 国产黄av| 久久午夜精品视频 | 最近中文字幕在线中文高清版 | 国产精品18久久久久vr手机版特色 | 亚洲综合五月 | 婷婷视频在线播放 | 欧美日韩中文字幕视频 | 成人黄色小说视频 | 精品欧美一区二区精品久久 | 国产一区二区在线免费 | 国产伦理久久精品久久久久_ | 国产精品激情偷乱一区二区∴ | 麻豆久久久久久久 | 日日操操 | 综合网伊人 | 97av影院 | 婷婷深爱网 | 看黄色91 | 五月天天在线 | 国内免费久久久久久久久久久 | 成年人视频在线免费观看 | 国产精品爽爽久久久久久蜜臀 | 久久九九国产视频 | 国产精品毛片久久蜜 | 免费在线观看一级片 | 97国产精品 | 一区二区三区动漫 | 天天夜夜狠狠操 | 久久人人爽人人片av | 久久免费片 | 成人91在线观看 | 欧美性另类 | 亚洲国产字幕 | 国产精品一区二区久久精品 | 综合久久综合久久 | 毛片a级片 | 久草视频资源 | 天天天天射 | 日韩伦理一区二区三区av在线 | 美女国内精品自产拍在线播放 | 91香蕉国产在线观看软件 | 人人草人人草 | 草久久影院 | 狠狠色丁香久久婷婷综合五月 | 免费黄色特级片 | 天天爱天天操天天干 | 中文字幕国产精品 | 久久午夜视频 | 国产精品久久久久久久毛片 | 久久久免费看 | 狠狠网站| 国产精品观看在线亚洲人成网 | 国产不卡在线看 | 超碰公开在线 | 欧美性色综合网 | 精品视频在线观看 | 国产69精品久久久久9999apgf | 久久视频在线观看免费 | 国产一区在线不卡 | 日韩欧美一区视频 | 日韩有码在线播放 | 亚洲高清视频在线 | 在线观看黄网站 | 成人片在线播放 | 亚洲精品一区二区在线观看 | 日韩中文字幕a | 日韩在线 一区二区 | 久久久麻豆 | 91刺激视频 | 久久色亚洲 | 国产精品6 | 99久久精品国产一区 | 久久99国产精品久久99 | 超碰97.com| 高清国产在线一区 | 9999免费视频 | 又粗又长又大又爽又黄少妇毛片 | 九色91在线| 亚洲毛片久久 | 久久经典国产 | av黄色大片 | 日日夜夜精品网站 | 成人影片在线免费观看 | 免费在线黄 | 亚洲电影免费 | 欧美黄色特级片 | 亚洲视频在线观看 | 亚洲一区二区视频 | 高清av在线| 视频一区在线免费观看 | 夜夜摸夜夜爽 | 在线观看中文字幕第一页 | 亚洲性少妇性猛交wwww乱大交 | 免费中文字幕 | 深爱激情亚洲 | 婷婷久久国产 | 日本午夜在线观看 | 久久99久久99精品免观看软件 | 亚洲欧美日韩国产一区二区三区 | 亚洲午夜精品一区二区三区电影院 | 欧亚日韩精品一区二区在线 | 99精品国产99久久久久久97 | 亚州精品一二三区 | 亚洲黄色av网址 | 国产精品国产毛片 | 深爱婷婷 | 亚州激情视频 | 精品久久精品久久 | 成人小视频在线 | 欧美一二三区在线播放 | 视频在线日韩 | 国产精品久久久久久久久久东京 | 日日操天天射 | 美女免费视频观看网站 | 日韩有码在线播放 | 国产无区一区二区三麻豆 | 91热爆视频 | 日韩av视屏 | 午夜av影院 | 91激情小视频 | 国产中文在线视频 | 国产精品h在线观看 | 在线播放 亚洲 | 国产va饥渴难耐女保洁员在线观看 | 久久精品国产一区二区电影 | 亚洲精品国久久99热 | 97香蕉久久超级碰碰高清版 | 日本女人逼 | 亚洲精品久久久久中文字幕二区 | 在线播放 一区 | 精品色999| 亚洲高清色综合 | 亚洲视频在线观看网站 | 久草www | 黄色av在| 日本99久久| 香蕉视频在线免费 | 国产精品久久久久久久婷婷 | 国产高清在线观看 | 一级黄色在线视频 | 手机av看片| 99爱精品在线 | 日狠狠| 亚洲欧洲成人精品av97 | 人人看人人 | avav片| 国内外成人在线 | 中文资源在线官网 | 天天插天天爱 | www.久久久.com | 欧美久久久久久久久久 | 97成人在线免费视频 | 97精品久久人人爽人人爽 | 最近中文字幕免费av | 久久99精品视频 | 91黄色在线视频 | 亚洲一二三区精品 | 高清av影院 | 日日操天天操夜夜操 | 五月激情姐姐 | 91香蕉亚洲精品 | 一区二区电影在线观看 | 精品久久久久久久久久久久 | 亚洲综合在线一区二区三区 | 国产精品资源网 | 黄色软件网站在线观看 | 天天干天天草天天爽 | 特黄一级毛片 | 国产网红在线观看 | 麻豆av一区二区三区在线观看 | 日韩欧美国产免费播放 | 在线中文字幕播放 | 丁香伊人网 | 在线视频福利 | 国产一级片观看 | 日本中文字幕视频 | 一区二区三区久久精品 | 中文字幕在线观看国产 | 欧美嫩草影院 | 麻豆传媒视频在线免费观看 | 日韩一区二区三区不卡 | 国产精品第二十页 | 精品自拍网 | 久久国产亚洲 | 天天操天天干天天操天天干 | 高清av在线 | 国产精品免费久久久久久久久久中文 | 亚洲国产精品成人综合 | 欧美一区二区三区在线播放 | 久久久网页 | 在线看成人 | 人人爽人人澡人人添人人人人 | 久草免费电影 | 又色又爽又激情的59视频 | 久久久久久久国产精品视频 | 欧美91av | 亚洲欧美综合精品久久成人 | 99精品福利视频 | 久久国产女人 | 国产精品久久久久久久久免费 | 天堂久色 | 黄色性av| 国产精品美 | 久久国语露脸国产精品电影 | 91视频网址入口 | 99精品国产免费久久 | 91视频亚洲| 国产九九九精品视频 | 六月激情 | 亚洲精品国产综合99久久夜夜嗨 | 九精品| 国产免费av一区二区三区 | 成人午夜毛片 | 免费黄色av片 | 日韩欧美有码在线 | 天天天色综合 | 激情综合色播五月 | 久久色中文字幕 | 国内精品免费久久影院 | 激情综合网五月激情 | 中文字幕av免费 | 日韩免费一级a毛片在线播放一级 | 欧美va天堂在线电影 | 欧美日韩国产精品一区二区亚洲 | 国产精品剧情 | 韩国精品福利一区二区三区 | 久久免费视频6 | 欧美成人h版电影 | 色婷婷久久久综合中文字幕 | 麻豆视频免费入口 | 91天堂在线观看 | 国产传媒一区在线 | 香蕉视频久久久 | 中文字幕免费一区二区 | 91香蕉久久 | 黄色av一区 | 2023国产精品自产拍在线观看 | 亚洲区精品视频 | 国产高清免费av | 国产日韩欧美在线影视 | av一二三区| .国产精品成人自产拍在线观看6 | 国产黄大片在线观看 | 黄色毛片大全 | 国产原创在线视频 | 啪啪凸凸| 久久久久久久久艹 | 999电影免费在线观看 | 国产偷国产偷亚洲清高 | 亚洲精品乱码久久久一二三 | 成人一级片免费看 | 99视频国产精品 | 久久免费资源 | 日批网站在线观看 | 一区 二区电影免费在线观看 | 五月天亚洲激情 | 国产999免费视频 | 亚洲精品视频在线免费播放 | 少妇性aaaaaaaaa视频 | 天天视频亚洲 | 久久精品国产免费看久久精品 | 97日日碰人人模人人澡分享吧 | 婷婷 综合 色 | 一区二区三区在线免费播放 | 久久综合成人 | 久久天天躁狠狠躁亚洲综合公司 | 99视频导航| 国产小视频在线 | 欧美a级成人淫片免费看 | 久久精品欧美一 | 欧美精品一区二区蜜臀亚洲 | 精品国产大片 | 国产精品综合久久久久 | 日韩高清一区在线 | 最近中文字幕在线播放 | 91pony九色丨交换 | 91精品蜜桃 | 夜夜操狠狠操 | 91一区啪爱嗯打偷拍欧美 | 色婷婷亚洲婷婷 | 中文字幕在线观看1 | 亚洲精品视频免费在线 | 免费欧美| 狠狠狠干狠狠 | 99精品热| 日本精品xxxx| 中文字幕在线观看亚洲 | 狠狠色丁香婷婷综合久久片 | 久久久91精品国产一区二区精品 | 国产精品久久9 | 久久久影院官网 | 国产精品69av | 97在线成人 | 国产亚洲精品女人久久久久久 | 日韩一区二区三区免费视频 | 丁香婷婷激情国产高清秒播 | 成人免费观看大片 | 欧美一级黄大片 | 国产亚洲成av人片在线观看桃 | 91大神精品视频 | 婷婷国产一区二区三区 | 国产一区福利在线 | 蜜桃av人人夜夜澡人人爽 | 毛片美女网站 | 免费看十八岁美女 | 亚洲精品久久久久中文字幕二区 | www.久久久 | 日本黄色大片儿 | 天天爽夜夜爽人人爽曰av | 探花视频免费观看 | 亚洲精品一区中文字幕乱码 | 亚洲一级国产 | 欧美成人aa | 亚洲一区二区麻豆 | 麻豆果冻剧传媒在线播放 | 国产一级在线观看视频 | 99视频播放 | 国产精品av久久久久久无 | 亚洲国产精品女人久久久 | 天天色天| 亚洲国产经典视频 | 天天色天天艹 | 一级黄色a视频 | 成人在线视频免费看 | 97超碰站 | 中文字幕资源在线 | 少妇18xxxx性xxxx片 | www.色就是色 | 91精品国产综合久久福利不卡 | 国内精品视频在线 | 日韩一区二区三区免费视频 | 久久久麻豆视频 | 九热精品 | a在线观看国产 | 国产精品久久久久久av | 天天插天天狠天天透 | 91免费在线视频 | 国产亚洲精品xxoo | 国产成人av免费在线观看 | 久久黄色免费观看 | 99久免费精品视频在线观看 | 欧美久久久久久久 | 天天五月天色 | 在线免费中文字幕 | 九九天堂 | 国产午夜精品av一区二区 | 精品一区二区在线观看 | 免费视频黄色 | av色综合网| 午夜精品久久久久久久99热影院 | 久久毛片视频 | 激情婷婷久久 | 日韩大片在线观看 | 日日夜夜天天射 | 国产在线不卡 | 国产精品女主播一区二区三区 | 国产精品v a免费视频 | 久久久久久久久久电影 | 精品九九久久 | 久久观看最新视频 | 久久伊人精品一区二区三区 | 日本黄色大片免费看 | 在线免费观看成人 | 91成熟丰满女人少妇 | 在线观看日韩视频 | 国产精品毛片久久久久久久久久99999999 | 最新av电影网址 | 欧美日韩一区久久 | 福利视频一二区 | 欧美日韩一区二区三区视频 | 激情欧美日韩一区二区 | 深爱婷婷网 | 国产黄色免费观看 | 狠狠色噜噜狠狠狠狠 | 在线播放 日韩专区 | 国模吧一区 | 精品99在线 | 国产剧情在线一区 | 午夜视频不卡 | 久久夜色精品国产亚洲aⅴ 91chinesexxx | 国产97av| 伊人久久五月天 | 在线之家免费在线观看电影 | 在线日韩中文 | 日韩欧美视频在线播放 | 天天干天天操天天爱 | 国产高清视频在线播放一区 | 亚洲欧美日韩精品久久久 | 玖草在线观看 | 亚洲成人家庭影院 | 中文字幕av播放 | 婷婷电影在线观看 | 亚洲国产片色 | 欧美午夜久久久 | 国产日产精品久久久久快鸭 | 成年人在线| 日韩,中文字幕 | 久久在线视频在线 | 国产 日韩 中文字幕 | 91一区二区三区久久久久国产乱 | 久草免费在线视频 | 中文字幕在线影院 | 激情在线网址 | 日韩在线观看网址 | 国产又粗又猛又爽又黄的视频先 | 国产成本人视频在线观看 | a资源在线 | 国产精品3| 毛片黄色一级 | 99久久99视频 | 中文字幕免费在线 | 在线播放视频一区 | 日日摸日日 | 中文字幕资源网 | 国产一区国产精品 | 在线观看视频福利 | www免费视频com━ | 91九色视频在线播放 | 久久亚洲综合国产精品99麻豆的功能介绍 | 国产视频精品视频 | 天天干天天干天天射 | 欧美电影在线观看 | 中文字幕黄色网址 | 成人免费xxx在线观看 | 91av色 | 99国产精品一区二区 | 成人黄大片视频在线观看 | 草久在线观看视频 | 99免费观看视频 | 国产美女精品人人做人人爽 | 午夜视频播放 | 中文理论片 | 成年人看片| 亚洲成a人片综合在线 | 国产成人av网 | 色综合 久久精品 | 国产一区二区三区免费观看视频 | 国内精品国产三级国产aⅴ久 | 日本高清免费中文字幕 | 午夜久久| 激情丁香 | 色网免费观看 | 免费99视频 | 日韩理论片在线 | 2021国产在线 | 最近中文字幕完整高清 | 欧美贵妇性狂欢 | 亚洲欧美日韩国产一区二区三区 | 五月天九九| 免费网站在线 | 色丁香久久 | 午夜美女福利直播 | 欧美一级片播放 | 欧美在线视频一区二区三区 | 久久,天天综合 | 国产原创在线 | 91视频com | 西西www4444大胆视频 | 免费观看的av网站 | 天天操导航 | 国产精品手机在线播放 | 911国产在线观看 | 久久久国产精华液 | 欧美一级免费片 | 亚洲精品小视频 | 激情网站网址 | 天天综合人人 | 99综合电影在线视频 | 一 级 黄 色 片免费看的 | www.久久久.cum| 亚洲精品在线播放视频 | 日韩在线视频免费看 | 亚洲首页| 日本视频不卡 | a一片一级 | 亚洲永久av| 日韩在线观看三区 | 91九色丨porny丨丰满6 | 99热在线看 | av观看免费在线 | 99久久精品免费看国产麻豆 | 麻豆视频国产在线观看 | www.久久免费视频 | 天天干天天上 | 色99网| 国产一区在线免费观看视频 | 成人宗合网 | 日韩免费av在线 | 国产玖玖在线 | www.天天射.com | 日日躁夜夜躁aaaaxxxx | 四虎成人av | 五月天婷亚洲天综合网精品偷 | 狠狠干电影 | 99久久精品国产一区二区三区 | 日本精品视频一区二区 | av超碰在线| 日韩二区在线播放 | 五月花激情 | 日韩电影在线观看一区二区三区 | 91在线91| 欧美va天堂va视频va在线 | 五月婷婷在线观看视频 | 国产九色在线播放九色 | 国产精品亚洲成人 | 在线观看va| 婷婷久久网站 | 在线观看av大片 | 91大神在线看| 亚洲情感电影大片 | 免费人成在线观看网站 | 国产亚洲精品久久网站 | 99国产情侣在线播放 | 人人草在线观看 | 91视频在线自拍 | 欧美国产精品一区二区 | 亚洲免费色 | 国产糖心vlog在线观看 | 国产精品一区二区在线 | 久久国产高清 | 在线免费观看黄色av | 婷婷丁香在线观看 | 亚洲精品国产精品国自产观看 | 午夜精品一区二区三区视频免费看 | 国产免费视频在线 | 天天激情综合网 | av免费在线免费观看 | 九九在线高清精品视频 | 国产护士在线 | www.操.com| 五月婷影院 | 国产成人一区二区精品非洲 | 国产精品毛片完整版 | 成人免费共享视频 | 精品国产激情 | 人人草人 | 久久国内精品99久久6app | av一级久久 | 在线免费观看欧美日韩 | 亚洲精品在线观看的 | 欧美国产精品久久久久久免费 | 久久综合久久综合这里只有精品 | 四虎影视国产精品免费久久 | 特级毛片网站 | 国产黄在线播放 | 国产精品久久久视频 | 国产黄免费 | 国产一区二区三区在线 | 亚洲欧美视频在线播放 | 国产一区二区不卡视频 | 91视频xxxx| 国产色婷婷精品综合在线手机播放 | 久久男女视频 | 久草精品视频在线播放 | 国产精品二区三区 | 国产一区二区在线视频观看 | 精品欧美一区二区精品久久 | 久久伊人综合 | 日韩黄色在线电影 | 成人国产精品久久久久久亚洲 | 亚洲女欲精品久久久久久久18 | 高清不卡毛片 | 一区二区中文字幕在线播放 | 久久看毛片 | 热久久免费国产视频 | 亚洲精品国产精品国产 | 午夜视频在线观看欧美 | 毛片www| 91视频在线看 | 国模吧一区 | 国产aaa免费视频 | 91麻豆传媒 | 在线久热| 狠狠色噜噜狠狠狠 | 亚洲日本一区二区在线 | 麻豆影视在线观看 | 亚洲理论视频 | bbbbb女女女女女bbbbb国产 | 又湿又紧又大又爽a视频国产 | 国产精品电影一区 | 一本色道久久综合亚洲二区三区 | 免费国产ww | 欧美成人免费在线 | 人人揉人人揉人人揉人人揉97 | 日韩sese| 国产看片 色| 国产群p视频 | 日韩国产精品一区 | 国产一区在线免费 | 日韩精品一区二区在线观看 | 久久96国产精品久久99漫画 | 激情五月亚洲 | 玖玖视频国产 | 521色香蕉网站在线观看 | 狠狠久久伊人 | 国产午夜麻豆影院在线观看 | 91激情视频在线 | 美女中文字幕 | 在线观看久草 | av一级在线 | 天天碰天天操视频 | 国产精品美女网站 | 国产精品久久久久久久久毛片 | 色吊丝在线永久观看最新版本 | 97视频播放 | 五月天六月丁香 | 精品av在线播放 | 欧美大片在线观看一区 | 国产91全国探花系列在线播放 | 日韩高清一区在线 | 国产中的精品av小宝探花 | 欧美a级在线播放 | 亚洲三级黄色 | 精品国产一二三 | 97av在线| 久久国产精品影视 | 五月婷婷激情综合网 | 天天摸天天操天天舔 | 99久久精品免费看国产麻豆 | 婷婷色狠狠 | 欧洲高潮三级做爰 | 欧美日韩二区在线 | 亚洲无人区小视频 | 日韩精品aaa| 欧美在线观看视频一区二区三区 | 中文字幕中文中文字幕 | 婷婷深爱网 | 久久一区二区免费视频 | 国产高清视频在线播放一区 | 日日夜精品 | 五月婷婷黄色 | 日韩一区二区三区免费视频 | 992tv人人网tv亚洲精品 | 国产精品九九九九九九 | 首页国产精品 | 色在线免费 | 中文字幕人成一区 | 色免费在线| 亚洲国产成人精品在线观看 | 精产嫩模国品一二三区 | 国产精品成人国产乱一区 | 久久久久久久看片 | 久久电影中文字幕视频 | 国产福利网站 | 久久久久久久久久久网站 | 91成人看片| 日日躁你夜夜躁你av蜜 | 超碰在线97观看 | 狠狠综合久久av | 国产美腿白丝袜足在线av | 亚洲视频在线视频 | 日韩在线视频线视频免费网站 | 日本公妇在线观看 | 亚洲精品综合欧美二区变态 | 高清中文字幕av | 日韩在线观看视频在线 | 伊人婷婷综合 | 麻豆影音先锋 | 欧美va天堂在线电影 | 91在线精品秘密一区二区 | 日韩乱码在线 | 在线观看国产一区二区 | 黄色一级免费网站 | 91福利视频一区 | 欧美作爱视频 | 亚洲日本韩国一区二区 | 国产在线观看午夜 | 久热av| 日韩三级av | 国产尤物视频在线 | 高清一区二区三区av | 91亚洲精品国偷拍自产在线观看 | 91中文在线视频 | 亚洲一区二区三区毛片 | 国产一区久久久 | 日韩在线视频免费播放 | 亚洲国产97在线精品一区 | 亚洲码国产日韩欧美高潮在线播放 | 久久尤物电影视频在线观看 | 日韩精品大片 | 精品亚洲成人 | 精品国产综合区久久久久久 | 国产精品久久网 | 在线免费观看黄 | 999在线精品 | 蜜桃av综合网 | 欧美精品久久久久久 | 天天干,天天操 | 天天操天天摸天天爽 | 欧美福利片在线观看 | 97国产精品久久 | 99久久婷婷国产综合亚洲 | 亚洲精品成人在线 | 色视频在线观看 | 在线观看你懂的网址 | 日本精品视频在线播放 | 久久久久久久久久久成人 | 色97在线| 在线观看亚洲精品视频 | 丁香婷婷综合激情五月色 | 亚洲激情精品 | 久久一区二区三区国产精品 | 日韩免费中文 | 婷婷网站天天婷婷网站 | 国产免费xvideos视频入口 | 99在线精品免费视频九九视 | 亚洲一二三久久 | 在线视频区| 手机av网站| 337p日本欧洲亚洲大胆裸体艺术 | 久久久久久久久久久久久影院 | 五月婷婷在线视频观看 | 日日爱视频 | 玖玖综合网 | 久久久国产视频 | 五月婷激情 | 91视频在线免费观看 | 国产一在线精品一区在线观看 | 成人宗合网 | 99精品国产免费久久久久久下载 | av三级av| 中文字幕在线观看完整版电影 | 亚洲一级电影 | 成人丁香花| 一区二区三区在线免费 | 久久久精品成人 | 国产伦精品一区二区三区高清 | 欧美日韩午夜爽爽 | 色婷婷综合久久久中文字幕 | 欧美日本啪啪无遮挡网站 | 精品福利在线观看 | 超碰在线亚洲 | 色偷偷88888欧美精品久久 | 狠狠色丁香婷婷综合久小说久 | 三级黄色大片在线观看 | 久久久久久久久久久影院 | 91亚洲精品国产 | 日日躁夜夜躁xxxxaaaa | 美女又爽又黄 | 六月天综合网 | 成人av影视在线 | 午夜三级理论 | 国产精品porn | 免费看的毛片 | 黄色的网站在线 | 96香蕉视频 | 91桃色国产在线播放 | 美女视频免费一区二区 | 婷婷久久综合网 | 国产永久免费 | 午夜在线国产 | 一区二区三区手机在线观看 | 国内偷拍精品视频 | 久久亚洲美女 | 日本久久久久久科技有限公司 | 中文字幕有码在线 | 色狠狠综合 | 久久精品视 | 91亚洲欧美 | 六月色婷婷 | 日韩精品一区二区不卡 | 99精品免费久久久久久久久日本 | 日韩精品欧美一区 | 超碰成人免费电影 | 国产日韩在线观看一区 | 成+人+色综合| av观看久久久 | 99热官网 | 在线观看成人小视频 | 久久天天躁狠狠躁亚洲综合公司 | 中文字幕一区二区三区视频 | 久久综合加勒比 | 国产一级在线免费观看 | 欧美精品久久天天躁 | 亚洲精品乱码白浆高清久久久久久 | 国产精品va最新国产精品视频 | 激情视频一区二区三区 | 日韩欧美亚州 | 久久久99精品免费观看乱色 | 亚洲激情小视频 | av日韩中文 | 一区二区精品视频 | 亚洲日本在线视频观看 | 黄色大片免费网站 | 久久精品男人的天堂 | 成人av在线网 | 亚洲成人资源网 | 国产精品成人一区二区三区 | 24小时日本在线www免费的 | 99热官网 | 狠狠躁日日躁狂躁夜夜躁 | 六月色婷 | 天天干天天射天天爽 | 国产91成人 | 亚洲天堂精品视频在线观看 | 黄色www免费| 久久国产精品久久w女人spa | 国产 精品 资源 | 黄污视频网站大全 | 蜜臀av在线一区二区三区 | 国产精品视频区 | 欧美一二三专区 | 欧美在线观看视频一区二区 | 国产免费一区二区三区最新6 | 最近久乱中文字幕 | 91av资源网 | 欧美黑人性爽 | 成人在线免费观看网站 | 国产精久久 | 日韩亚洲国产精品 | 中文字幕文字幕一区二区 | 国产精品综合久久久久久 | 久久久久久久久久国产精品 | 久草在线播放视频 | 色a4yy| 久久精品国产亚洲精品2020 | 人人澡澡人人 | 久久九九影视网 | 看av免费网站 | 成人毛片一区二区三区 | 久久99国产一区二区三区 | 国产欧美精品在线观看 | 深爱激情综合 | 成年人免费看av | 国产黄色av影视 | 五月婷婷,六月丁香 | 日韩亚洲国产中文字幕 | 69国产在线观看 | 精品一二三区视频 | 久草免费电影 | 欧美另类一二三四区 | 亚洲午夜av| 欧美一级黄大片 | 久久亚洲精品国产亚洲老地址 | 国产视频欧美视频 | 日日弄天天弄美女bbbb | 麻豆免费在线播放 | 手机在线小视频 | 激情五月婷婷综合网 | 在线观看自拍 | 视频在线观看国产 | 一区二区三区影院 | 99在线热播精品免费 | 免费男女羞羞的视频网站中文字幕 | 中文字幕高清av | 亚洲永久精品一区 | 天天躁日日躁狠狠躁av麻豆 | 国产韩国日本高清视频 | 亚洲国产电影在线观看 | 久久亚洲区 | 国产视频美女 | 国产99久久久久 | 不卡的av电影在线观看 | 天天综合天天做天天综合 | 日本久久久精品视频 | 亚洲国产美女精品久久久久∴ | h视频日本 | 久久久久久综合 | 国产 欧美 在线 | 国产成人精品免费在线观看 | av在线色| 午夜婷婷在线播放 | 久久天天躁夜夜躁狠狠85麻豆 | 久久99久久99精品免观看粉嫩 | 99精品亚洲 | 成人一级免费视频 | 国产精品一区二区三区在线播放 | 日批网站免费观看 | 久久综合狠狠综合久久激情 | 久久综合狠狠综合久久激情 | 天天操夜 | 国产午夜视频在线观看 | 久草视频观看 | 亚洲乱亚洲乱妇 | 国产一线二线三线在线观看 | 成人毛片一区 | 日本三级香港三级人妇99 | 涩av在线 | 欧美精品久久天天躁 | 99自拍视频在线观看 | 日韩电影中文,亚洲精品乱码 | 国产小视频福利在线 | 激情欧美一区二区三区免费看 | 伊人超碰在线 | 在线观看av片 | 国产精品麻豆91 | 欧美aaa一级| av黄色成人| 日韩91精品 | 国产专区免费 | 久久激情五月激情 | .精品久久久麻豆国产精品 亚洲va欧美 | 国产 日韩 欧美 在线 | 欧美色伊人 | 中国老女人日b | 亚洲欧美日韩在线看 | 97免费| 国产亚洲精品久久久久久 | 国产精品高清一区二区三区 | 精品爱爱 | 成人av资源 | 久久尤物电影视频在线观看 | 欧美一级免费黄色片 | 欧美精品成人在线 | 99色婷婷| 中文字幕黄色av |