Android企業(yè)級架構(gòu)-仿微信-網(wǎng)絡架構(gòu)

市場上的絕大多數(shù)APP,都離不開網(wǎng)絡數(shù)據(jù)請求,從注冊到登錄、提交/拉取數(shù)據(jù),都是我們必須要面對的問題,可見網(wǎng)絡數(shù)據(jù)請求在APP產(chǎn)品中的重要性。那么網(wǎng)絡請求在企業(yè)級架構(gòu)中,應該怎樣處理是比較合理的呢?這篇文章我們來聊一聊網(wǎng)絡數(shù)據(jù)請求的架構(gòu)處理。

可能有人會問:網(wǎng)上有太多合適的框架供我們選擇和使用,比如原生的HttpClient、HttpURLConnection,Google在前兩者上擴展的Volley,Square組織的OkHttp(+Retrofit組合)等等,都是很方便的網(wǎng)絡請求框架,那我們?yōu)槭裁催€要自己去探究,重復造輪呢?
PS:HttpClient因維護成本大,在2.3版本就不建議使用,6.0版本已經(jīng)廢棄,還在用的小伙伴要注意

原因很簡單,在企業(yè)級開發(fā)過程中,最重要的指標是穩(wěn)定,同時要控制更新、替換和后期維護的成本,這一指標對所有第三方框架都相同。另外還需要控制Apk文件的大小,不可以無限制的添加第三方庫,或僅為一個小功能添加了一個龐大且沒用的庫。

說了這么多廢話,下面扯正題

我們要做什么?是要自己寫一套網(wǎng)絡請求庫?

No,網(wǎng)上有那么多寫的牛X的網(wǎng)絡庫,我們怎么寫也寫不過他們。我們做的事,只為我們自己和我們的產(chǎn)品就夠了。那應該做什么事呢?
封裝
我們需要封裝自己的網(wǎng)絡層外殼,有了這個外殼,其它開發(fā)者不需要關(guān)注網(wǎng)絡請求的具體實現(xiàn),直接調(diào)用我們的API即可。以前Volley熱門,我們可以使用Volley的實現(xiàn),現(xiàn)在OkHttp比較火,我們可以使用OkHttp的實現(xiàn),但是不管怎么換,都不會影響上層的使用。大大降低了更新、維護的成本。
時下最火的Retrofit是一款很時尚的封裝框架,它的框架設計能力非一般人所能及,接口的設計與邏輯完全解耦,非常適合學習研究,只是今天火的是Retrofit,明天可能就是其它的XXX。所以筆者對于這種框架,僅停留在學習階段,不會輕易使用,寫一套最適合自己的,最普通的框架,對自己和團隊的學習本成來說,都只有好處沒有壞處

建議愛學習的朋友,仔細讀一下Retrofit,網(wǎng)上有很多文章,我就不寫了

原始底層封裝

這節(jié)寫封裝的框架,是一套原始框架,其中不包涵任何的與需求相關(guān)的邏輯,所以今天這一節(jié)的代碼,不管拿到哪個項目上,都可以適用。我們的網(wǎng)絡連接層使用OkHttp3,優(yōu)點大家都知道,不了解的去看一下。

網(wǎng)絡數(shù)據(jù)請求是一套工作流,簡單畫了一副圖,大家看一下

數(shù)據(jù)請求工作流

