RxJava + Retrofit2 + OkHttp3 封装及踩坑(续)

前一篇文章(也是我在简书上的第一篇技术文章.)讲了Android三剑客的基础用法和简单封装,有一些封装只是一笔带过,还有些用法被遗漏没讲到的,所以在这篇里统一做下查漏补缺。

0x00 先做一下纠正:

https和失败重连,OkHttp默认是支持的,并不用手动去设置(在OkHttpClient.Builder中已默认设置),所以OkHttpClient.Builder的初始化可以简化为:

// 创建OkHttpClient
OkHttpClient.Builder builder = new OkHttpClient.Builder()
        // 超时设置
        .connectTimeout(DEFAULT_CONNECT_TIMEOUT, TimeUnit.SECONDS)
        .readTimeout(DEFAULT_READ_TIMEOUT, TimeUnit.SECONDS)
        .writeTimeout(DEFAULT_WRITE_TIMEOUT, TimeUnit.SECONDS)
        // cookie管理
        .cookieJar(new PersistentCookieJar(new SetCookieCache(), new SharedPrefsCookiePersistor(App.getInstance())));

0x01 Cookie持久化管理

这部分主要参考了这篇文章

  • 不带持久化
builder.cookieJar(new CookieJar() {
        private final HashMap<HttpUrl, List<Cookie>> cookieStore = new HashMap<>();

        @Override
        public void saveFromResponse(HttpUrl url, List<Cookie> cookies) {
            cookieStore.put(url, cookies);
        }

        @Override
        public List<Cookie> loadForRequest(HttpUrl url) {
            List<Cookie> cookies = cookieStore.get(url);
            return cookies != null ? cookies : new ArrayList<Cookie>();
        }
    });

这种简单的实现,每次重启App都会需要重新登录,不可取。

  • 带持久化
CookieHandler cookieHandler = new CookieManager(
        new PersistentCookieStore(context), CookiePolicy.ACCEPT_ALL);
builder.cookieJar(new JavaNetCookieJar(cookieHandler));

这里出现了两个类:JavaNetCookieJarPersistentCookieStore

  • JavaNetCookieJar就是对CookieJar的封装实现,里面实现了对Cookie持久化存储和获取的调用逻辑,OkHttp已经帮我们实现了这个类,需要引入下面这个包:
compile 'com.squareup.okhttp3:okhttp-urlconnection:3.5.0'
  • PersistentCookieStore是具体实现Cookie持久化的类,使用的是SharedPreferences,具体代码实现可参考这篇
    当然,如果你想通过数据库实现持久化,也可以自己封装一个类似的类去实现。

  • 再介绍一个封装了Cookie持久化的第三方库(推荐)

ClearableCookieJar cookieJar = new PersistentCookieJar(
        new SetCookieCache(), new SharedPrefsCookiePersistor(context));
builder.cookieJar(cookieJar);

需要引入下面这个包:

compile 'com.github.franmontiel:PersistentCookieJar:v1.0.0

0x02. 拦截器

  • addInterceptor和addNetworkInterceptor的区别
    前一篇文章有同学问到两者的区别,okHttp官方对拦截器做了解释,并给了一张图,感觉挺一目了然的。

    Paste_Image.png
    Paste_Image.png

    两种拦截器简单来说就是调用时机的区别,应用拦截器调用时机较早,也就是进入chain.proceed的递归较早,相应的完成递归得到response会较晚;而网络拦截器则相反,request请求调用时机较晚,会较早完成chain.proceed递归调用,得到response的时机较早。
    简单来说就是应用拦截器较上层,而网络拦截器较底层,所有拦截器就是一个由浅入深的递归调用。具体还是得看源码。

  • Http Header
    可以通过这个拦截器为Request添加全局统一的Header。

/**
 * 网络请求公共头信息插入器
 *
 * Created by XiaoFeng on 17/1/18.
 */
public class HttpHeaderInterceptor implements Interceptor {
    @Override
    public Response intercept(Chain chain) throws IOException {
        Request original = chain.request();
        Request request = original.newBuilder()
                .header("User-Agent", "Android, xxx")
                .header("Accept", "application/json")
                .header("Content-type", "application/json")
                .method(original.method(), original.body())
                .build();
        return chain.proceed(request);
    }
}
  1. 公共参数
    主要参考这篇
