在寫這個框架的時候,技術(shù)Leader跟我強(qiáng)調(diào)過一句話:’封裝是為了更方便使用,但不要為了封裝而封裝‘
也正是因為這句話,我在寫完beta版本時,又重新審視了一遍,果然發(fā)現(xiàn)不少可以精進(jìn)的地方,最終才有了現(xiàn)在這樣清晰結(jié)構(gòu),感受頗深。
本篇是在幾個項目都使用Rxjava 、Retrofit+OkHttp后總結(jié)的知識與經(jīng)驗的基礎(chǔ)上,綜合考慮過項目開發(fā)需求、開發(fā)使用習(xí)慣后而產(chǎn)生的封裝思路。
先放一張框架結(jié)構(gòu)圖

這是一次常規(guī)請求從發(fā)起到結(jié)束的整個過程,綠框中的內(nèi)容就是需要封裝來做的事情。
第一次畫這個,很蹩腳,東西不多還算清晰吧 哈哈
如果你更關(guān)心OkHttp 如何使用Interceptor 來解決 session ,token 過期后 同步重新獲取再繼續(xù)原本的請求,可以參照我的實(shí)例 SignInvalidInterceptor.java
封裝后解決的核心問題:
1.簡化接口的冗余代碼及線程調(diào)度代碼
2.合理的釋放線程和Context解綁釋放
3.為加解密提供合理易拆分的入口
4.提供可選擇的多種緩存模式
5.更清晰的攔截網(wǎng)絡(luò)異常,優(yōu)雅的下發(fā)異常信息
使用示例
GET:
HttpUtils.getHttpApi() .get(GITHUB_RESTFUL, new HttpCallBack<GithubEntity>(RxHttpTest.this) {
@Override
public void onSuccess (GithubEntity githubEntity) {
printLog("githubEntity:" + githubEntity.getName());
}
@Override
public void onFailure (int errorCode, String message) {
printLog("onFailure errorCod:" + errorCode + " errorMsg:" + message);
}
@Override
public CacheModel cacheModel () {
return CacheModel.NORMAL;
}
});
}
POST使用:
HttpUtils.getHttpApi() .postJson(API.loginUrl, PostDataUtils.getSiginParameter(), new HttpCallBack<SiginResponseBean>(RxHttpTest.this) {
@Override
public void onSuccess (SiginResponseBean s) {
}
@Override
public CacheModel cacheModel () {
return CacheModel.NORMAL;
}
});
看起來好像其實(shí)沒什么區(qū)別,幾乎和原來的Retrofit 接口差不多,并不影響使用習(xí)慣和代碼風(fēng)格
封裝一個庫不僅僅是為了減少重復(fù)代碼,也應(yīng)該解決一些使用中的問題
下面記錄此次加入Rxjava到其中后產(chǎn)生的新思路
一、為什么這樣封裝以及如何用封裝解決
1.用過Retrofit +Rxjava 的都知道,需要為不同接口寫不同的Service,使用GsonConvertfactory可以在每個接口返回值直接返回ResultObject。 但這樣面臨的問題是什么呢?
1).我們需要為每一個接口都在Service 層多加一個接口,想我之前直接使用Service。項目比較大單個Service中至少寫了60多個方法,它僅僅是一個接口,加上必要注釋大概有300多行。這其中包含大量的重復(fù)代碼,比如一樣的返回值,一樣的ip引用,僅僅是對應(yīng)后臺的接口名不同,這顯然需要處理掉
解決思路:Retrofit 提供的接口非常豐富,但常用的無非就是Get 、Post、Formdata、和支持文件的Multipart類型。
下面是準(zhǔn)備的常用接口:
為什么是Response<String>而不是Object下文會有解釋
@GET
Observable<Response<String>> get(@Header("Cache-Control") String cacheControl, @Url String url);
@GET
Observable<Response<String>> get(@Header("Cache-Control") String cacheControl,@Url String url, @QueryMap Map<String, Object> map);
@POST
@FormUrlEncoded
Observable<Response<String>> postForm(@Header("Cache-Control") String cacheControl,@Url String url, @FieldMap Map<String, Object> map);
@POST
@Headers("Content-Type:application/json;")
Observable<Response<String>> postJson(@Header("Cache-Control") String cacheControl,@Url String url, @Body Object body);
@POST
Observable<Response<String>> postIndependent(@Header("Cache-Control") String cacheControl,@Url String url, @Body RequestBody body);
@POST
Observable<Response<String>> postFormDataFiles(@Header("Cache-Control") String cacheControl,@Url String url, @Body MultipartBody body);
這里為了方便閱讀,去掉了注釋,如果有對Retrofit支持的接口類型不熟悉的可以自信查閱后再閱讀本篇。
為了保證良好的拓展,專門準(zhǔn)備了支持RequestBody作為最高級支持的參數(shù)類型
2).使用RxJava 是為了方便管理線程和數(shù)據(jù)轉(zhuǎn)換,為此需要為每一個接口都寫一遍線程調(diào)度,處理異常和管理內(nèi)存釋放。但其實(shí)通常在Android 中接口請求都是異步的,并不是每一個接口都需要去更換線程調(diào)用方式和數(shù)據(jù)轉(zhuǎn)換。
解決思路:既然線程調(diào)度可以保持不變,那就用一個模式搞定他,有了問題1中的接口,就需要一個類似于港口的類來負(fù)責(zé)轉(zhuǎn)接(可以理解為代理,但他并不是代理模式)。
源碼實(shí)例:
public interface HttpApi {
void get(String url,HttpResponseListener responseListener);
void get(String url, String[] values,HttpResponseListener responseListener);
void get(String url, Map<String, Object> map,HttpResponseListener responseListener);
void postJson(String url, JsonBody json,HttpResponseListener responseListener);
void postRequestBody(String url, RequestBody body,HttpResponseListener responseListener);
void postFormData(String url, Map<String, Object> map ,HttpResponseListener responseListener);
void postFormDataFiles(String url, Map<String, Object> map, List<File> files, MediaType contentType,HttpResponseListener responseListener);
}
final public class RxHttpApiImpl implements HttpApi {
代碼省略······
private void subscribe (final Observable<Response<String>> observable, final HttpResponseListener responseListener) {
if (null == observable) return;
final Subscriber<Response<String>> subscriber = new ResponseHandler(responseListener);
observable.subscribeOn(Schedulers.io())//被觀察者創(chuàng)建線程 事件產(chǎn)生的線程 變量X
.unsubscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())//觀察者接受回調(diào)線程 事件接受線程 應(yīng)變量Y
.retryWhen(new RetryWhenTimeout())
.doOnSubscribe(new Action0() {
@Override
public void call () {
httpTaskManagement.addSubscription(responseListener.getContext(), subscriber);
}
})
.doOnUnsubscribe(new Action0() {
@Override
public void call () {
httpTaskManagement.remove(responseListener.getContext());
}
})
.subscribe(subscriber);
}
@Override
public void get (String url, HttpResponseListener responseListener) {
subscribe(apiService.get(checkCacheModel(responseListener.cacheModel()),url), responseListener);
}
代碼省略······
}
HttpApi 是面向外部的接口,而在實(shí)現(xiàn)類RxHttpApiImpl則負(fù)責(zé)實(shí)現(xiàn)統(tǒng)一的線程調(diào)度和解綁
這里解綁包含Subscriber解綁和Context與網(wǎng)絡(luò)框架解綁。
與Context解綁就是上面doOnUnsubscr中用到的HttpTaskmanagement處理的。
代碼如下:
代碼省略······
@Override
public void unSubscribe(Object tag) {
if (null == tasks || tasks.isEmpty()) return;
RxHttpLog.printI("RxHttpTaskmanagement", "執(zhí)行取消訂閱 key:" + tag.hashCode());
Subscription subscription = tasks.get(tag);
if (null != subscription && !subscription.isUnsubscribed()) {
subscription.unsubscribe();
RxHttpLog.printI("RxHttpTaskmanagement", "取消了訂閱 key:" + tag.hashCode());
tasks.remove(tag);
}
}
代碼省略······
Subscriber解綁:
@Override
public void onStart () {
super.onStart();
responseListener.onStart();
//攔截?zé)o網(wǎng)絡(luò)可用
if (!NetworkStatusUtils.networkIsConnected(responseListener.getContext())) {
responseListener.onFailure(HttpCode.CODE_NO_INTERNET, "網(wǎng)絡(luò)不可用");
unsubscribe();
onCompleted();
}
}
3).因為使用了GsonConvertFactory,所以入?yún)⒑头祷囟际荍SON格式的。而且必須是Object子類去做,那如果要加密怎么辦?顯然這樣是無法滿足的,只想讓他作為網(wǎng)絡(luò)請求的橋梁而非限制如何使用
這里有兩種思路去插入加密解密的地方:
a.一個是直接為OkHttp添加Intercepter
這種方法比較死板,容易出錯。因為攔截器我們攔截到的是Retrofit中編碼后的數(shù)據(jù),這樣并不利于我們直接操作。通常一個項目中,我們使用的client為了保持一致都是單例的,添加了加密的攔截器有加密的無加密的就無法拆分。所以我并沒有實(shí)踐這種思路
b.重寫Convertfactory
在傳給OkHttp 之前就進(jìn)行加密操作,在OkHttp正常返回后進(jìn)行解密。
加解密我并沒有世界操作,但我大概肯定下面的方法是可行的
加密處
GsonRequestBodyConverter源碼 :
@Override
public RequestBody convert(T value) throws IOException {
Buffer buffer = new Buffer();
Writer writer = new OutputStreamWriter(buffer.outputStream(), UTF_8);
JsonWriter jsonWriter = gson.newJsonWriter(writer);
adapter.write(jsonWriter, value);
jsonWriter.close();
return RequestBody.create(MEDIA_TYPE, buffer.readByteString());
}
可以看到這里重新進(jìn)行了編碼,那我們只用對buffer進(jìn)行加密即可
解密處
StringResponseBodyConverter:
@Override
public String convert (ResponseBody value) throws IOException {
//這里不用攔截 contentLength ==-1?: 源碼中轉(zhuǎn)string 時已經(jīng)判斷了
RxHttpLog.printI("StringResponseBodyConverter","ResponseBody:"+value.contentLength());
try {
return value.string();
} finally {
value.close();
}
}
4.)關(guān)于緩存的實(shí)現(xiàn)思路
a.為Okhttpclient 添加Intercepter
死板 不利于實(shí)現(xiàn)有緩 無緩兩種方案
b.利用Retroift支持@Header注解在接口出作為參數(shù)添加Cache-Control
實(shí)現(xiàn)代碼:
@GET
Observable<Response<String>> get(@Header("Cache-Control") String cacheControl, @Url String url);
public class CacheConfig {
/**
* CacheControl.Builder :
- noCache();//不使用緩存,用網(wǎng)絡(luò)請求
- noStore();//不使用緩存,也不存儲緩存
- onlyIfCached();//只使用緩存
- noTransform();//禁止轉(zhuǎn)碼
- maxAge(10, TimeUnit.MILLISECONDS);//超過 maxAge 將走網(wǎng)絡(luò)。
- maxStale(10, TimeUnit.SECONDS);//超過 maxStale 緩存將不可用
但依然由 maxAge決定是否走網(wǎng)絡(luò),所以 精良讓maxAge<maxStale 避免返回 空結(jié)果
- minFresh(10, TimeUnit.SECONDS);//超時時間為當(dāng)前時間加上10秒鐘。
*/
/**
* 不考慮緩存,直接走網(wǎng)絡(luò),但會存儲響應(yīng)到緩存區(qū)
* @return
*/
public static String forceNetWork () {
return CacheControl.FORCE_NETWORK.toString();
}
/**
* 不使用緩存,并且不會存儲響應(yīng)到緩存區(qū)
* @return
*/
public static String forceNetWorkAndNoStore () {
return new CacheControl.Builder()
.noCache()
.noStore()
.build().toString();
}
/**
* 直接讀取緩存區(qū)
* 如果已有緩存,則將緩存可用時間設(shè)置為 Integer.MAX_VALUE
* 如果緩存區(qū)無數(shù)據(jù)則返回null code: 504-unsatisfiable request
* @return
*/
public static String forceCache () {
return CacheControl.FORCE_CACHE.toString();
}
/**
* 完全交給Cache-Control:value
* 由value來決定 重新請求還是使用緩存
* 另外:Cache-Control 區(qū)分 client 與 server,
* 也就是服務(wù)器對應(yīng)的 expires 有效時間 client max-age
* 并且由 client 為準(zhǔn) server 為輔
*
* max-age client自主決定緩存有效期 超過有效期才請求網(wǎng)絡(luò)
* 但是如果服務(wù)器依然返回304 則將繼續(xù)使用緩存
*
* @return
*/
public static String normal () {
return new CacheControl.Builder()
.maxAge(30, TimeUnit.SECONDS)
.build().toString();
}
/**
* 針對近似永久緩存數(shù)據(jù),使用如下策略
* 接口配置 只使用緩存,加入無緩存 504 攔截器
* 為此次任務(wù)重新配置cache-control 在首次讀取時從網(wǎng)絡(luò)下載一次
* @return
*/
public static String forever () {
return new CacheControl.Builder()
.maxAge(1, TimeUnit.SECONDS)
.maxStale(Integer.MAX_VALUE,TimeUnit.SECONDS)
.build().toString();
}
}
5).關(guān)于OkhttpClient網(wǎng)絡(luò)異常(不包含40x 50x 這類正常狀態(tài)碼,這些交給調(diào)用層按需求處理),通常我們只判斷了手機(jī)的網(wǎng)絡(luò)開關(guān)與模式,不易與發(fā)現(xiàn)其他網(wǎng)絡(luò)異常。Retroift +RxJava輕松幫我們都攔截到,我們只需單獨(dú)判斷出來即可。
經(jīng)過細(xì)致測試,按照由先到后,又子類到父類的策略攔截異常,保證能抓取到準(zhǔn)確的異常信息
代碼如下:
/**
* 協(xié)議層 讀存數(shù)據(jù)發(fā)生錯誤才會走這里
*
* @param e
*/
@Override
public void onError (Throwable e) {
//此處可攔截到的異常均發(fā)生在請求發(fā)起以后
RxHttpLog.printI(TAG, "onError Throwable subClass :" + e.getClass() + " errorMsg:"+e.getMessage());
if(e instanceof NullPointerException){
/*
* 響應(yīng)返回或者緩存返回為null
*此處的空指針來自于StringConverterFactory中轉(zhuǎn)換響應(yīng)結(jié)果時發(fā)生,會先調(diào)用onResponse 才會掉onError
* 所以這里不下發(fā)給onFailure
*/
}else if(e instanceof UnknownHostException){
responseListener.onFailure(HttpCode.CODE_UNKNOW_HOST, "訪問的目標(biāo)主機(jī)不存在");
}else if (e instanceof HttpException) {
//SSL 驗證成功 有正常的 響應(yīng)返回,服務(wù)器無包裝時 協(xié)議碼為標(biāo)準(zhǔn) 401 404 等。。。
HttpException httpException = (HttpException) e;
responseListener.onFailure(httpException.code(), httpException.message());
} else if (e instanceof SSLException) {
//無法鏈接目標(biāo)主機(jī)-本地網(wǎng)絡(luò)無法訪問外部服務(wù)器地址-服務(wù)器地址無法連接
responseListener.onFailure(HttpCode.CODE_INTENET_IS_ABNORMAL, "無法與目標(biāo)主機(jī)建立鏈接");
} else if (e instanceof ConnectException
|| e instanceof SocketTimeoutException
|| e instanceof TimeoutException) {
responseListener.onFailure(HttpCode.CODE_TIME_OUT, "鏈接超時");
} else if (e instanceof IOException) {
//有網(wǎng)但請求失敗包含:網(wǎng)絡(luò)未授權(quán)訪問外部地址,只讀緩存時緩存區(qū)無緩存,等。。。
responseListener.onFailure(HttpCode.CODE_RESPONSE_ERROR, "有網(wǎng)但請求失敗包含:網(wǎng)絡(luò)未授權(quán)訪問外部地址,只讀緩存時緩存區(qū)無緩存,等。。。");
} else{
responseListener.onFailure(HttpCode.CODE_UNKNOW, "未知網(wǎng)絡(luò)錯誤");
}
responseListener.onFinish();
}
END
框架已發(fā)布JitPack:
github開源地址:TBRetroift
歡迎Star me fork me
如果本篇內(nèi)容有錯或者在使用后有問題歡迎下方留言或者GitHub上提issue。