發(fā)起一個網(wǎng)絡請求簡單來說有5個節(jié)點:1. 發(fā)起請求,2. 連接請求,3. 請求響應,4. 數(shù)據(jù)解析,5. 返回結(jié)果。其中節(jié)點2我們使用OkHttp來幫我們做,那余下的4個節(jié)點,就是我們要做的事情了。

  1. 配置OkHttp3:打開OkHttp的GitHub地址我們看到OkHttp現(xiàn)在最新的版本是3.7.0,打開工程目錄下的build.gradle文件,在ext中添加如下變量:
    配置OkHttp的版本號

    然后打開network目錄下的build.gradle文件,在dependencies下添加如下引用
    compile "com.squareup.okhttp3:okhttp:$rootProject.ext.okhttpVersion"
    然后同步一下工程,將OkHttp的庫下載到本地。
  2. 打開network目錄下的src->main->java->com.monch.network,開始我們真正的Code之旅。
    整個文件目錄并不算復雜,如下圖所示:


    Java文件結(jié)構(gòu)

    整個文件目錄,是圍繞圖1的流程所示,從上到下分別是:executor(執(zhí)行器)目錄、AccountException(帳號異常)、ApiCallback(網(wǎng)絡請求回調(diào))、ApiRequest(網(wǎng)絡請求)、ApiResponse(網(wǎng)絡請求響應)、ApiResult(網(wǎng)絡請求結(jié)果)、ArrayFactory(Array創(chuàng)建器)、Failed(錯誤類型枚舉)
    在executor(執(zhí)行器)目錄下,包括IExecutor(執(zhí)行器接口)、OkHttpCallback(OkHttp響應回調(diào))、OkHttpClientFactory(OkHttpClient創(chuàng)建器)、OkHttpExecutor(OkHttp的執(zhí)行器實現(xiàn))、RequestUtils(請求工具類)

  3. 下面我們按照一個請求的流程來分解代碼。
    請求的最開始是ApiRequest,代碼如下
// 請求實例池
    private static final Pools.Pool<ApiRequest> POOL = new Pools.SynchronizedPool<>(30);

    // 生成ApiRequest實例
    public static ApiRequest obtain(Builder builder) {
        ApiRequest instance = POOL.acquire();
        if (instance == null) {
            instance = new ApiRequest(builder);
        }
        return instance;
    }

    // 釋放ApiRequest實例
    public static void release(ApiRequest request) {
        if (request == null) return;
        IExecutor executor = request.executor;
        if (executor != null) {
            // 釋放請求緩存
            executor.releaseCache(request);
        }
        request.url = null;
        // 在這里,我們將所有的ArrayMap都置為null,
        // 是因為之前的請求有可能已經(jīng)將ArrayMap的空間增加的足夠大
        // 為了避免浪費多余分配的空間,之后的每次使用我們都重新創(chuàng)建
        if (request.parameters != null) {
            request.parameters.clear();
            request.parameters = null;
        }
        if (request.headers != null) {
            request.headers.clear();
            request.headers = null;
        }
        if (request.files != null) {
            request.files.clear();
            request.files = null;
        }
        request.callback = null;
        request.charset = null;
        request.tag = null;
        POOL.release(request);
    }

    // 默認執(zhí)行器
    private static IExecutor mDefaultExecutor = new OkHttpExecutor();
    // 默認編碼方式
    private static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");

    /* 請求類型 */
    public static final int GET = 0;
    public static final int POST = GET + 1;
    public static final int UPLOAD = POST + 1;
    public static final int DOWNLOAD = UPLOAD + 1;

    private String url;     // 請求URL
    private int type;       // 請求類型
    private ArrayMap<String, String> parameters;    // 參數(shù)集合
    private ArrayMap<String, String> headers;       // 請求頭集合
    private ArrayMap<String, File> files;           // 上傳文件集合
    private ApiCallback callback;                   // 請求回調(diào)
    private Charset charset;                        // 編碼
    private Object tag;                             // 標記
    private IExecutor executor;                     // 執(zhí)行器

    private ApiRequest(Builder builder) {
        this.url = builder.url;
        this.type = builder.type;
        this.parameters = builder.parameters;
        this.headers = builder.headers;
        this.files = builder.files;
        this.callback = builder.callback;
        this.charset = builder.charset != null ? builder.charset : DEFAULT_CHARSET;
        this.tag = builder.tag;
        this.executor = builder.executor != null ? builder.executor : mDefaultExecutor;
    }

    // 開始請求
    private void request() {
        switch (type) {
            case POST:
                executor.doPost(this);
                break;
            case UPLOAD:
                executor.doUpload(this);
                break;
            case DOWNLOAD:
                executor.doDownload(this);
                break;
            default:
                executor.doGet(this);
                break;
        }
    }

    public String getUrl() {
        return url;
    }

    public int getType() {
        return type;
    }

    public ArrayMap<String, String> getParameters() {
        return parameters;
    }

    public ArrayMap<String, String> getHeaders() {
        return headers;
    }

    public ArrayMap<String, File> getFiles() {
        return files;
    }

    public ApiCallback getCallback() {
        return callback;
    }

    public Charset getCharset() {
        return charset;
    }

    public Object getTag() {
        return tag;
    }

    public IExecutor getExecutor() {
        return executor;
    }

    public void cancel() {
        if (executor != null) {
            executor.cancel(this);
        }
    }

    public static Builder newBuilder() {
        return new Builder();
    }
    
    public static class Builder {
        private String url;
        private int type = GET;
        private ArrayMap<String, String> parameters;
        private ArrayMap<String, String> headers;
        private ArrayMap<String, File> files;
        private ApiCallback callback;
        private Charset charset;
        private Object tag;
        private IExecutor executor;
        private Builder(){}
        public Builder url(String url) {
            this.url = url;
            return this;
        }
        public Builder addParameter(String key, String value) {
            if (parameters == null) {
                parameters = ArrayFactory.createArrayMap();
            }
            parameters.put(key, value);
            return this;
        }
        public Builder addHeader(String key, String value) {
            if (headers == null) {
                headers = ArrayFactory.createArrayMap(1);
            }
            headers.put(key, value);
            return this;
        }
        public Builder addFile(String key, File file) {
            if (files == null) {
                files = ArrayFactory.createArrayMap(1);
            }
            files.put(key, file);
            return this;
        }
        public Builder callback(ApiCallback callback) {
            this.callback = callback;
            return this;
        }
        public Builder charset(Charset charset) {
            this.charset = charset;
            return this;
        }
        public Builder tag(Object tag) {
            this.tag = tag;
            return this;
        }
        public Builder executor(IExecutor executor) {
            this.executor = executor;
            return this;
        }
        public ApiRequest get() {
            this.type = GET;
            ApiRequest request = obtain(this);
            request.request();
            return request;
        }
        public ApiRequest post() {
            this.type = POST;
            ApiRequest request = obtain(this);
            request.request();
            return request;
        }
        public ApiRequest upload() {
            this.type = UPLOAD;
            ApiRequest request = obtain(this);
            request.request();
            return request;
        }
        public ApiRequest download() {
            this.type = DOWNLOAD;
            ApiRequest request = obtain(this);
            request.request();
            return request;
        }
    }

在企業(yè)級工程中,網(wǎng)絡請求的需求量是巨大的,所以我們對每個ApiRequest都使用Pools.Pool,這是一個實例緩存池,避免在運行時重復創(chuàng)建大量的實例,了解過JVM的同學都知道,創(chuàng)建實例是一個比較耗時的過程,而且大量實例廢棄后會引發(fā)GC頻繁,造成卡頓。這里我要強調(diào)一點,優(yōu)化都是從一點一滴做起的,在優(yōu)化的過程中,我們學習的內(nèi)容也能更深入。
obtain和release兩個靜態(tài)方法,是生成和釋放ApiRequest實例的方法,用法大家可以看一下代碼。
接下來就是一些屬性的定義:
mDefaultExecutor:這個是靜態(tài)變量,存放默認的網(wǎng)絡請求執(zhí)行器,在我們的框架中,默認為OkHttpExecutor的實現(xiàn),如果某一天大家需要更改執(zhí)行器,那么直接替換這里就可以了。
默認的編碼方式是UTF-8,這個沒什么好說,大家都這樣。

接下來是請求類型的定義,共使用4種:GET、POST、UPLOAD、DOWNLOAD,網(wǎng)絡請求還有一些其它的類型,比如PUT、DELETE等,但現(xiàn)在基本很少使用,所以這里不定義。
以上的這些屬性都是靜態(tài)的,在內(nèi)存中單獨存在,不屬于任何一個實例。下面是一個請求真正需要的變量:請求地址、請求類型、參數(shù)、請求頭、文件、回調(diào)、編碼、標記、執(zhí)行器。ApiRequest類的構(gòu)造函數(shù)是私有的,強制使用obtain方法創(chuàng)建實例。
tag(標記)屬性需要說明一下,這是一個起到上下文的屬性,舉個粟子:我們在Service中發(fā)起了一個獲取好友列表的請求,假設這個接口比較費時,當數(shù)據(jù)還沒有返回時,用戶已經(jīng)退出登錄又換了一個帳號登錄上,然后獲取好友列表的請求才返回,如果這些數(shù)據(jù)按照當前已經(jīng)換帳號的環(huán)境去處理,必然是錯的,所以這個時候,大家就有必要做一下處理了。這個屬性我們可以在發(fā)起請求時保存一個userId,在處理之前,對比一下當前的userId是否一至,當一至時再處理。
代碼很簡單,大家都應該能看懂。對外暴露的接口就是ApiRequest.Builder創(chuàng)建請求。

接下來看一下IExecutor接口

/**
     * 拉取請求
     * @param request
     */
    void doGet(ApiRequest request);

    /**
     * 提交請求
     * @param request
     */
    void doPost(ApiRequest request);

    /**
     * 上傳請求
     * @param request
     */
    void doUpload(ApiRequest request);

    /**
     * 下載請求
     * @param request
     */
    void doDownload(ApiRequest request);

    /**
     * 釋放緩存
     * @param request
     */
    void releaseCache(ApiRequest request);

    /**
     * 取消請求
     * @param request
     */
    void cancel(ApiRequest request);

    /**
     * 取消所有請求
     */
    void cancelAll();

這個接口定義了這些方法,此版本我們使用OkHttp來實現(xiàn)這些接口,如果有一天我們需要換成Volley連接,那么直接創(chuàng)建一個VolleyExecutor類,實現(xiàn)IExecutor接口即可。

OkHttpExecutor的具體實現(xiàn)如下

private OkHttpClient mOkHttpClient;
    private Map<ApiRequest, Call> mRequestCache = ArrayFactory.createConcurrentHashMap();

    public OkHttpExecutor() {
        mOkHttpClient = OkHttpClientFactory.getClient();
    }

    @Override
    public void doGet(ApiRequest request) {
        OkHttpClient client = mOkHttpClient;
        Charset charset = request.getCharset();
        String url = RequestUtils.makeUrl(request.getUrl(), request.getParameters(), charset);
        ApiCallback apiCallback = request.getCallback();
        if (apiCallback != null) {
            apiCallback.onStart(request);
        }
        Request.Builder builder = new Request.Builder();
        builder.url(url);
        builder.tag(request.getTag());
        builder.get();
        RequestUtils.makeHeader(builder, request.getHeaders(), charset);
        Call call = client.newCall(builder.build());
        call.enqueue(new OkHttpCallback(request));
        mRequestCache.put(request, call);
    }

    @Override
    public void doPost(ApiRequest request) {
        OkHttpClient client = mOkHttpClient;
        Charset charset = request.getCharset();
        ApiCallback callback = request.getCallback();
        if (callback != null) {
            callback.onStart(request);
        }
        Request.Builder builder = new Request.Builder();
        builder.url(request.getUrl());
        builder.tag(request.getTag());
        builder.post(formBody(request.getParameters(), charset));
        RequestUtils.makeHeader(builder, request.getHeaders(), charset);
        Call call = client.newCall(builder.build());
        call.enqueue(new OkHttpCallback(request));
        mRequestCache.put(request, call);
    }

    private RequestBody formBody(Map<String, String> params, Charset charset) {
        FormBody.Builder builder = new FormBody.Builder();
        if (params != null && !params.isEmpty()) {
            for (Map.Entry<String, String> entry : params.entrySet()) {
                String key = RequestUtils.encode(entry.getKey(), charset);
                String value = RequestUtils.encode(entry.getValue(), charset);
                if (!TextUtils.isEmpty(key) && value != null) {
                    builder.addEncoded(key, value);
                }
            }
        }
        return builder.build();
    }

    @Override
    public void doUpload(ApiRequest request) {
        OkHttpClient client = mOkHttpClient;
        Charset charset = request.getCharset();
        ApiCallback callback = request.getCallback();
        if (callback != null) {
            callback.onStart(request);
        }
        Request.Builder builder = new Request.Builder();
        builder.url(request.getUrl());
        builder.tag(request.getTag());
        builder.post(formBody(request.getParameters(), request.getFiles(), charset));
        RequestUtils.makeHeader(builder, request.getHeaders(), charset);
        Call call = client.newCall(builder.build());
        call.enqueue(new OkHttpCallback(request));
        mRequestCache.put(request, call);
    }

    private static final String DEFAULT_CONTENT = "Content-Disposition";

    private RequestBody formBody(Map<String, String> params, Map<String, File> files, Charset charset) {
        MultipartBody.Builder builder = new MultipartBody.Builder();
        builder.setType(MultipartBody.FORM);
        if (params != null && !params.isEmpty()) {
            for (Map.Entry<String, String> entry : params.entrySet()) {
                String key = RequestUtils.encode(entry.getKey(), charset);
                byte[] content = entry.getValue().getBytes(charset);
                builder.addPart(Headers.of(DEFAULT_CONTENT, getParamValue(key)),
                        RequestBody.create(MediaType.parse(charset.name()), content));
            }
        }
        if (files != null && !files.isEmpty()) {
            for (Map.Entry<String, File> entry : files.entrySet()) {
                String name = RequestUtils.encode(entry.getKey(), charset);
                File file = entry.getValue();
                String fileName = file.getName();
                builder.addPart(Headers.of(DEFAULT_CONTENT, getFileValue(name, fileName)),
                        RequestBody.create(MediaType.parse(guessMimeType(fileName)), file));
            }
        }
        return builder.build();
    }

    private static String guessMimeType(String path) {
        FileNameMap fileNameMap = URLConnection.getFileNameMap();
        String contentTypeFor = fileNameMap.getContentTypeFor(path);
        if (contentTypeFor == null) {
            contentTypeFor = "application/octet-stream";
        }
        return contentTypeFor;
    }

    private static String getParamValue(String name) {
        return "form-data; name=\"" + name + "\"";
    }

    private static String getFileValue(String name, String fileName) {
        return "form-data; name=\"" + name + "\"; filename=\"" + fileName + "\"";
    }

    @Override
    public void doDownload(ApiRequest request) {
        doGet(request);
    }

    @Override
    public void releaseCache(ApiRequest request) {
        if (request == null) return;
        if (mRequestCache.containsKey(request)) {
            mRequestCache.remove(request);
        }
    }

    @Override
    public void cancel(ApiRequest request) {
        if (request == null) return;
        if (mRequestCache.containsKey(request)) {
            Call call = mRequestCache.get(request);
            if (call != null && !call.isCanceled()) {
                call.cancel();
            }
            mRequestCache.remove(request);
        }
    }

    @Override
    public void cancelAll() {
        if (!mRequestCache.isEmpty()) {
            for (Call call : mRequestCache.values()) {
                if (call != null && !call.isCanceled()) {
                    call.cancel();
                }
            }
            mRequestCache.clear();
        }
    }