/**
 * 网络请求公共参数插入器
 * <p>
 * Created by XiaoFeng on 2017/1/25.
 */
public class CommonParamsInterceptor implements Interceptor {
    @Override
    public Response intercept(Chain chain) throws IOException {
        Request request = chain.request();

        if (request.method().equals("GET")) {
            HttpUrl httpUrl = request.url().newBuilder()
                    .addQueryParameter("version", "xxx")
                    .addQueryParameter("device", "Android")
                    .addQueryParameter("timestamp", String.valueOf(System.currentTimeMillis()))
                    .build();
            request = request.newBuilder().url(httpUrl).build();
        } else if (request.method().equals("POST")) {
            if (request.body() instanceof FormBody) {
                FormBody.Builder bodyBuilder = new FormBody.Builder();
                FormBody formBody = (FormBody) request.body();

                for (int i = 0; i < formBody.size(); i++) {
                    bodyBuilder.addEncoded(formBody.encodedName(i), formBody.encodedValue(i));
                }

                formBody = bodyBuilder
                        .addEncoded("version", "xxx")
                        .addEncoded("device", "Android")
                        .addEncoded("timestamp", String.valueOf(System.currentTimeMillis()))
                        .build();

                request = request.newBuilder().post(formBody).build();
            }
        }

        return chain.proceed(request);
    }
}
  1. 缓存策略
/**
 * 网络请求缓存策略插入器
 *
 * Created by XiaoFeng on 17/1/17.
 */
public class HttpCacheInterceptor implements Interceptor {
    @Override
    public Response intercept(Chain chain) throws IOException {
        Request request = chain.request();
        // 无网络时,始终使用本地Cache
        if (!NetworkUtil.isNetworkConnected()) {
            request = request.newBuilder()
                    .cacheControl(CacheControl.FORCE_CACHE)
                    .build();
        }

        Response response = chain.proceed(request);
        if (NetworkUtil.isNetworkConnected()) {
            // 有网络时,设置缓存过期时间0个小时
            int maxAge = 0;
            response.newBuilder()
                    .header("Cache-Control", "public, max-age=" + maxAge)
                    .removeHeader("Pragma") // 清除头信息,因为服务器如果不支持,会返回一些干扰信息,不清除下面无法生效
                    .build();
        } else {
            // 无网络时,设置缓存过期超时时间为4周
            int maxStale = 60 * 60 * 24 * 28;
            response.newBuilder()
                    .header("Cache-Control", "public, only-if-cached, max-stale=" + maxStale)
                    .removeHeader("Pragma")
                    .build();
        }
        return response;
    }
}
  1. 调试工具
    使用的是Facebook推出的一个集成到Chrome中的调试工具,需要引入下面两个库:
compile 'com.facebook.stetho:stetho:1.4.1'
compile 'com.facebook.stetho:stetho-okhttp3:1.4.1'

在Application中初始化就可以用了

Stetho.initializeWithDefaults(this);

如何调试?

  • 打开Chrome浏览器
  • 地址栏输入chrome://inspect
  • 进入页面后,在左边的DevTools -> Devices -> Remote Target下,可以找到你连接的手机设备,点开后就会出现调试页面了,后面就自己研究吧,不光可以调试网络请求,还可以查看手机中的数据库和SharePreference等持久化文件,而且不用root,很强大!

0x03. FastJson解析库封装

网上很多介绍retrofit的文章,对网络请求返回的结果,使用的都是默认的Gson库,虽然可以满足大部分人的需求,但是有些对性能要求高一点的人,还是习惯使用FastJson库做解析,这里就讲讲如何把默认的Gson库替换成FastJson库。

首先,默认Gson库的设置是这样的:

Retrofit retrofit = new Retrofit.Builder()
                .client(builder.build())
                .addConverterFactory(GsonConverterFactory.create())
                .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
                .baseUrl(BASE_URL)
                .build();

用FastJson库替换后是这样的:

