前言
這是前段時間我在公司內(nèi)部Android組的技術(shù)分享會上,以響應(yīng)式編程為主題做的一個專題分享嗓化,反饋還不錯撩鹿,但是也有很多問題睡扬,因此我根據(jù)反饋重新修改和完善了相關(guān)的論述盟蚣,組成一篇文章分享給大家。
研究這個問題的初衷在于目前很多人對于RxJava這種庫卖怜,以及它背后所體現(xiàn)的編程思想了解不多屎开,而網(wǎng)上也很少有人能夠把它講明白,很多時候只能參考網(wǎng)絡(luò)上的一些RxJava項目實踐去學(xué)習(xí)RxJava的使用马靠。但是我始終認(rèn)為奄抽,只有熟悉響應(yīng)式編程的思想,才能更好的使用RxJava這個Rx拓展庫甩鳄。
目前網(wǎng)絡(luò)上中英文的資料對于響應(yīng)式編程的描述有些兩極分化逞度,要么只能將響應(yīng)式的概念解釋清楚,沒有可實踐性妙啃,要么就是從RxJava的定義出發(fā)來解釋響應(yīng)式編程第晰。比如“響應(yīng)式編程就是異步數(shù)據(jù)流編程”這種話,看似抓住了重點彬祖,但是實際上你很難從這個定義中收獲有用的東西。
因此品抽,今天我希望講講響應(yīng)式編程的思想和它的優(yōu)勢储笑,以及怎樣去理解響應(yīng)式編程才能更好的把它融入到我們的編程工作中,把響應(yīng)式編程變成我們手中的利器圆恤。
響應(yīng)式的由來
我們先來聊一聊響應(yīng)式的由來突倍,對于它的由來,我們可能需要先從一段常見的代碼片段看起
int a=1;
int b=a+1;
System.out.print(“b=”+b) // b=2
a=10;
System.out.print(“b=”+b) // b=2
上面是一段很常見的代碼盆昙,簡單的賦值打印語句羽历,但是這種代碼有一個缺陷,那就是如果我們想表達(dá)的并不是一個賦值動作淡喜,而是b和a之間的關(guān)系,即無論a如何變化秕磷,b永遠(yuǎn)比a大1。那么可以想見炼团,我們就需要花額外的精力去構(gòu)建和維護(hù)一個b和a的關(guān)系澎嚣。
而響應(yīng)式編程的想法正是企圖用某種操作符幫助你構(gòu)建這種關(guān)系。
它的思想完全可以用下面的代碼片段來表達(dá):
int a=1;
int b <= a+1; // <= 符號只是表示a和b之間關(guān)系的操作符
System.out.print(“b=”+b) // b=2
a=10;
System.out.print(“b=”+b) // b=11
這就是是響應(yīng)式的思想瘟芝,它希望有某種方式能夠構(gòu)建關(guān)系易桃,而不是執(zhí)行某種賦值命令。
至此你可能不禁要問锌俱,我們?yōu)槭裁葱枰獦?gòu)建關(guān)系的代碼而不是命令式的代碼呢晤郑?如果你翻一翻自己正在開發(fā)的APP,你就能看到的每一個交互的頁面其實內(nèi)部都包含了一系列的業(yè)務(wù)邏輯。而產(chǎn)品的每個需求造寝,其實也對應(yīng)了一系列的業(yè)務(wù)邏輯相互作用磕洪。總之,我們的開發(fā)就是在構(gòu)建一系列的業(yè)務(wù)邏輯之間的關(guān)系扔枫。你說我們是不是需要構(gòu)建關(guān)系的代碼蝶棋?
說回響應(yīng)式,前期由于真實的編程環(huán)境中并沒有構(gòu)建關(guān)系的操作符叫榕,主流的編程語言并不支持這種構(gòu)建關(guān)系的方式,所以一開始響應(yīng)式主要停留在想的層面姊舵,直到出現(xiàn)了Rx和一些其他支持這種思想的框架,才真正把響應(yīng)式編程引入到了實際的代碼開發(fā)中晰绎。
Rx是響應(yīng)式拓展,即支持響應(yīng)式編程的一種拓展,為響應(yīng)式在不同語言中的實現(xiàn)提供指導(dǎo)思想
什么是響應(yīng)式編程
說完了了響應(yīng)式的由來括丁,我們就可以談?wù)勈裁词琼憫?yīng)式編程了荞下。
響應(yīng)式編程是一種通過異步和數(shù)據(jù)流來構(gòu)建事務(wù)關(guān)系的編程模型。這里每個詞都很重要史飞,“事務(wù)的關(guān)系”是響應(yīng)式編程的核心理念尖昏,“數(shù)據(jù)流”和“異步”是實現(xiàn)這個核心理念的關(guān)鍵。為了幫助大家理解這個概念构资,我們不妨以APP初始化業(yè)務(wù)為例來拆解一下這幾個詞抽诉。
這是一個比較理想化的APP初始化邏輯,完成SDK初始化吐绵,數(shù)據(jù)庫初始化迹淌,登陸,之后跳轉(zhuǎn)主界面
事務(wù)的關(guān)系
-
事務(wù)
- 是一個十分寬泛的概念己单,它可以是一個變量唉窃,一個對象,一段代碼纹笼,一段業(yè)務(wù)邏輯.....但實際上我們往往把事務(wù)理解成一段業(yè)務(wù)邏輯(下文你均可以將事務(wù)替換為業(yè)務(wù)邏輯來理解)纹份,比如上圖中,事務(wù)就是指APP初始化中的四個業(yè)務(wù)邏輯廷痘。
-
事務(wù)的關(guān)系
- 這種關(guān)系不是類的依賴關(guān)系矮嫉,而是業(yè)務(wù)之間實際的關(guān)系。比如APP初始化中牍疏,SDK初始化蠢笋,數(shù)據(jù)庫初始化,登陸接口鳞陨,他們共同被跳轉(zhuǎn)頁面業(yè)務(wù)所依賴昨寞。但是他們?nèi)齻€本身并沒有關(guān)聯(lián)瞻惋。這也只是業(yè)務(wù)之間較為簡單的關(guān)系,實際上援岩,根據(jù)我們的需求App端會產(chǎn)生出許多業(yè)務(wù)之間錯綜復(fù)雜的關(guān)系歼狼。
數(shù)據(jù)流
關(guān)于Rx的數(shù)據(jù)流有很多說法,比如“Everything is a stream”,“Thinking with stream”等等享怀。雖然我明白這只是想強(qiáng)調(diào)流的重要性羽峰,可是這些話折射出來的編程思路其實是很虛無縹緲的,只會讓開發(fā)者對于Rx編程更加迷惑添瓷。
實際上梅屉,數(shù)據(jù)流只是事務(wù)之間溝通的橋梁。
比如在APP初始化中鳞贷,SDK初始化坯汤,數(shù)據(jù)庫初始化,登陸接口這些業(yè)務(wù)完成之后才會去安排頁面跳轉(zhuǎn)的操作搀愧,那么這些上游的業(yè)務(wù)在自己工作完成之后惰聂,就需要通知下游,通知下游的方式有很多種咱筛,其中最棒的的方式就是通過數(shù)據(jù)(事件)流搓幌。每一個業(yè)務(wù)完成后,都會有一條數(shù)據(jù)(一個事件)流向下游迅箩,下游的業(yè)務(wù)收到這條數(shù)據(jù)(這個事件)溉愁,才會開始自己的工作。
但是沙热,只有數(shù)據(jù)流是不能完全正確的構(gòu)建出事務(wù)之間的關(guān)系的。我們依然需要異步編程罢缸。
異步
異步編程本身是有很多優(yōu)點的篙贸,比如挖掘多核心CPU的能力,提高效率枫疆,降低延遲和阻塞等等爵川。但實際上,異步編程也給我們構(gòu)建事務(wù)的關(guān)系提供了幫助息楔。
在APP初始化中寝贡,我們能發(fā)現(xiàn)SDK初始化,數(shù)據(jù)庫初始化值依,登陸接口這三個業(yè)務(wù)本身相互獨立圃泡,應(yīng)當(dāng)在不同的線程環(huán)境中執(zhí)行,以保證他們不會相互阻塞愿险。而假如沒有異步編程颇蜡,我們可能只能在一個線程中順序調(diào)用這三個相對耗時較多的業(yè)務(wù),最終再去做頁面跳轉(zhuǎn),這樣做不僅沒有忠實反映業(yè)務(wù)本來的關(guān)系风秤,而且會讓你的程序“反應(yīng)”更慢
小結(jié)
總的來說鳖目,異步和數(shù)據(jù)流都是為了正確的構(gòu)建事務(wù)的關(guān)系而存在的。只不過缤弦,異步是為了區(qū)分出無關(guān)的事務(wù)领迈,而數(shù)據(jù)流(事件流)是為了聯(lián)系起有關(guān)的事務(wù)。
APP初始化應(yīng)該怎么寫
許多使用Rx編程的同學(xué)可能會使用這種方式來完成APP的初始化碍沐。
Observable.just(context)
.map((context)->{login(getUserId(context))})
.map((context)->{initSDK(context)})
.map((context)->{initDatabase(context)})
.subscribeOn(Schedulers.newThread())
.subscribe((context)->{startActivity()})
其實狸捅,這種寫法并不是響應(yīng)式的,本質(zhì)上還是創(chuàng)建一個子線程抢韭,然后順序調(diào)用代碼最后跳轉(zhuǎn)頁面薪贫。這種代碼依然沒有忠實反映業(yè)務(wù)之間的關(guān)系。
在我心目中刻恭,響應(yīng)式的代碼應(yīng)該是這樣的
Observable obserInitSDK=Observable.create((context)->{initSDK(context)}).subscribeOn(Schedulers.newThread())
Observable obserInitDB=Observable.create((context)->{initDatabase(context)}).subscribeOn(Schedulers.newThread())
Observable obserLogin=Observable.create((context)->{login(getUserId(context))})
.map((isLogin)->{returnContext()})
.subscribeOn(Schedulers.newThread())
Observable observable = Observable.merge(obserInitSDK,obserInitDB,obserLogin)
observable.subscribe(()->{startActivity()})
大家應(yīng)該能很明顯看到兩段代碼的區(qū)別瞧省,第二段代碼完全遵照了業(yè)務(wù)之間客觀存在的關(guān)系,可以說代碼和業(yè)務(wù)關(guān)系是完全對應(yīng)的鳍贾。
那么這帶來了什么好處呢鞍匾?當(dāng)initSDK,initDB,Login都是耗時較長的操作時骑科,遵照業(yè)務(wù)關(guān)系編寫響應(yīng)式代碼可以極大的提高程序的執(zhí)行效率橡淑,降低阻塞。
理論上講咆爽,遵照業(yè)務(wù)關(guān)系運行的代碼在執(zhí)行效率上是最優(yōu)的梁棠。
為什么引入響應(yīng)式編程
對響應(yīng)式編程有了一些了解之后,我知道馬上會由很多人跳出來說斗埂,不使用這些響應(yīng)式編程我們還不是一樣開發(fā)APP符糊?
在這里我希望你理解一點,當(dāng)我們用老辦法開發(fā)APP的時候呛凶,其實做了很多妥協(xié)男娄,比如上面的APP初始化業(yè)務(wù),三個無關(guān)耗時操作為了方便漾稀,我們往往就放在一個線程環(huán)境中去執(zhí)行模闲,從而犧牲了程序運行的效率。而且實際開發(fā)中崭捍,這種類似的業(yè)務(wù)邏輯還有很多尸折,甚至更加復(fù)雜。假如不引入響應(yīng)式的思路殷蛇,不使用Rx的編程模型翁授,我們面對這么些復(fù)雜的業(yè)務(wù)關(guān)系真的會很糟心拣播。假如你做一些妥協(xié),那就會犧牲程序的效率收擦,假如你千辛萬苦構(gòu)建出業(yè)務(wù)關(guān)系贮配,最終寫出來的代碼也一定很復(fù)雜難以維護(hù)。所以塞赂,響應(yīng)式編程其實是一種更友好更高效的開發(fā)方式泪勒。
根據(jù)個人經(jīng)驗來看,響應(yīng)式編程至少有如下好處:
- 在業(yè)務(wù)層面實現(xiàn)代碼邏輯分離宴猾,方便后期維護(hù)和拓展
- 極大提高程序響應(yīng)速度圆存,充分發(fā)掘CPU的能力
- 幫助開發(fā)者提高代碼的抽象能力和充分理解業(yè)務(wù)邏輯
- Rx豐富的操作符會幫助我們極大的簡化代碼邏輯
一個復(fù)雜一些的例子
接下來,我就以我們團(tuán)隊目前的一款產(chǎn)品的頁面為例仇哆,詳細(xì)點介紹運用響應(yīng)式編程的正確姿勢沦辙。
首先,UI和產(chǎn)品溝通后讹剔,可能會給我們這樣的設(shè)計圖(加上一些尺寸的標(biāo)注)油讯。但是我們并不需要急忙編碼,我們首先要做的是區(qū)分其中相對獨立的模塊延欠。
上圖我做了一點簡單的標(biāo)注陌兑。把這個頁面的業(yè)務(wù)邏輯簡單的分為四個相互獨立的模塊,分別是視頻模塊由捎,在線人數(shù)模塊兔综,禮物模塊,消息模塊狞玛。他們相互獨立软驰,互不影響。接下來心肪,我們再去分析每個模塊內(nèi)部的業(yè)務(wù)并構(gòu)建起業(yè)務(wù)之間的關(guān)系锭亏。大致如下:
構(gòu)建了業(yè)務(wù)之間的關(guān)系圖,其實我們的工作已經(jīng)完成了一半了蒙畴,接下來就是用代碼實現(xiàn)這個關(guān)系圖贰镣。在這里呜象,我就以其中一小段業(yè)務(wù)關(guān)系來編寫代碼給大家示范膳凝。
Observable obserDownload=Observable.just(url)
.map((url)->{getZipFileFromRemote(url)});
Observable obserLocal=Observable.just(url)
.map((url)->{getZipFileFromLocal(url)});
Observable obserGift=Observable.concat(obserLocal,obserDownload)
.takeUnitl((file)->{file!=null});
obserGift.subscribeOn(Schedulers.io()).flatMap((file)->{readBitmapsFromZipFile(file)})
.subscribe((bitmap)->{showBitmap(bitmap)})
以上是我手寫的偽代碼,可能細(xì)節(jié)上有些問題恭陡,但大體思路就是這樣蹬音。
有人可能會說,那是因為你運用操作符比較熟練才能這么寫休玩。其實操作符都是我查的著淆,我記不住那么多操作符劫狠,所以基本上我都是先理清楚業(yè)務(wù)之間的關(guān)系,需要和并邏輯的時候永部,就去去查合并類的操作符独泞,需要條件判斷來分流的邏輯時去找條件判斷類的操作符√β瘢基本上都能滿足需求懦砂。你瞧,寫代碼就是這么簡單组橄,后續(xù)即使需要增加需求荞膘,代碼修改起來也很清晰,因為無關(guān)的業(yè)務(wù)已經(jīng)被你分離好了玉工。
所以羽资,趕緊在你的項目中引入響應(yīng)式編程吧!
勘誤
暫無
后記
前一段時間換了工作遵班,在目前的團(tuán)隊里除了實現(xiàn)日常需求屠升,也會負(fù)責(zé)項目重構(gòu)這一塊,我會更多的使用響應(yīng)式的方式重構(gòu)項目费奸,同時也會盡力在團(tuán)隊內(nèi)部推動響應(yīng)式編程的使用弥激,一旦有了新的體會,我也會在第一時間和大家分享愿阐。
由于這篇文章講的是響應(yīng)式編程微服,因此更多的使用的Rx這個名稱,而不是RxJava,因為RxJava只是響應(yīng)式編程在Java語言中的實現(xiàn)缨历。不過里面的偽代碼都是使用RxJava來編寫的以蕴,希望大家能夠理解。
假如你對RxJava不是十分了解的話辛孵,歡迎大家去看我之前寫的Rxjava系列文章