OkHttp的具體用法在這就不講了,如果有不清楚的,去看一下OkHttp的使用即可。
這里有一點需要注意,就是我們的mRequestCache(請求緩存)變量,因為存在并發(fā)的問題,所以我這里定義為線程安全的ConcurrentHashMap。
請求的參數(shù),在這里都已經(jīng)做過Encoder處理,GET請求也會自動的將參數(shù)拼接到URL上,所以大家不需要擔心。

OkHttpClient的賦值,是使用OkHttpClientFactory類的getClient方法,這個方法是獲取了一個支持所有證書的SSL連接的實例,可用于https的請求。代碼如下
···

private static final int TIMEOUT = 60;
private volatile static OkHttpClient mOkHttpClient;
static OkHttpClient getClient() {
    if (mOkHttpClient == null) {
        synchronized (OkHttpClientFactory.class) {
            if (mOkHttpClient == null) {
                try {
                    X509TrustManager trustManager = createInsecureTrustManager();
                    SSLSocketFactory sslSocketFactory = createInsecureSslSocketFactory(trustManager);
                    mOkHttpClient = new OkHttpClient.Builder()
                            .connectTimeout(TIMEOUT, TimeUnit.SECONDS)
                            .readTimeout(TIMEOUT, TimeUnit.SECONDS)
                            .writeTimeout(TIMEOUT, TimeUnit.SECONDS)
                            .sslSocketFactory(sslSocketFactory, trustManager)
                            .hostnameVerifier(createInsecureHostnameVerifier())
                            .build();
                } catch (Exception e) {
                    Log.e(TAG, "Get client error", e);
                    mOkHttpClient = new OkHttpClient.Builder()
                            .connectTimeout(TIMEOUT, TimeUnit.SECONDS)
                            .readTimeout(TIMEOUT, TimeUnit.SECONDS)
                            .writeTimeout(TIMEOUT, TimeUnit.SECONDS)
                            .build();
                }
            }
        }
    }
    return mOkHttpClient;
}

