我們眼中 RxJava 式的網路請求
誕生 5 年之久的 RxJava,已經不只是一個開源庫,可以說它的誕生 改變了我們寫程式碼的方式 ,把它比作「神兵利器」也毫不為過。我們現在已經能看到各式各樣名為「最佳實踐」的使用教程,如果我們沒能用好這把利器,不僅不會發揮它的作用,反而會傷著我們自己。
回顧它的誕生原因,是為了解決 回撥地獄 (callback hell) 以及 麻煩的執行緒切換 。在 Android 開發中,哪個地方最會出現 多層的回撥巢狀 以及 頻繁的執行緒切換 呢?對!沒錯!是「網路請求」。所以 RxJava、Retrofit 這倆兄弟總會一起出現的,我們專案中關於 RxJava 的使用,也幾乎都和網路請求相關。
過去的經驗
最初我們對 RxJava + Retrofit 的使用經驗都是來源於 RxJava 與 Retrofit 結合的最佳實踐 這篇文章,相信大家都看過。這篇文章中的基本封裝思想是: 訂閱每個網路請求的流,將流的訂閱結果再通過回撥的方式返給流(也就是網路請求)的建立者。 如下所示:
//HttpMethod public void getTopMovie(final ResultListener listener, int start, int count){ movieService.getTopMovie(start, count) .subscribeOn(Schedulers.io()) .unsubscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Subscriber(){ @Override public void onStart() {} @Override public void onNext(Subject t) { listener.onNext(t) } @Override public void onError(Throwable e) {} @Override public void onCompleted() {} }) } //Activity HttpMethods.getInstance().getTopMovie(new ResultListener(){ @Override public void onNext(Subject t){ //handle result } }, 0, 10)
有什麼問題?
這種封裝方式,對於初步的使用以及簡單的專案,是沒有問題的。但是遇到複雜一點的網路請求,它的擴充套件性就不那麼靈活了:
- 多個連續的網路請求怎麼寫?按照上面的那種封裝方式,我們有兩種選擇
- 拆解這個請求,在
subscribe
之前通過flatMap
發起第二個或者第三個網路請求。這種寫法肯定會影響專案中已有的外部呼叫。 - 在
onNext
中發起第二個請求,再在第二個網路請求的onNext
中發起第三個網路請求……這一層又一層的回撥巢狀,正是用 RxJava 所能解決、避免的這樣寫,我們就又回到了最初的原點。
- 拆解這個請求,在
- 怎麼取消網路請求?不取消,意味著記憶體洩露的風險。
回到 RxJava 本身
RxJava 提供給我們的、我們所中意的強大之處在哪?在於它的「操作符」, map
、 flatMap
、 zip
等等,甚至執行緒的切換 subscribeOn
、 observeOn
也是操作符。RxJava 的各種強大的功能就是通過各式各樣的「操作符」實現的。
操作符操作的是什麼?流。流( Observable
、 Flowable
)是 RxJava 的基本單位。所以一套鏈式請求拆開應該是這樣的:

所以說,網路請求庫對外提供網路請求的結果應該是以「流」的形式進行提供:
- 單個網路請求,對外提供單個「流」
- 多個網路請求,將多個網路請求結果流通過「操作符」組合成一個「流」對外提供
- 持久化:網路請求結果流和持久化的快取流,總能通過「操作符」組合成一個對外提供的結果「流」
我們需要背壓嗎?
當生產者大於消費者,
Flowable
天然支援背壓。所以
Flowable
這個萬金油,不管三七二十一,直接拿來用是沒有問題的。
但是, 網路請求,會產生背壓問題嗎?不會,為了防止抬槓,可以說大部分情況下是不會的。 網路請求的每一個流,即用即走,上游的生產者( Request
)和下游的消費者( Responese
),永遠是一對一的關係,不會出現連續的事件流。殺雞焉用牛刀,所以我們可以退一步,改用 Observable
。
網路請求不會出現連續的事件流,在 onNext
出現之後, onComplete
馬上就會被呼叫,所以只需要這兩者中的一個就夠了,也就不用考慮 Observable
,同樣 Maybe
也是可以排除的。
剩下的也就只有 Single
和 Completable
了,相對於 Single
, Completable
沒有 map
和 flatMap
方法。所以需要進一步處理網路請求結果的我們,可以選擇使用 Single
。
丟擲異常
網路請求過程,協議層的異常會自動拋至 onError()
,如 404、503 錯誤。對於如下 有請求結果但無目標請求資料 ,我們也應當作為異常來處理:
{ "code": "6002", "msg": "公鑰為空" }
畢竟這樣的請求結果,是後端經過異常處理返回給我們的。
假定我們的請求結果是這樣的正規化:
data CommonResult<T>( var code: Int = 0, var data: T? = null, var message: String? = null )
我們活用 RxJava 的操作符,用 map
來處理請求到的 ResponseBody
(這也是前面選擇 Single 的原因),為了便於複用,可以定義一個這樣的 mapper:
class CommonResultMapper<T> : Function<CommonResult<T>, T> { override fun apply(t: CommonResult<T>): T { val data = t.data if (t.code == SUCCESS_CODE && data != null) { return data } else { //丟擲異常 throw Throwable("請求 $t 失敗") } } }
使用這個定義好的 mapper:
@GET(PUSH_URL) fun fetchTag(@Query("udid") udid: String): Single<CommonResult<Tag>> fun fetchTagResult(udid: String): Single<Tag> { return netService.fetchTag(udid) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .map(CommonResultMapper()) }
如果你願意,你還可以將 執行緒切換 和 資料處理 結合在一起,使用 RxJava 的 Transformer
//定義一個 transformer fun <T> resultTransformer(): SingleTransformer<CommonResult<T>, T> { return SingleTransformer { single -> single.subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .map(CommonResultMapper()) } } //使用 fun fetchTagResult(udid: String): Single<Tag> { return netService.fetchTag(udid) .compose(resultTransformer()) }
使用這樣封裝,結果歸結果,異常歸異常。
fetchTagResult("123321") .subscribeBy( onSuccess = { tag -> //結果 }, onError = { e -> //異常 } )
回顧上圖中的 對內封裝 和 對外可見 ,在得到真正想要的網路請求結果之前,需要一直保持 對內封裝 的狀態。因此,如果需要同時或者按順序發起多個網路請求,那麼就應該在 對內封裝 中進行操作,例如可以使用 flatMap
按順序發起第二個網路請求:
fun fetchUserSingle(tag: Tag): Single<User> { return netService.fetchUser(tag) } fun fetchUserResult(udid: String): Single<User> { return netService.fetchTag(udid) .compose(resultTransformer()) .flatMap{ tag -> //使用第一個請求的結果作為第二個請求的引數 return@flatMap fetchUserSingle(tag) } }
無論如何,善用 操作符 ,我們的程式碼總會是「鏈式」的。
取消網路請求與記憶體洩漏
最後還需要關注一下這裡的記憶體洩漏問題,在 Activity
銷燬時,要及時取消掉這些已經失去上下文意義的網路請求。這裡我們及時 unsubscribe
就好了。
同時在管理生命週期方面,也有更成熟的方案: RxLifecycle 。
以上
關注公眾號,瞭解更多:point_down: