android开发工作随笔我的安卓学习记录Android开发经验谈

Rxjava+Retrofit+Okhttp 之为所欲为

2017-11-26  本文已影响190人  橘子周二

在写这个框架的时候,技术Leader跟我强调过一句话:’封装是为了更方便使用,但不要为了封装而封装‘
也正是因为这句话,我在写完beta版本时,又重新审视了一遍,果然发现不少可以精进的地方,最终才有了现在这样清晰结构,感受颇深。

本篇是在几个项目都使用Rxjava 、Retrofit+OkHttp后总结的知识与经验的基础上,综合考虑过项目开发需求、开发使用习惯后而产生的封装思路。

先放一张框架结构图

tbretrofitV2.0.png

这是一次常规请求从发起到结束的整个过程,绿框中的内容就是需要封装来做的事情。
第一次画这个,很蹩脚,东西不多还算清晰吧 哈哈

如果你更关心OkHttp 如何使用Interceptor 来解决 session ,token 过期后 同步重新获取再继续原本的请求,可以参照我的实例 SignInvalidInterceptor.java

封装后解决的核心问题:

1.简化接口的冗余代码及线程调度代码
2.合理的释放线程和Context解绑释放
3.为加解密提供合理易拆分的入口
4.提供可选择的多种缓存模式
5.更清晰的拦截网络异常,优雅的下发异常信息

使用示例
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;
                    }
                });

看起来好像其实没什么区别,几乎和原来的Retrofit 接口差不多,并不影响使用习惯和代码风格

封装一个库不仅仅是为了减少重复代码,也应该解决一些使用中的问题
下面记录此次加入Rxjava到其中后产生的新思路

一、为什么这样封装以及如何用封装解决

1.用过Retrofit +Rxjava 的都知道,需要为不同接口写不同的Service,使用GsonConvertfactory可以在每个接口返回值直接返回ResultObject。 但这样面临的问题是什么呢?

1).我们需要为每一个接口都在Service 层多加一个接口,想我之前直接使用Service。项目比较大单个Service中至少写了60多个方法,它仅仅是一个接口,加上必要注释大概有300多行。这其中包含大量的重复代码,比如一样的返回值,一样的ip引用,仅仅是对应后台的接口名不同,这显然需要处理掉
解决思路:Retrofit 提供的接口非常丰富,但常用的无非就是Get 、Post、Formdata、和支持文件的Multipart类型。
下面是准备的常用接口:

为什么是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支持的接口类型不熟悉的可以自信查阅后再阅读本篇。
为了保证良好的拓展,专门准备了支持RequestBody作为最高级支持的参数类型

2).使用RxJava 是为了方便管理线程和数据转换,为此需要为每一个接口都写一遍线程调度,处理异常和管理内存释放。但其实通常在Android 中接口请求都是异步的,并不是每一个接口都需要去更换线程调用方式和数据转换。
解决思路:既然线程调度可以保持不变,那就用一个模式搞定他,有了问题1中的接口,就需要一个类似于港口的类来负责转接(可以理解为代理,但他并不是代理模式)。
源码实例:

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())//被观察者创建线程 事件产生的线程 变量X
                .unsubscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())//观察者接受回调线程 事件接受线程 应变量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 是面向外部的接口,而在实现类RxHttpApiImpl则负责实现统一的线程调度和解绑
这里解绑包含Subscriber解绑和Context与网络框架解绑。
与Context解绑就是上面doOnUnsubscr中用到的HttpTaskmanagement处理的。
代码如下:

    代码省略······
    @Override
    public void unSubscribe(Object tag) {
        if (null == tasks || tasks.isEmpty()) return;
        RxHttpLog.printI("RxHttpTaskmanagement", "执行取消订阅 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();
        //拦截无网络可用
        if (!NetworkStatusUtils.networkIsConnected(responseListener.getContext())) {
            responseListener.onFailure(HttpCode.CODE_NO_INTERNET, "网络不可用");
            unsubscribe();
            onCompleted();
        }
    }

3).因为使用了GsonConvertFactory,所以入参和返回都是JSON格式的。而且必须是Object子类去做,那如果要加密怎么办?显然这样是无法满足的,只想让他作为网络请求的桥梁而非限制如何使用
这里有两种思路去插入加密解密的地方:
a.一个是直接为OkHttp添加Intercepter
这种方法比较死板,容易出错。因为拦截器我们拦截到的是Retrofit中编码后的数据,这样并不利于我们直接操作。通常一个项目中,我们使用的client为了保持一致都是单例的,添加了加密的拦截器有加密的无加密的就无法拆分。所以我并没有实践这种思路
b.重写Convertfactory
在传给OkHttp 之前就进行加密操作,在OkHttp正常返回后进行解密。
加解密我并没有世界操作,但我大概肯定下面的方法是可行的

加密处
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());
    }

可以看到这里重新进行了编码,那我们只用对buffer进行加密即可

解密处
StringResponseBodyConverter:

 @Override
    public String convert (ResponseBody value) throws IOException {
        //这里不用拦截 contentLength ==-1?: 源码中转string 时已经判断了
        RxHttpLog.printI("StringResponseBodyConverter","ResponseBody:"+value.contentLength());
        try {
            return value.string();
        } finally {
            value.close();
        }
    }