private static HostnameVerifier createInsecureHostnameVerifier() {
    return new HostnameVerifier() {
        @Override public boolean verify(String s, SSLSession sslSession) {
            return true;
        }
    };
}

private static SSLSocketFactory createInsecureSslSocketFactory(TrustManager trustManager) {
    try {
        SSLContext context = SSLContext.getInstance("TLS");
        context.init(null, new TrustManager[] {trustManager}, new SecureRandom());
        return context.getSocketFactory();
    } catch (Exception e) {
        throw new AssertionError(e);
    }
}

/**
 * 信任所有證書
 */
private static X509TrustManager createInsecureTrustManager() {
    return new X509TrustManager() {
        @Override public void checkClientTrusted(X509Certificate[] chain, String authType) {
        }

        @Override public void checkServerTrusted(X509Certificate[] chain, String authType) {
        }

        @Override public X509Certificate[] getAcceptedIssuers() {
            return new X509Certificate[0];
        }
    };
}

···
接下來再看如下代碼:
Call call = client.newCall(builder.build());
call.enqueue(new OkHttpCallback(request));
這是將每個具體請求加入到請求隊列中,我們都創(chuàng)建了一個OkHttpCallback實例,用于請求響應的回調(diào)。代碼如下

public class OkHttpCallback implements Callback {

    /** 主線程執(zhí)行器 **/
    private static Handler mResponseHandler = new Handler(Looper.getMainLooper());
    private static Executor mResponsePoster = new Executor() {
        @Override
        public void execute(@NonNull Runnable command) {
            mResponseHandler.post(command);
        }
    };

    private ApiRequest apiRequest;

    public OkHttpCallback(ApiRequest apiRequest) {
        this.apiRequest = apiRequest;
    }

    @Override
    public void onFailure(Call call, IOException e) {
        // 請求失敗,將任務拋回主線程執(zhí)行
        mResponsePoster.execute(new FailedRunnable(apiRequest, call, e));
    }

    @Override
    public void onResponse(Call call, Response response) throws IOException {
        if (call.isCanceled()) {
            // 請求已終止
            mResponsePoster.execute(new FailedRunnable(apiRequest, call, null));
            return;
        }
        if (response == null) {
            // 請求未響應
            mResponsePoster.execute(new FailedRunnable(apiRequest, call,
                    new IOException("Response is null.")));
            return;
        }
        if (!response.isSuccessful()) {
            // 請求失敗
            mResponsePoster.execute(new FailedRunnable(apiRequest, call,
                    new IOException("Unexpected code " + response)));
            return;
        }
        ApiCallback apiCallback = apiRequest.getCallback();
        if (apiCallback == null) return;
        ResponseBody body = response.body();
        try {
            // 填充請求響應數(shù)據(jù)
            ApiResponse apiResponse = ApiResponse.obtain();
            apiResponse.setCode(response.code());
            apiResponse.setCharset(apiRequest.getCharset());
            apiResponse.setBody(body.bytes());
            Headers headers = response.headers();
            for (String key : headers.names()) {
                apiResponse.addHeader(key, headers.get(key));
            }
            apiResponse.setTag(apiRequest.getTag());
            // 回調(diào)響應,運行在子線程,用于數(shù)據(jù)解析
            ApiResult apiResult = apiCallback.onResponse(apiResponse);
            // 釋放響應數(shù)據(jù)
            ApiResponse.release(apiResponse);
            // 拋回主線程執(zhí)行完成
            mResponsePoster.execute(new SuccessRunnable(apiRequest, call, apiResult));
        } catch (Exception e) {
            // 拋回主線程執(zhí)行失敗
            mResponsePoster.execute(new FailedRunnable(apiRequest, call, e));
        } finally {
            if (body != null) body.close();
        }
    }

