安卓網(wǎng)絡(luò)請求最佳實踐
在安卓開發(fā)中栏笆,經(jīng)過多年的發(fā)展森渐,網(wǎng)絡(luò)請求的架構(gòu)基本定型做入,通常是 OkHttp + Retrofit + RxJava
,盡管在目前在 google 的推動下同衣,有些開發(fā)者正在使用 Kotlin
的 協(xié)程
代替 RxJava
母蛛。但是也僅是一線程調(diào)度的一部分,本文依舊可以在網(wǎng)絡(luò)請求部分給出很好的建議乳怎。
返回錯誤碼
網(wǎng)絡(luò)請求可能發(fā)生各種各樣的錯誤彩郊,當錯誤發(fā)生時前弯,Http 會通過響應頭返回 狀態(tài)碼
和 原因短語
來標識錯誤狀態(tài)和原因。
自從 RxJava 2.0
普及之后秫逝,一些項目中的錯誤碼返回開始出現(xiàn)了不太好的方式 —— 在 Http body
中自定義響應碼恕出。
{
"code": 200,
"msg": "",
"datas": ...
}
處于什么樣的考慮很難一一找到原始開發(fā)人員進行解答,從項目中代碼的使用可以推斷违帆,很可能有以下原因:
- Retrofit 的 Converter 可以返回直接將 body 體轉(zhuǎn)換成數(shù)據(jù)對象浙巫。這樣很多返回結(jié)果可能不想要響應頭。于是會出現(xiàn)這樣的寫法刷后。
@GET("test")
fun getPost(): Observer<BodyContent<Post>>
BodyContent 則是自定義的 Bean 類的畴,用于轉(zhuǎn)換 json 數(shù)據(jù)。
class BodyContent<D> {
var code = 0
var msg: String? = null
var data: D? = null
}
這樣做并不是一個好的實踐尝胆,因為 Http 響應頭和響應體中自定義的狀態(tài)碼丧裁,不能僅判斷響應體中的狀態(tài)碼,這樣做相當于忽略了響應頭中的錯誤含衔。一些服務器服務器本身出現(xiàn)錯誤或者中間件中出現(xiàn)錯誤煎娇,可能沒有執(zhí)行到該請求的代碼,就返回了錯誤贪染,此時可能沒有響應體或自定義錯誤碼缓呛,而是 Http 請求頭中返回錯誤。
如果我們除了檢測自定義的錯誤碼杭隙,還檢測 HTTP 請求頭中錯誤碼哟绊,就需要兩層判斷,代碼就顯得繁雜且冗余痰憎。而且有時候兩層狀態(tài)碼相同的數(shù)字表示不同的含義匿情,很讓人迷惑。
- 跟響應體一樣信殊,通過
converter
獲取狀態(tài)碼炬称。
如果僅僅是為了獲取狀態(tài)碼,這有點得不償失涡拘。因為 Retrofit 的一個 Converter 插件寫的非常好玲躯,我們可以使用 Retrofit 的響應頭即可自動轉(zhuǎn)換
// Response 是 Retrofit 自帶的數(shù)據(jù)類,可以通過 Retrofit 的 code() 和 message() 方法來獲取狀態(tài)碼和狀態(tài)短語鳄乏。
@GET("post/{id}")
fun getPost(
@Path("id") postId: Long
): Observer<Response<Post>>
- 另一個原因是因為
RxJava 2
中不允許發(fā)送null
值跷车,當有些結(jié)果不需要返回結(jié)果的時候,例如上傳數(shù)據(jù)橱野,body 體可能是空的朽缴,轉(zhuǎn)換會發(fā)送一個 null 值,會導致 RxJava 檢測而拋出異常水援。這時候為了正常走返回結(jié)果邏輯密强,需要填充一點默認數(shù)據(jù)茅郎,為了和錯誤狀態(tài)統(tǒng)一,添加狀態(tài)碼就成了一個可能的選擇或渤。
為了處理這種情況系冗,一種選擇是返回 Retrofit 的 Response
作為結(jié)果,即使請求體為空薪鹦,也一定有響應掌敬。
// retrofit 的 Response 作為返回結(jié)果來處理響應體為 null。
@POST("send/post")
fun sendPost(
@Body post: Post
): Observer<Response<Void>>
然而池磁,這并不是唯一的選擇奔害,如果我們并不想處理狀態(tài)碼,或者響應頭地熄,返回這些數(shù)據(jù)并優(yōu)雅华临。
RxJava 真的不能處理返回結(jié)果為 null
嗎?其實它只是不允許在發(fā)送序列流中摻雜 null
值离斩。對于網(wǎng)絡(luò)請求這種不返回任何數(shù)據(jù)的银舱。其實就是請求完成了瘪匿。我們可以使用 Completable
來處理請求跛梗。
@POST("send/post")
fun sendPost(
@Body post: Post
): Completable
其實,對于 HTTP 請求這種結(jié)果棋弥,我們根本沒有必要使用 Observer
和 Flowable
核偿。因為它之后一個結(jié)果,而不會出現(xiàn)不斷發(fā)送數(shù)據(jù)的事件流顽染。
對于一定有響應體的請求漾岳,使用
Single
。一定沒有響應體的請求粉寞,使用
Completable
尼荆。特殊請求,有時候有響應體唧垦,有時候沒有的捅儒,返回
Maybe
。
合理的使用 Single
振亮、Completable
和 Maybe
可以有效的提高代碼的清晰度巧还。如果確實需要響應頭的中內(nèi)容,可以在他們的泛型中使用 Retrofit 的 Response
來獲取坊秸。
- 如果僅僅是因為包含一些自己應用中特殊狀態(tài)的狀態(tài)碼麸祷,這完全沒有必要,因為 Http 的響應碼僅使用了幾個褒搔,還剩下許多狀態(tài)碼可用于表示特屬含義阶牍。例如喷面,2xx 開頭的狀態(tài)碼表示正常結(jié)果。
RFC
僅定義了200 ~ 206
的狀態(tài)碼荸恕,其余都可以根據(jù)應用自己定義使用乖酬。我們甚至可以跳過 250 之前的,使用 251 ~ 299 之間的表示正常結(jié)果的某些情況融求,防止在未來它們被HTTP
標準協(xié)議使用咬像。
上面說來這么多錯誤碼,馬上就來討論處理錯誤生宛。
錯誤處理
有過單獨使用 OkHttp 和 Retrofit 而不使用 RxJava县昂,和 RxJava 一起使用兩種使用方案的人可能會意識到,當狀態(tài)碼是 200 ~ 299 以外的錯誤碼時陷舅,OkHttp 和 RxJava 走了 onResponse, 而 RxJava 走了 onError倒彰。 當使用 Body 中使用自定義的響應碼時,它也走了 RxJava 的 onNext 或者 onSuccess莱睁。這種將錯誤碼和正常結(jié)果放在一起會有以下問題:
在正常顯示數(shù)據(jù)之前要先判斷一次錯誤結(jié)果待讳,如果錯誤則要調(diào)用錯誤處理方法,而在
onFailure/OnError
方法中也需要調(diào)用錯誤處理處理方法仰剿,多一條分支顯得不是那么完美创淡。OkHttp/Retrofit 和 RxJava 的錯誤和正確流走的路徑不太一致。甚至 RxJava 自己的錯誤分支都不一致南吮。如果在 RxJava 中琳彩,返回結(jié)果僅僅是響應體則非 200 ~ 299 的錯誤走了
onError
。
@GET("post/{id}")
fun getPost(
@Path("id") postId: Long
): Observer<Post> // 僅返回 Response Body 數(shù)據(jù)
在 adapter-rxjava
的 BodyObservable
中的代碼中可以看到:
@Override
public void onNext(Response<R> response) {
// isSuccessfull 判斷了狀態(tài)碼
// public boolean isSuccessful() {
// return code >= 200 && code < 300;
// }
if (response.isSuccessful()) {
observer.onNext(response.body());
} else {
terminated = true;
Throwable t = new HttpException(response);
try {
observer.onError(t);
} catch (Throwable inner) {
...
}
}
}
而如果結(jié)果返回的是Retrofit 的 Response
部凑。
@GET("post/{id}")
fun getPost(
@Path("id") postId: Long
): Observer<Response<Post>> // 返回 Response露乏。
在 adapter-rxjava
的 ResultObservable
中的代碼則是:
@Override
public void onNext(Response<R> response) {
observer.onNext(Result.response(response));
}
@Override
public void onError(Throwable throwable) {
try {
observer.onNext(Result.error(throwable));
} catch (Throwable t) {
...
return;
}
observer.onComplete();
}
讓我們總結(jié)一些這些錯誤處理的流程
OkHttp <---------- 失敗重連,重定向跟蹤涂邀。Auth 認證重試
|
| -------------------┐
| |
| |
| |
onResponse onFailure 超時瘟仿,鏈接失敗,拋出 IOException 等異常比勉。
(Response包含非 |
200~300錯誤) |
| |
∨ ∨
Retrofit --------------┤ <--- Retrofit 主要處理請求參數(shù)劳较,和返回數(shù)據(jù)的轉(zhuǎn)換。錯誤處理沒有任何改變敷搪。
| |
| |
(Response) |
包含200~300錯誤 跟OkHttp 一樣
| |
∨ ∨
RxJava(ResponseBody) ---┤
| |
(200 ~299 正常結(jié)果) 增加 非 200~ 299 的錯誤結(jié)果
| |
| |
onNext/onSuccess onError
| |
∨ ∨
body 里判斷結(jié)果 錯誤顯示兴想。
為 Response 的情況
或 body 里包含錯誤碼
希望做的的情況
網(wǎng)絡(luò)請求框架的 Http 響應碼走一套邏輯。
能在一處判斷赡勘,不要有不同的分支嫂便。無論使用沒使用 Retrofit 和 RxJava 都是一樣的。
對上層開發(fā)者透明闸与,不用因為網(wǎng)絡(luò)請求錯誤而單獨繼承或者調(diào)用函數(shù)判斷錯誤碼毙替。
即達到這種效果
OkHttp/Retrofit/RxJava
|
┌------判斷響應碼--------┐
| |
onRespons |
onSuccess onFailure/OnError
(只有200~299的正確結(jié)果) |
| |
| |
∨ ∨
渲染邏輯 錯誤提示岸售。
要讓所有的上層結(jié)果返回結(jié)果都一致,OkHttp 的攔截器是絕佳的選擇厂画。我們即在此來處理請求結(jié)果凸丸。
public class ResponseStatusInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
Response response = chain.proceed(request);
if (!response.isSuccessful()) {
throw new ApiException(response.code(), response.message());
}
return response;
}
需要說明的是,如果使用 Java袱院,拋出的異常不是繼承自 IOException
將會有錯誤提示屎慢,但是如果使用 Kotlin,則不會提示忽洛,你必須使用繼承自 IOException
的類腻惠,否則將不會走到 onFaile 的回調(diào),這是因為在 OkHttp 的 RealCall
中判斷請求結(jié)果的代碼中:
@Override protected void execute() {
...
try {
Response response = getResponseWithInterceptorChain();
signalledCallback = true;
responseCallback.onResponse(RealCall.this, response);
} catch (IOException e) {
...
responseCallback.onFailure(RealCall.this, e); // IOException 會回調(diào) onFailure.
} catch (Throwable t) {
...
throw t; // 非 IOException 則會以及拋出異常欲虚。
} finally {
...
}
}
可以看到集灌,判斷如此簡單。但是如果你之前自定義了響應碼复哆,或者你項目中之前在代碼中處理錯誤碼的欣喧。很可能這些錯誤處理的地方非常多,一時修改不完梯找,你并不想一次性將整個項目修改唆阿,這樣工作量很大,也很難保證修改的正確性初肉。我們希望漸進式的演化酷鸦,在新的接口中使用這種方式饰躲,而老的接口不會有影響牙咏。我們將在 Retrofit 的接口定義中添加一個請求頭,然后在攔截器中獲取并移除掉嘹裂。
open class ResponseStatusInterceptor : Interceptor {
companion object {
const val CHECK_HTTP_RESPONSE_CODE = "Check-Http-Response-Code: true"
private const val CHECK_HEADER = "Check-Http-Response-Code"
private val gson = Gson()
private val UTF8 = StandardCharsets.UTF_8
}
@Throws(IOException::class)
override fun intercept(chain: Interceptor.Chain): Response {
var request = chain.request()
val value = request.headers()[CHECK_HEADER]
request = request.newBuilder().removeHeader(CHECK_HEADER)
.build() // 移除請求頭中的無用標識妄壶,不會發(fā)送到服務器。
val response = chain.proceed(request)
if ("true" == value) {
if (!response.isSuccessful) {
throw ApiException(response.code(), response.message())
}
// Make sure the content is json.
val contentType = response.headers()["Content-Type"]
if (contentType != null && response.body() != null && contentType.contains("application/json")) {
val status =
gson.fromJson<BodyContent<Void>>(
bodyContent(response.body()),
object : TypeToken<BodyContent<Void?>?>() {}.type
)
if (status != null && status.code != 1) {
throw ApiException(status.code, (if (status.msg == null) "" else status.msg!!))
}
}
}
return response
}
@Throws(IOException::class)
protected fun bodyContent(responseBody: ResponseBody?): String? {
if (responseBody == null) return null
val contentLength = responseBody.contentLength()
val source = responseBody.source()
source.request(contentLength) // Buffer the entire body.
val buffer = source.buffer
return if (contentLength != 0L) {
buffer.clone().readString(UTF8)
} else null
}
}
我們將在新接口中添加請求頭寄狼,以表示攔截響應碼丁寄。
@Headers(ResponseStatusInterceptor.CHECK_HTTP_RESPONSE_CODE) // 請求頭中添加標識。
@GET("not_exit_file")
fun testRetrofitErrorCode(): Call<String>
同時我們需要修改自定義響應碼的映射類 BodyContent
泊愧,將默認值改為正常請求的伊磺,具體是什么依賴于你項目,這里假設(shè)是 200删咱。
class BodyContent<D> {
companion object {
const val SUCCESS = 200
}
var code = SUCCESS
...
}
這樣如果我們在 body 體中定義了響應碼的話屑埋,就可在因接口中不再使用自定義的,而使用請求頭痰滋。而此時摘能,默認值是 200续崖,而仍然會走正常的流程。唯一需要注意的是团搞,該字段不會被用于其他含義的數(shù)據(jù)严望。
同時,我們將使用 @Deprecated
將 BodyContent
標記為廢棄逻恐,提醒其他人員轉(zhuǎn)移到新的錯誤處理體系上來像吻。
最后我們形成如下的流程
OkHttp
|
|
攔截器攔截錯誤 ------------┒
| |
| |
| |
(onResponse) onFailure 超時,鏈接失敗复隆,拋出 IOException 等異常萧豆。200~300錯誤.
| |
↓ |
Retrofit |
| |
| |
(Response) -------- onResponse 可能會增加一些 json 解析錯誤。
| |
↓ |
RxJava -------- onError 可能會增加一些 UI 渲染昏名,空指針錯誤
| |
| |
| |
(onNext/onSuccess) |
| |
| |
渲染 UI 錯誤處理邏輯涮雷。
你可以使用任何上層的技術(shù)組合,他們的流程都是一樣的轻局。只需要在 onFailure 或 onError 中處理錯誤即可洪鸭。至于是哪一個,取決于你使用的是 Retroit 還是 RxJava.
盡管異常處理的流程分支統(tǒng)一了仑扑,但是有一種特殊異忱谰簦基本是統(tǒng)一的,那就是 401 授權(quán)失敗異常镇饮。這種異常會在各個頁面都可能發(fā)生蜓竹,然而處理流程和結(jié)果卻是一樣的: 顯示提醒 ——> 跳轉(zhuǎn)到登錄頁面 ——> 返回刷新數(shù)據(jù)。針對這樣的異常储藐,應該是統(tǒng)一處理俱济,而不是每個頁面單獨寫自己的邏輯。我見過不同的方案來實現(xiàn)這種跳轉(zhuǎn)钙勃,有的直接在攔截器中就跳轉(zhuǎn)了蛛碌;有的使用 RxJava 的 Observer 作為基類在 onError 中處理;有的在 RxJava 的 retry 操作符中進行重連辖源。這里將比較它們實現(xiàn)中的問題蔚携,同時給出一個最佳的組合。
登錄授權(quán)
如上的錯誤處理有一個問題克饶,那就是登錄授權(quán)酝蜒。需要登錄驗證通過才能訪問的接口會在驗證失敗時在響應頭中返回 401 錯誤碼。此時我們分兩種情況:
token 過期可以在后臺通過接口自動更新 token矾湃。
沒有登錄或者登錄失敗等需要用戶操作才能登陸的情況亡脑。
自動更新 token 的情況
對于第一中,OkHttp 已經(jīng)提供了 authenticator 攔截器,唯一需要注意的是远豺,由于網(wǎng)絡(luò)請求可以并發(fā)發(fā)出奈偏,因此你需要避免發(fā)出多個更新 token 請求。這里使用加鎖和本地緩存來避免重復請求躯护。
OkHttpClient.Builder()
.authenticator(object :Authenticator {
override fun authenticate(route: Route?, response: Response): Request? {
synchronized(Client::class.java) {
val token = if (getCachedAuth() == response.request().header("Authorization" )) {
val request = Request.Builder().url("authenticator url").build()
val response = okHttpClient.newCall(request).execute()
val token = "從請求結(jié)果 response 中拿到" // response.body(). 獲取token.
updateCachedToken(token) // 更新本地緩存的 token
token
} else {
getCachedAuth()
}
return response.request().newBuilder()
.addHeader("Authorization","Bearer {$token}")
.build()
}
}
})
這里沒有使用雙重檢測鎖惊来,主要考慮刷新 token 并不是一個頻繁的操作,代碼簡潔也很重要棺滞。根據(jù)你自己的考慮裁蚁,你可以添加雙重判斷。
獲取和更新本地緩存的方法需要你根據(jù)你的方案來自己實現(xiàn)操作:
fun getCachedAuth(): String {
return "Bearer " + "本地存貯的值"
}
fun updateCachedToken(token: String) {
// 更新本地緩存的 token
}
需要通知用戶手動登錄的情況
第二種情況顯得不那么好處理继准,錯誤的處理情況千差萬別枉证,處理的方式也不一樣。因為這里涉及到 UI 操作移必,一個好的做法是室谚,在 BaseActivity 中添加一個函數(shù),用于處理全部都有的登錄部分崔泵。
// BaseActivity 中添加
protected fun handleException(exception: Throwable): Boolean {
reportException(exception)
return if (exception is ApiException && exception.unauthorized()) {
// 顯示登陸提醒的彈窗秒赤。詢問是否登陸,跳轉(zhuǎn)到登陸頁面憎瘸。
true
} else {
false
}
}
fun reportException(exception: Throwable) {
// 上報錯誤數(shù)據(jù)
}
在各個 Activity 中對接網(wǎng)絡(luò)請求和錯誤處理入篮。
val disposable = Client.getServerApi()
.getPost(12)
.subscribe({
// update ui
}, { exception ->
// 對 UI 顯示是否提醒和更改狀態(tài),例如隱藏加在動畫幌甘。
if (攔截) {
// 如果需要攔截潮售,就不用調(diào)用 handleException。否則就交給默認的錯誤處理锅风。
} else if (!handleException(exception)) {
// 默認處理沒有相應酥诽,就自己處理。
}
})
中間的交付過程可以使用 LiveData遏弱、RxJava盆均、協(xié)稱的任意組合塞弊。這樣對中間交付過程沒有任何要求漱逸,處理起來非常靈活。
- token 過期游沿,
線程切換
由于安卓的特性饰抒,網(wǎng)絡(luò)請求必須在后臺線程中執(zhí)行。因此切換線程就成了一個必選項诀黍。至于在何處切換線程有了五花八門的寫法袋坑。我建議始終把 subscribeOn
和 observerOn
寫在緊挨著 subsctibe
的前面。例如:
Single.just(1)
.doOnSubscribe {
printThread("doOnSubscribe")
}
.map {
printThread("Map")
it
}
...
.subscribeOn(Schedulers.io())
.observeOn(Schedulers.newThread())
.subscribe(...)
fun printThread(tag: String) {
println(tag + "........"+ Thread.currentThread().name)
}
由于網(wǎng)絡(luò)請求是必須的操作眯勾,.subscribeOn(Schedulers.io())
就成了必須要做的工作枣宫。而 RxJava-adapter 已經(jīng)為我們提供了這個操作婆誓。在添加 Retrofit 的 CallAdapterFActory 時,我們可以選擇添加線程調(diào)度器也颤。
Retrofit.Builder()
.addCallAdapterFactory(RxJava3CallAdapterFactory.createWithScheduler(Schedulers.io())) // 指定 io 線程訪問網(wǎng)絡(luò)洋幻。
既然切換到后臺線程有默認的方法可供使用,那切換到前臺呢翅娶?然而并沒有文留。 我們來討論為什么不應該這樣做!
這是因為 observeOn 影響它后面流的操作符的線程竭沫。一旦我們把 observeOn
寫在靠前的位置燥翅,后面的變換操作符將在前臺線程中。如果變換操作中有一些耗時的操作蜕提,我們將不得不調(diào)用 observeOn(..)
切換到其他線程森书,然后再次調(diào)用一次,這樣一次流中的線程切換頻繁顯得非常繁瑣谎势。另一方面拄氯,切換到前臺線程的操作不是必須的,例如一個請求是作為另一個請求的輸入它浅,或者我們使用 LiveData 的 postValue 將數(shù)據(jù)更新到 UI 線程時译柏,切換到前臺線程顯得多余。
那如果是這樣姐霍,subscribeOn
為什么提供了默認的接口呢鄙麦?它也智能影響它前面的 doOnSubscribe
, 我們再最上游的流設(shè)置了 subscribeOn
并不能使后面的操作也在后臺線程中。 我想著主要是因為兩方面
網(wǎng)絡(luò)在后臺線程請求是必須的镊折,而切換到前臺更新 UI 則不一定會發(fā)生胯府。
doOnSubscribe
沒有變換操作頻繁。幾乎每個接口請求后都會有一些額外操作恨胚,而請求之前的操作卻不是那么常見骂因,即使有也很少有耗時的操作,并且一個項目中也不會有多少個調(diào)用赃泡。