Retrofit retrofit = new Retrofit.Builder()
                .client(builder.build())
                .addConverterFactory(FastJsonConvertFactory.create())
                .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
                .baseUrl(BASE_URL)
                .build();

是不是很像,没错,就是把ConverterFactory替换了一下而已。

至于FastJsonConvertFactory的实现,其实就是仿造GsonConverterFactory的源码来写的,并不复杂。

主要有三个类:

  1. 工厂类:FastJsonConvertFactory,里面就是分别创建了Request和Response的转换类。
/**
 *
 * Created by XiaoFeng on 2017/1/17.
 */
public class FastJsonConvertFactory extends Converter.Factory {
    public static FastJsonConvertFactory create() {
        return new FastJsonConvertFactory();
    }

    @Override
    public Converter<?, RequestBody> requestBodyConverter(Type type, Annotation[] parameterAnnotations, Annotation[] methodAnnotations, Retrofit retrofit) {
        return new FastJsonRequestConverter<>();
    }

    @Override
    public Converter<ResponseBody, ?> responseBodyConverter(Type type, Annotation[] annotations, Retrofit retrofit) {
        return new FastJsonResponseConverter<>(type);
    }
}
  1. Request转换类:FastJsonRequestConverter
/**
 *
 * Created by XiaoFeng on 2017/1/17.
 */
public class FastJsonRequestConverter<T> implements Converter<T, RequestBody> {
    private static final MediaType MEDIA_TYPE = MediaType.parse("application/json; charset=UTF-8");
    private static final Charset UTF_8 = Charset.forName("UTF-8");

    @Override
    public RequestBody convert(T value) throws IOException {
        return RequestBody.create(MEDIA_TYPE, JSON.toJSONBytes(value));
    }
}
  1. Response转换类:FastJsonResponseConverter
/**
 *
 * Created by XiaoFeng on 2017/1/17.
 */
public class FastJsonResponseConverter<T> implements Converter<ResponseBody, T> {
    private final Type type;

    public FastJsonResponseConverter(Type type) {
        this.type = type;
    }

    @Override
    public T convert(ResponseBody value) throws IOException {
        BufferedSource buffer = Okio.buffer(value.source());
        String s = buffer.readUtf8();
        buffer.close();
        return JSON.parseObject(s, type);
    }
}

是不是很简单,如果想再换成别的第三方json解析库,照着这个写就可以了。

0x04. 生命周期

上一篇中还有同学提到RxJava的生命周期管理,防止内存泄漏,这个可以直接使用第三方库,参考这篇
一般引用下面两个库就够了:

compile 'com.trello:rxlifecycle:1.0'
compile 'com.trello:rxlifecycle-components:1.0'

有两种使用方式:
1. 自动取消订阅,使用bindToLifecycle。
需要继承至RxActivity或者RxFragment等基类。

@Override
protected void onStart() {
    super.onStart();
    // Using automatic unsubscription, this should determine that the correct time to
    // unsubscribe is onStop (the opposite of onStart).
    Observable.interval(1, TimeUnit.SECONDS)
            .doOnUnsubscribe(new Action0() {
                @Override
                public void call() {
                    Log.i(TAG, "Unsubscribing subscription from onStart()");
                }
            })
            // 因为bindToLifecycle是在onStart的时候调用,所以在onStop的时候自动取消订阅
            .compose(this.<Long>bindToLifecycle())
            .subscribe(new Action1<Long>() {
                @Override
                public void call(Long num) {
                    Log.i(TAG, "Started in onStart(), running until in onStop(): " + num);
                }
            });
}

从下面这段核心函数可以看清自动取消订阅的规则,就是在哪个生命周期内调用bindToLifecycle,就在与其对应的结束生命周期函数调用时自动取消订阅。

