RxJava + Retrofit2 + OkHttp3 封裝及踩坑(續(xù))

前一篇文章(也是我在簡(jiǎn)書上的第一篇技術(shù)文章.)講了Android三劍客的基礎(chǔ)用法和簡(jiǎn)單封裝绍撞,有一些封裝只是一筆帶過,還有些用法被遺漏沒講到的得院,所以在這篇里統(tǒng)一做下查漏補(bǔ)缺傻铣。

0x00 先做一下糾正:

https和失敗重連,OkHttp默認(rèn)是支持的祥绞,并不用手動(dòng)去設(shè)置(在OkHttpClient.Builder中已默認(rèn)設(shè)置)非洲,所以O(shè)kHttpClient.Builder的初始化可以簡(jiǎn)化為:

// 創(chuàng)建OkHttpClient
OkHttpClient.Builder builder = new OkHttpClient.Builder()
        // 超時(shí)設(shè)置
        .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>();
        }
    });

這種簡(jiǎn)單的實(shí)現(xiàn)就谜,每次重啟App都會(huì)需要重新登錄怪蔑,不可取。

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

這里出現(xiàn)了兩個(gè)類:JavaNetCookieJarPersistentCookieStore

  • JavaNetCookieJar就是對(duì)CookieJar的封裝實(shí)現(xiàn)丧荐,里面實(shí)現(xiàn)了對(duì)Cookie持久化存儲(chǔ)和獲取的調(diào)用邏輯缆瓣,OkHttp已經(jīng)幫我們實(shí)現(xiàn)了這個(gè)類,需要引入下面這個(gè)包:
compile 'com.squareup.okhttp3:okhttp-urlconnection:3.5.0'
  • PersistentCookieStore是具體實(shí)現(xiàn)Cookie持久化的類虹统,使用的是SharedPreferences弓坞,具體代碼實(shí)現(xiàn)可參考這篇
    當(dāng)然车荔,如果你想通過數(shù)據(jù)庫實(shí)現(xiàn)持久化渡冻,也可以自己封裝一個(gè)類似的類去實(shí)現(xiàn)。

  • 再介紹一個(gè)封裝了Cookie持久化的第三方庫(推薦)

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

需要引入下面這個(gè)包:

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

0x02. 攔截器

  • addInterceptor和addNetworkInterceptor的區(qū)別
    前一篇文章有同學(xué)問到兩者的區(qū)別忧便,okHttp官方對(duì)攔截器做了解釋族吻,并給了一張圖,感覺挺一目了然的。

    Paste_Image.png
    Paste_Image.png

    兩種攔截器簡(jiǎn)單來說就是調(diào)用時(shí)機(jī)的區(qū)別超歌,應(yīng)用攔截器調(diào)用時(shí)機(jī)較早砍艾,也就是進(jìn)入chain.proceed的遞歸較早,相應(yīng)的完成遞歸得到response會(huì)較晚巍举;而網(wǎng)絡(luò)攔截器則相反脆荷,request請(qǐng)求調(diào)用時(shí)機(jī)較晚,會(huì)較早完成chain.proceed遞歸調(diào)用懊悯,得到response的時(shí)機(jī)較早蜓谋。
    簡(jiǎn)單來說就是應(yīng)用攔截器較上層,而網(wǎng)絡(luò)攔截器較底層炭分,所有攔截器就是一個(gè)由淺入深的遞歸調(diào)用桃焕。具體還是得看源碼。

  • Http Header
    可以通過這個(gè)攔截器為Request添加全局統(tǒng)一的Header捧毛。

/**
 * 網(wǎng)絡(luò)請(qǐng)求公共頭信息插入器
 *
 * 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. 公共參數(shù)
    主要參考這篇
/**
 * 網(wǎng)絡(luò)請(qǐng)求公共參數(shù)插入器
 * <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. 緩存策略
/**
 * 網(wǎng)絡(luò)請(qǐng)求緩存策略插入器
 *
 * Created by XiaoFeng on 17/1/17.
 */