4.)关于缓存的实现思路
a.为Okhttpclient 添加Intercepter
死板 不利于实现有缓 无缓两种方案
b.利用Retroift支持@Header注解在接口出作为参数添加Cache-Control
实现代码:

  @GET
    Observable<Response<String>> get(@Header("Cache-Control") String cacheControl, @Url String url);
public class CacheConfig {

    /**
     * CacheControl.Builder :
     - noCache();//不使用缓存,用网络请求
     - noStore();//不使用缓存,也不存储缓存
     - onlyIfCached();//只使用缓存
     - noTransform();//禁止转码
     - maxAge(10, TimeUnit.MILLISECONDS);//超过 maxAge 将走网络。
     - maxStale(10, TimeUnit.SECONDS);//超过 maxStale 缓存将不可用
       但依然由 maxAge决定是否走网络,所以 精良让maxAge<maxStale 避免返回 空结果

     - minFresh(10, TimeUnit.SECONDS);//超时时间为当前时间加上10秒钟。

     */


    /**
     * 不考虑缓存,直接走网络,但会存储响应到缓存区
     * @return
     */
    public static String forceNetWork () {

        return CacheControl.FORCE_NETWORK.toString();
    }

    /**
     * 不使用缓存,并且不会存储响应到缓存区
     * @return
     */
    public static String forceNetWorkAndNoStore () {
        return new CacheControl.Builder()
                .noCache()
                .noStore()
                .build().toString();
    }

    /**
     * 直接读取缓存区
     * 如果已有缓存,则将缓存可用时间设置为 Integer.MAX_VALUE
     * 如果缓存区无数据则返回null  code: 504-unsatisfiable request
     * @return
     */
    public static String forceCache () {

        return CacheControl.FORCE_CACHE.toString();
    }


    /**
     * 完全交给Cache-Control:value
     * 由value来决定 重新请求还是使用缓存
     * 另外:Cache-Control 区分 client  与 server,
     * 也就是服务器对应的 expires 有效时间   client max-age
     * 并且由 client 为准 server 为辅
     *
     * max-age client自主决定缓存有效期 超过有效期才请求网络
     * 但是如果服务器依然返回304 则将继续使用缓存
     *
     * @return
     */
    public static String normal () {
        return new CacheControl.Builder()
                .maxAge(30, TimeUnit.SECONDS)
                .build().toString();
    }

    /**
     * 针对近似永久缓存数据,使用如下策略
     * 接口配置 只使用缓存,加入无缓存 504 拦截器
     * 为此次任务重新配置cache-control 在首次读取时从网络下载一次
     * @return
     */
    public static String forever () {
        return new CacheControl.Builder()
                .maxAge(1, TimeUnit.SECONDS)
                .maxStale(Integer.MAX_VALUE,TimeUnit.SECONDS)
                .build().toString();
    }
}

5).关于OkhttpClient网络异常(不包含40x 50x 这类正常状态码,这些交给调用层按需求处理),通常我们只判断了手机的网络开关与模式,不易与发现其他网络异常。Retroift +RxJava轻松帮我们都拦截到,我们只需单独判断出来即可。
经过细致测试,按照又先到后,又子类到父类的策略拦截异常,保证能抓取到准确的异常信息
代码如下:

 /**
     * 协议层 读存数据发生错误才会走这里
     *
     * @param e
     */
    @Override
    public void onError (Throwable e) {
        //此处可拦截到的异常均发生在请求发起以后
        RxHttpLog.printI(TAG, "onError Throwable subClass :" + e.getClass() + " errorMsg:"+e.getMessage());
        if(e instanceof NullPointerException){
              /*
               * 响应返回或者缓存返回为null
               *此处的空指针来自于StringConverterFactory中转换响应结果时发生,会先调用onResponse 才会掉onError
               * 所以这里不下发给onFailure
               */
        }else if(e instanceof UnknownHostException){
            responseListener.onFailure(HttpCode.CODE_UNKNOW_HOST, "访问的目标主机不存在");

        }else if (e instanceof HttpException) {
            //SSL 验证成功 有正常的 响应返回,服务器无包装时 协议码为标准 401 404 等。。。
            HttpException httpException = (HttpException) e;
            responseListener.onFailure(httpException.code(), httpException.message());

        } else if (e instanceof SSLException) {
            //无法链接目标主机-本地网络无法访问外部服务器地址-服务器地址无法连接
            responseListener.onFailure(HttpCode.CODE_INTENET_IS_ABNORMAL, "无法与目标主机建立链接");

        } else if (e instanceof ConnectException
                || e instanceof SocketTimeoutException
                || e instanceof TimeoutException) {
            responseListener.onFailure(HttpCode.CODE_TIME_OUT, "链接超时");

        } else  if (e instanceof IOException) {
            //有网但请求失败包含:网络未授权访问外部地址,只读缓存时缓存区无缓存,等。。。
            responseListener.onFailure(HttpCode.CODE_RESPONSE_ERROR, "有网但请求失败包含:网络未授权访问外部地址,只读缓存时缓存区无缓存,等。。。");

        } else{

            responseListener.onFailure(HttpCode.CODE_UNKNOW, "未知网络错误");
        }
        responseListener.onFinish();
    }

END


框架已发布JitPack:
github开源地址:TBRetroift
欢迎Star me fork me
如果本篇内容有错或者在使用后有问题欢迎下方留言或者GitHub上提issue。

上一篇下一篇

猜你喜欢

热点阅读