private static final Func1<ActivityEvent, ActivityEvent> ACTIVITY_LIFECYCLE =
        new Func1<ActivityEvent, ActivityEvent>() {
            @Override
            public ActivityEvent call(ActivityEvent lastEvent) {
                switch (lastEvent) {
                    case CREATE:
                        return ActivityEvent.DESTROY;
                    case START:
                        return ActivityEvent.STOP;
                    case RESUME:
                        return ActivityEvent.PAUSE;
                    case PAUSE:
                        return ActivityEvent.STOP;
                    case STOP:
                        return ActivityEvent.DESTROY;
                    case DESTROY:
                        throw new OutsideLifecycleException("Cannot bind to Activity lifecycle when outside of it.");
                    default:
                        throw new UnsupportedOperationException("Binding to " + lastEvent + " not yet implemented");
                }
            }
        };

2. 手动取消订阅,使用bindUntilEvent。
需要继承至RxActivity或者RxFragment等基类。

@Override
protected void onResume() {
    super.onResume();
    // `this.<Long>` is necessary if you're compiling on JDK7 or below.
    // If you're using JDK8+, then you can safely remove it.
    Observable.interval(1, TimeUnit.SECONDS)
            .doOnUnsubscribe(new Action0() {
                @Override
                public void call() {
                    Log.i(TAG, "Unsubscribing subscription from onResume()");
                }
            })
            // 手动设置在Activity onDestroy的时候取消订阅
            .compose(this.<Long>bindUntilEvent(ActivityEvent.DESTROY))
            .subscribe(new Action1<Long>() {
                @Override
                public void call(Long num) {
                    Log.i(TAG, "Started in onResume(), running until in onDestroy(): " + num);
                }
            });
}

3. 自定义RxActivity/RxFragment
直接继承RxActivity/RxFragment有时会碰到问题,因为有可能本身已经有一个基类需要继承,java不能多继承。不过不要慌,我们可以自定义一个自己的基类,实现方式参考RxActivity。

public abstract class RxActivity extends Activity implements LifecycleProvider<activityevent> {
    private final BehaviorSubject<activityevent> lifecycleSubject = BehaviorSubject.create();

    public RxActivity() {
    }

    @NonNull
    @CheckResult
    public final Observable<activityevent> lifecycle() {
        return this.lifecycleSubject.asObservable();
    }

    @NonNull
    @CheckResult
    public final <t> LifecycleTransformer<t> bindUntilEvent(@NonNull ActivityEvent event) {
        return RxLifecycle.bindUntilEvent(this.lifecycleSubject, event);
    }

    @NonNull
    @CheckResult
    public final <t> LifecycleTransformer<t> bindToLifecycle() {
        return RxLifecycleAndroid.bindActivity(this.lifecycleSubject);
    }

    @CallSuper
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        this.lifecycleSubject.onNext(ActivityEvent.CREATE);
    }

    @CallSuper
    protected void onStart() {
        super.onStart();
        this.lifecycleSubject.onNext(ActivityEvent.START);
    }

    @CallSuper
    protected void onResume() {
        super.onResume();
        this.lifecycleSubject.onNext(ActivityEvent.RESUME);
    }

    @CallSuper
    protected void onPause() {
        this.lifecycleSubject.onNext(ActivityEvent.PAUSE);
        super.onPause();
    }

    @CallSuper
    protected void onStop() {
        this.lifecycleSubject.onNext(ActivityEvent.STOP);
        super.onStop();
    }

    @CallSuper
    protected void onDestroy() {
        this.lifecycleSubject.onNext(ActivityEvent.DESTROY);
        super.onDestroy();
    }
}

突然发现写文章真是一个知识梳理,自我学习的好方法,比没有目的性的看很多技术文章有用很多倍,极力推荐有能力的同学都去尝试写属于自己的技术博客。^ ^

参考:
https://gold.xitu.io/entry/572ed42ddf0eea0063186e1f
https://gist.github.com/franmontiel/ed12a2295566b7076161
https://gold.xitu.io/entry/5825300b2f301e005c47fac5
http://www.codexiu.cn/android/blog/39432/
http://androidxx.ren/forum.php?mod=viewthread&tid=19
https://gold.xitu.io/entry/58290ea2570c35005878ce8f

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,198评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,663评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,985评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,673评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,994评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,399评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,717评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,407评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,112评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,371评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,891评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,255评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,881评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,010评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,764评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,412评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,299评论 2 260

推荐阅读更多精彩内容