public class HttpCacheInterceptor implements Interceptor {
    @Override
    public Response intercept(Chain chain) throws IOException {
        Request request = chain.request();
        // 無網(wǎng)絡(luò)時(shí)覆旭,始終使用本地Cache
        if (!NetworkUtil.isNetworkConnected()) {
            request = request.newBuilder()
                    .cacheControl(CacheControl.FORCE_CACHE)
                    .build();
        }

        Response response = chain.proceed(request);
        if (NetworkUtil.isNetworkConnected()) {
            // 有網(wǎng)絡(luò)時(shí),設(shè)置緩存過期時(shí)間0個(gè)小時(shí)
            int maxAge = 0;
            response.newBuilder()
                    .header("Cache-Control", "public, max-age=" + maxAge)
                    .removeHeader("Pragma") // 清除頭信息岖妄,因?yàn)榉?wù)器如果不支持,會(huì)返回一些干擾信息寂祥,不清除下面無法生效
                    .build();
        } else {
            // 無網(wǎng)絡(luò)時(shí)荐虐,設(shè)置緩存過期超時(shí)時(shí)間為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. 調(diào)試工具
    使用的是Facebook推出的一個(gè)集成到Chrome中的調(diào)試工具,需要引入下面兩個(gè)庫:
compile 'com.facebook.stetho:stetho:1.4.1'
compile 'com.facebook.stetho:stetho-okhttp3:1.4.1'

在Application中初始化就可以用了

Stetho.initializeWithDefaults(this);

如何調(diào)試丸凭?

  • 打開Chrome瀏覽器
  • 地址欄輸入chrome://inspect
  • 進(jìn)入頁面后福扬,在左邊的DevTools -> Devices -> Remote Target下,可以找到你連接的手機(jī)設(shè)備惜犀,點(diǎn)開后就會(huì)出現(xiàn)調(diào)試頁面了铛碑,后面就自己研究吧,不光可以調(diào)試網(wǎng)絡(luò)請(qǐng)求虽界,還可以查看手機(jī)中的數(shù)據(jù)庫和SharePreference等持久化文件汽烦,而且不用root,很強(qiáng)大莉御!

0x03. FastJson解析庫封裝

網(wǎng)上很多介紹retrofit的文章撇吞,對(duì)網(wǎng)絡(luò)請(qǐng)求返回的結(jié)果,使用的都是默認(rèn)的Gson庫礁叔,雖然可以滿足大部分人的需求牍颈,但是有些對(duì)性能要求高一點(diǎn)的人,還是習(xí)慣使用FastJson庫做解析琅关,這里就講講如何把默認(rèn)的Gson庫替換成FastJson庫煮岁。

首先,默認(rèn)Gson庫的設(shè)置是這樣的:

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();

是不是很像,沒錯(cuò)画机,就是把ConverterFactory替換了一下而已冶伞。

至于FastJsonConvertFactory的實(shí)現(xiàn),其實(shí)就是仿造GsonConverterFactory的源碼來寫的色罚,并不復(fù)雜碰缔。

主要有三個(gè)類:

  1. 工廠類:FastJsonConvertFactory,里面就是分別創(chuàng)建了Request和Response的轉(zhuǎn)換類戳护。
/**
 *
 * 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轉(zhuǎn)換類: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轉(zhuǎn)換類: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);
    }
}

是不是很簡(jiǎn)單金抡,如果想再換成別的第三方j(luò)son解析庫,照著這個(gè)寫就可以了腌且。

0x04. 生命周期

上一篇中還有同學(xué)提到RxJava的生命周期管理梗肝,防止內(nèi)存泄漏,這個(gè)可以直接使用第三方庫铺董,參考這篇巫击。
一般引用下面兩個(gè)庫就夠了:

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

有兩種使用方式:
1. 自動(dòng)取消訂閱,使用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()");
                }
            })
            // 因?yàn)閎indToLifecycle是在onStart的時(shí)候調(diào)用,所以在onStop的時(shí)候自動(dòng)取消訂閱
            .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);
                }
            });
}

從下面這段核心函數(shù)可以看清自動(dòng)取消訂閱的規(guī)則重付,就是在哪個(gè)生命周期內(nèi)調(diào)用bindToLifecycle顷级,就在與其對(duì)應(yīng)的結(jié)束生命周期函數(shù)調(diào)用時(shí)自動(dòng)取消訂閱。

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. 手動(dòng)取消訂閱确垫,使用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()");
                }
            })
            // 手動(dòng)設(shè)置在Activity onDestroy的時(shí)候取消訂閱
            .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有時(shí)會(huì)碰到問題删掀,因?yàn)橛锌赡鼙旧硪呀?jīng)有一個(gè)基類需要繼承翔冀,java不能多繼承。不過不要慌披泪,我們可以自定義一個(gè)自己的基類纤子,實(shí)現(xiàn)方式參考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();
    }
}

突然發(fā)現(xiàn)寫文章真是一個(gè)知識(shí)梳理款票,自我學(xué)習(xí)的好方法计福,比沒有目的性的看很多技術(shù)文章有用很多倍,極力推薦有能力的同學(xué)都去嘗試寫屬于自己的技術(shù)博客徽职。^ ^

參考:
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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末象颖,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子姆钉,更是在濱河造成了極大的恐慌说订,老刑警劉巖抄瓦,帶你破解...
    沈念sama閱讀 216,470評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異陶冷,居然都是意外死亡钙姊,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,393評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門埂伦,熙熙樓的掌柜王于貴愁眉苦臉地迎上來煞额,“玉大人,你說我怎么就攤上這事沾谜〔不伲” “怎么了?”我有些...
    開封第一講書人閱讀 162,577評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵基跑,是天一觀的道長(zhǎng)婚温。 經(jīng)常有香客問我,道長(zhǎng)媳否,這世上最難降的妖魔是什么栅螟? 我笑而不...
    開封第一講書人閱讀 58,176評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮篱竭,結(jié)果婚禮上力图,老公的妹妹穿的比我還像新娘。我一直安慰自己掺逼,他們只是感情好搪哪,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,189評(píng)論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著坪圾,像睡著了一般。 火紅的嫁衣襯著肌膚如雪惑朦。 梳的紋絲不亂的頭發(fā)上兽泄,一...
    開封第一講書人閱讀 51,155評(píng)論 1 299
  • 那天,我揣著相機(jī)與錄音漾月,去河邊找鬼病梢。 笑死,一個(gè)胖子當(dāng)著我的面吹牛梁肿,可吹牛的內(nèi)容都是我干的蜓陌。 我是一名探鬼主播,決...
    沈念sama閱讀 40,041評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼吩蔑,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼钮热!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起烛芬,我...
    開封第一講書人閱讀 38,903評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤隧期,失蹤者是張志新(化名)和其女友劉穎飒责,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體仆潮,經(jīng)...
    沈念sama閱讀 45,319評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡宏蛉,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,539評(píng)論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了性置。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片拾并。...
    茶點(diǎn)故事閱讀 39,703評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖鹏浅,靈堂內(nèi)的尸體忽然破棺而出嗅义,到底是詐尸還是另有隱情,我是刑警寧澤篡石,帶...
    沈念sama閱讀 35,417評(píng)論 5 343
  • 正文 年R本政府宣布芥喇,位于F島的核電站,受9級(jí)特大地震影響凰萨,放射性物質(zhì)發(fā)生泄漏继控。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,013評(píng)論 3 325
  • 文/蒙蒙 一胖眷、第九天 我趴在偏房一處隱蔽的房頂上張望武通。 院中可真熱鬧,春花似錦珊搀、人聲如沸冶忱。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,664評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽囚枪。三九已至,卻和暖如春劳淆,著一層夾襖步出監(jiān)牢的瞬間链沼,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,818評(píng)論 1 269
  • 我被黑心中介騙來泰國打工沛鸵, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留括勺,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,711評(píng)論 2 368
  • 正文 我出身青樓曲掰,卻偏偏與公主長(zhǎng)得像疾捍,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子栏妖,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,601評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容