    // 請求錯誤處理
    private static class FailedRunnable implements Runnable {
        private ApiRequest apiRequest;
        private Call call;
        private Throwable throwable;
        FailedRunnable(ApiRequest apiRequest, Call call, Throwable throwable) {
            this.apiRequest = apiRequest;
            this.call = call;
            this.throwable = throwable;
        }
        @Override
        public void run() {
            ApiCallback apiCallback = apiRequest.getCallback();
            if (apiCallback != null) {
                if (call.isCanceled()) {
                    apiCallback.onCancel();
                } else {
                    Failed f = Failed.OTHER;
                    if (throwable != null) {
                        if (throwable instanceof IOException) {
                            f = Failed.NETWORK_ERROR;       // 網(wǎng)絡異常
                        } else if (throwable instanceof TimeoutException) {
                            f = Failed.TIMEOUT_ERROR;       // 請求超時
                        } else if (throwable instanceof JSONException) {
                            f = Failed.PARSE_ERROR;         // 數(shù)據(jù)解析異常
                        } else if (throwable instanceof AccountException) {
                            apiCallback.onAccountError();   // 帳戶信息異常
                            return;
                        }
                    }
                    apiCallback.onFailure(f, throwable);
                }
            }
            ApiRequest.release(apiRequest);
        }
    }

    // 請求成功處理
    private static class SuccessRunnable implements Runnable {
        private ApiRequest apiRequest;
        private Call call;
        private ApiResult apiResult;
        SuccessRunnable(ApiRequest apiRequest, Call call, ApiResult apiResult) {
            this.apiRequest = apiRequest;
            this.call = call;
            this.apiResult = apiResult;
        }
        @Override
        public void run() {
            ApiCallback callback = apiRequest.getCallback();
            if (callback != null) {
                if (call.isCanceled()) {
                    callback.onCancel();
                } else {
                    callback.onComplete(apiResult);
                }
            }
            ApiResult.release(apiResult);
            ApiRequest.release(apiRequest);
        }
    }

}

這個類實現(xiàn)了okhttp3.Callback接口,需要實現(xiàn)兩個方法onFailure和onResponse,大家應該都知道這兩個方法的作用吧?
在onResponse回調(diào)方法中,我們創(chuàng)建了ApiResponse實例,將主要的響應數(shù)據(jù)填充進去,包括響應碼、響應數(shù)據(jù)、響應頭等信息,依靠這些數(shù)據(jù),我們使用ApiCallback的回調(diào),將這些數(shù)據(jù)拋給上層使用者去解析
ApiResult apiResult = apiCallback.onResponse(apiResponse);
然后將結(jié)果使用mResponsePoster拋回到主線程處理,至此,一個完整的請求流程完成了。
下面我們看一下ApiCallback都有哪些回調(diào)

public interface ApiCallback {

    /**
     * 請求開始回調(diào),運行在當前線程
     * @param request 請求數(shù)據(jù)
     */
    void onStart(ApiRequest request);

    /**
     * 請求響應回調(diào),運行在子線程,主要用于數(shù)據(jù)解析。
     * 在解析過程中,如果發(fā)現(xiàn)后臺返回的錯誤為帳戶異常,
     * 可直接拋出AccountException,在onAccountError回調(diào)中統(tǒng)一處理
     * @param response 響應數(shù)據(jù)
     * @return
     * @throws JSONException JSON解析異常
     * @throws AccountException 帳戶登錄異常
     */
    ApiResult onResponse(ApiResponse response) throws JSONException, AccountException;

    /**
     * 請求完成,運行在主線程,將解析后的結(jié)果返回
     * @param result 解析結(jié)果
     */
    void onComplete(ApiResult result);

    /**
     * 請求失敗回調(diào),運行在主線程
     * @param failed 錯誤類型
     * @param throwable 異常信息
     */
    void onFailure(Failed failed, Throwable throwable);

    /**
     * 帳戶異?;卣{(diào),運行在主線程,在onResponse回調(diào)中,如果接收到AccountException異常,會直接回調(diào)到這里
     */
    void onAccountError();

    /**
     * 請求取消回調(diào),運行在主線程
     */
    void onCancel();

}

注釋寫的比較清楚,不再贅述。

整個封裝的代碼框架差不多就這些,此框架當前狀態(tài)下,有著非常好的擴展性。當我需要替換成Volley,我只需要做到以下幾點:

  1. 創(chuàng)建一個類叫VolleyExecutor,實現(xiàn)IExecutor接口,然后按照Volley的方式填充各個方法。
  2. 再創(chuàng)建一個叫VolleyCallback的類,將Volley的回調(diào)轉(zhuǎn)換成我們的ApiCallback。
  3. 將ApiRequest類的mDefaultExecutor屬性,默認實現(xiàn)替換成new VolleyExecutor();即可,完全不影響上層的任何使用。
    這只是一個簡單的封裝,我們也可以更徹底一點,做一個請求分發(fā)流,這樣我們只需要使用OkHttp的連接功能,不過這樣做太費事,暫時不考慮。

下面的代碼,是現(xiàn)在狀態(tài)的使用

ApiRequest req = ApiRequest.newBuilder()
                .url("https://www.baidu.com")
                .addParameter("key1", "value1")
                .addParameter("key2", "value2")
                .addHeader("header1", "headerValue1")
                .callback(new ApiCallback() {
                    @Override
                    public void onStart(ApiRequest request) {
                        // 請求開始
                    }

                    @Override
                    public ApiResult onResponse(ApiResponse response) throws JSONException, AccountException {
                        JSONObject jsonObject = new JSONObject(response.getString());
                        ApiResult result = ApiResult.obtain();
                        int code = jsonObject.optInt("code");
                        
                        // 假如我們與服務端的同學設定,code等于10的時候,說明帳號異常,那么處理代碼如下:
                        if (code == 10) {
                            throw new AccountException();
                        }
                        
                        result.setCode(code);
                        result.setErrorMessage(jsonObject.optString("errorMsg"));
                        if (result.isSuccess()) {   // 當code為0時,表示成功
                            Object object = jsonObject.optString("test");
                            result.put("object", object);
                        }
                        return result;
                    }

                    @Override
                    public void onComplete(ApiResult result) {
                        if (result.isSuccess()) {
                            Object object = result.get("object");
                            if (object != null) {
                                Log.e("Test", object.toString());
                            }
                        }
                    }

                    @Override
                    public void onFailure(Failed failed, Throwable throwable) {
                        Toast.makeText(this, failed.error(), Toast.LENGTH_SHORT).show();
                    }

                    @Override
                    public void onAccountError() {
                        // 這里表示登錄異常,我們可以將用戶踢到登錄頁面
                    }

                    @Override
                    public void onCancel() {
                        // 請求取消的回調(diào)
                    }
                })
                .get();
        if (如果需要取消請求) {
            req.cancel();
        }

各位看官,是不是一看嚇了一跳,我擦,這么費事還搞個屁呀。。。
說明,說明,說明。。。這是半成品,現(xiàn)在的狀態(tài),是擴展性非常好的狀態(tài),但是使用還不方便,在真正的工程使用時,我們還需要結(jié)合具體業(yè)務,做二次封裝。比如我們所有的接口都需要上傳用戶的位置、版本號等信息,現(xiàn)在的狀態(tài)是不方便的。

今天說到這,具體代碼請移步GitHub查看。感謝!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

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

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 134,948評論 18 139
  • 1. Java基礎(chǔ)部分 基礎(chǔ)部分的順序:基本語法,類相關(guān)的語法,內(nèi)部類的語法,繼承相關(guān)的語法,異常的語法,線程的語...
    子非魚_t_閱讀 31,766評論 18 399
  • 一. Java基礎(chǔ)部分.................................................
    wy_sure閱讀 3,836評論 0 11
  • 從語言學角度來說,日本人是非??蓱z的,因為他們只有5個元音——あ a(阿)、い i(衣)、う u(宇)、え e(誒...
    詹小蟲閱讀 1,457評論 3 5
  • 寫在前面 十分果然地向前老板遞交辭職信后,我選擇跳進了或者說即將跳入另一個大坑:當一個互聯(lián)網(wǎng)產(chǎn)品經(jīng)理。作為一個-1...
    Jing_X閱讀 2,406評論 0 3