[Android]網(wǎng)絡(luò)庫的封裝/OkHttp

*[Disclaimer] 本文大量參考了這里。

0x00前言

每個團(tuán)隊都會有一套網(wǎng)絡(luò)庫。網(wǎng)絡(luò)庫無外乎是對HttpURLConnection,HttpClient,OkHttp,Volley等已有框架的封裝。
最早我開發(fā)的時候由于并不涉及很復(fù)雜的網(wǎng)絡(luò)操作,直接在AsyncTask里用了手寫了URLConnection的同步請求(默認(rèn)get方式)。

進(jìn)入公司后發(fā)現(xiàn)團(tuán)隊封裝了非常成熟的網(wǎng)絡(luò)框架。底層用的是HttpClient。包含了緩存,重連等。支持同步,異步,但我問了下,說并沒有用get,而是所有請求都用了post。

一些已有框架,摘自這里。

  • HttpURLConnection
    HttpURLConnection是一種多用途、輕量極的HTTP客戶端,使用它來進(jìn)行HTTP操作可以適用于大多數(shù)的應(yīng)用程序。雖然HttpURLConnection的API提供的比較簡單,但是同時這也使得我們可以更加容易地去使用和擴(kuò)展它。從Android4.4開始HttpURLConnection的底層實(shí)現(xiàn)采用的是okHttp。

  • HttpClient
    Apache HttpClient早就不推薦httpclient,5.0之后干脆廢棄,后續(xù)會刪除。6.0刪除了HttpClient。

  • OkHttp
    okhttp是高性能的http庫,支持同步、異步,而且實(shí)現(xiàn)了spdy、http2、websocket協(xié)議,api很簡潔易用,和volley一樣實(shí)現(xiàn)了http協(xié)議的緩存。picasso就是利用okhttp的緩存機(jī)制實(shí)現(xiàn)其文件緩存,實(shí)現(xiàn)的很優(yōu)雅,很正確,反例就是UIL(universal image loader),自己做的文件緩存,而且不遵守http緩存機(jī)制。

  • volley
    volley是一個簡單的異步http庫,僅此而已。缺點(diǎn)是不支持同步,這點(diǎn)會限制開發(fā)模式。自帶緩存,支持自定義請求。不適合大文件上傳和下載。
    Volley在Android 2.3及以上版本,使用的是HttpURLConnection,而在Android 2.2及以下版本,使用的是HttpClient。
    Volley自己的定位是輕量級網(wǎng)絡(luò)交互,適合大量的,小數(shù)據(jù)傳輸。
    不過再怎么封裝Volley在功能拓展性上始終無法與OkHttp相比。Volley停止了更新,而OkHttp得到了官方的認(rèn)可,并在不斷優(yōu)化。

0x01 對HttpURLConnection進(jìn)行簡單封裝

從文章開始的鏈接里找到的例子,在本地試了下:

首先,NetUtils 包含了最基本的post和get兩個方法。

public class NetUtils {
    public static String post(String url, String content) {
        HttpURLConnection conn = null;
        try {
            // 創(chuàng)建一個URL對象
            URL mURL = new URL(url);
            // 調(diào)用URL的openConnection()方法,獲取HttpURLConnection對象
            conn = (HttpURLConnection) mURL.openConnection();

            conn.setRequestMethod("POST");// 設(shè)置請求方法為post
            conn.setReadTimeout(5000);// 設(shè)置讀取超時為5秒
            conn.setConnectTimeout(10000);// 設(shè)置連接網(wǎng)絡(luò)超時為10秒
            conn.setDoOutput(true);// 設(shè)置此方法,允許向服務(wù)器輸出內(nèi)容

            // post請求的參數(shù)
            String data = content;
            // 獲得一個輸出流,向服務(wù)器寫數(shù)據(jù),默認(rèn)情況下,系統(tǒng)不允許向服務(wù)器輸出內(nèi)容
            OutputStream out = conn.getOutputStream();// 獲得一個輸出流,向服務(wù)器寫數(shù)據(jù)
            out.write(data.getBytes());
            out.flush();
            out.close();

            int responseCode = conn.getResponseCode();// 調(diào)用此方法就不必再使用conn.connect()方法
            if (responseCode == 200) {
                InputStream is = conn.getInputStream();
                String response = getStringFromInputStream(is);
                return response;
            } else {
                throw new NetworkErrorException("response status is " + responseCode);
            }

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (conn != null) {
                conn.disconnect();// 關(guān)閉連接
            }
        }

        return null;
    }

    public static String get(String url) {
        HttpURLConnection conn = null;
        try {
            // 利用string url構(gòu)建URL對象
            URL mURL = new URL(url);
            conn = (HttpURLConnection) mURL.openConnection();

            conn.setRequestMethod("GET");
            conn.setReadTimeout(5000);
            conn.setConnectTimeout(10000);

            int responseCode = conn.getResponseCode();
            if (responseCode == 200) {

                InputStream is = conn.getInputStream();
                String response = getStringFromInputStream(is);
                return response;
            } else {
                throw new NetworkErrorException("response status is " + responseCode);
            }

        } catch (Exception e) {
            e.printStackTrace();
        } finally {

            if (conn != null) {
                conn.disconnect();
            }
        }

        return null;
    }

    private static String getStringFromInputStream(InputStream is) throws IOException {
        ByteArrayOutputStream os = new ByteArrayOutputStream();
        // 模板代碼 必須熟練
        byte[] buffer = new byte[1024];
        int len = -1;
        while ((len = is.read(buffer)) != -1) {
            os.write(buffer, 0, len);
        }
        is.close();
        String state = os.toString();// 把流中的數(shù)據(jù)轉(zhuǎn)換成字符串,采用的編碼是utf-8(模擬器默認(rèn)編碼)
        os.close();
        return state;
    }
}

可以看到,post方法用到OutputStream 和InputStream ,get方法僅用到InputStream 。
直接調(diào)用上面的util里的兩個方法,那就是同步請求了(注意要放在子線程中?。?。
調(diào)試的話,在請求過程中會卡在:
int responseCode = conn.getResponseCode();
這一行,直至返回200的code。我發(fā)現(xiàn)訪問很多大網(wǎng)站都出現(xiàn)了302, 301的code。找了個gov的網(wǎng)站,返回了200。

另外,例子中提供了異步請求:

public class AsyncNetUtils {
    public interface Callback {
        void onResponse(String response);
    }

    public static void get(final String url, final Callback callback) {
        //綁定
        final Handler handler = new Handler(Looper.getMainLooper());
        new Thread(new Runnable() {
            @Override
            public void run() {
                //子線程中請求,請求完了用handler排隊,輪到了之后就通過回調(diào)通知到主線程
                final String response = NetUtils.get(url);
                handler.post(new Runnable() {
                    @Override
                    public void run() {
                        callback.onResponse(response);
                    }
                });
            }
        }).start();
    }

    public static void post(final String url, final String content, final Callback callback) {
        final Handler handler = new Handler();
        new Thread(new Runnable() {
            @Override
            public void run() {
                final String response = NetUtils.post(url, content);
                handler.post(new Runnable() {
                    @Override
                    public void run() {
                        callback.onResponse(response);
                    }
                });
            }
        }).start();
    }
}

關(guān)于同步和異步,我看例子中提到,異步的好基友是回調(diào)。這里利用了handler,
我看了下曾經(jīng)寫過的一段代碼:


同步和異步

可以看出,同步會一直等著返回值,得到返回值了才走下一步。

那對于上面的那兩個util,怎么實(shí)現(xiàn)同步和異步呢。例子里給出了異步的寫法,在AsyncNetUtils中比較清楚了。那如果是同步的話,其實(shí)也是要放在子線程中的,但是像我下面這樣寫的話,就體現(xiàn)得不是很清楚了:


同步get

我應(yīng)該再封裝一個SyncNetUtils,然后在子線程實(shí)現(xiàn)NetUtils的方法,并且返回一個response,這樣就是同步了,而不是每次自己搞一個子線程和handler。

事實(shí)上我感覺同步的使用場景確實(shí)遠(yuǎn)不如異步。畢竟異步也可以通過顯示不能取消的圓形ProgressBar來模擬同步效果,而且可以同時執(zhí)行多個請求。

這樣一個簡單的「網(wǎng)絡(luò)庫」的不足之處:

  • 每次都new Thread,new Handler消耗過大(用線程池、重用handler)
  • 沒有異常處理機(jī)制
  • 沒有緩存機(jī)制
  • 沒有完善的API(請求頭,參數(shù),編碼,攔截器等)與調(diào)試模式
  • 沒有Https

關(guān)于緩存機(jī)制,是把url和請求參數(shù)保存下來,匹配返回內(nèi)容(有「新鮮度」概念)。對于實(shí)時性比較高的APP,緩存通常是打開的。


緩存

0x02 OkHttp四大核心類

OkHttpClient、Request、Call 和 Response。

- OkHttpClient

關(guān)鍵詞是Client。也就是請求的客戶端。封裝成一個類,那么所有的請求都可以共用response緩存,線程池,連接池。

可以配置OkHttpClient的一些參數(shù),比如超時時間、緩存目錄、代理、Authenticator等,那么就需要用到內(nèi)部類OkHttpClient.Builder,設(shè)置如下所示:

OkHttpClient client = new OkHttpClient.Builder().
        readTimeout(30, TimeUnit.SECONDS).
        cache(cache).
        proxy(proxy).
        authenticator(authenticator).
        build();

看下它的documentation吧:


OkHttpClient文檔

文檔提到了3點(diǎn),

  1. OkHttpClients should be shared
  2. Customize your client with newBuilder()
  3. Shutdown isn't necessary

下面簡單看下第一點(diǎn)。

OkHttp performs best when you create a single OkHttpClient instance and reuse it for all of your HTTP calls. This is because each client holds its own connection pool and thread pools. Reusing connections and threads reduces latency and saves memory. Conversely, creating a client for each request wastes resources on idle pools.

只創(chuàng)建一個OkHttpClient實(shí)例的話,性能最好。這是因?yàn)槊總€client都有自己的連接池和線程池。Reuse連接和線程可以減少等待時間(這正是線程池的作用)和內(nèi)存消耗。

Use new OkHttpClient() to create a shared instance with the default settings:

// The singleton HTTP client.
public final OkHttpClient client = new OkHttpClient();

Or use new OkHttpClient.Builder() to create a shared instance with custom settings:

// The singleton HTTP client.
public final OkHttpClient client = new OkHttpClient.Builder()
    .addInterceptor(new HttpLoggingInterceptor())
    .cache(new Cache(cacheDir, cacheSize))
    .build();

同時,我們可以稍微看下ConnectionPool的開頭:

ConnectionPool

創(chuàng)建了一個線程池,用來在后臺清理過期的連接。注意看它的參數(shù),其實(shí)就是常用的CachedThreadPool。

CachedThreadPool 是通過 java.util.concurrent.Executors 創(chuàng)建的 ThreadPoolExecutor 實(shí)例。這個實(shí)例會根據(jù)需要,在線程可用時,重用之前構(gòu)造好的池中線程。這個線程池在執(zhí)行 大量短生命周期的異步任務(wù)時(many short-lived asynchronous task),可以顯著提高程序性能。調(diào)用 execute 時,可以重用之前已構(gòu)造的可用線程,如果不存在可用線程,那么會重新創(chuàng)建一個新的線程并將其加入到線程池中。如果線程超過 60 秒還未被使用,就會被中止并從緩存中移除。因此,線程池在長時間空閑后不會消耗任何資源。

稍微看下內(nèi)部類Builder:


OkHttpClient的無參構(gòu)造函數(shù)就會new一個Builder

- Request

Request類封裝了請求報文信息:請求的Url地址、請求的方法(如GET、POST等)、各種請求頭(如Content-Type、Cookie)以及可選的請求體。一般通過內(nèi)部類Request.Builder的鏈?zhǔn)秸{(diào)用生成Request對象。

GET A URL

POST TO A SERVER

- Call

A call is a request that has been prepared for execution. A call can be canceled. As this object
represents a single request/response pair (stream), it cannot be executed twice.

Call代表了一個實(shí)際的HTTP請求,它是連接Request和Response的橋梁,通過Request對象的newCall()方法可以得到一個Call對象。Call對象既支持同步獲取數(shù)據(jù),也可以異步獲取數(shù)據(jù)。
執(zhí)行Call對象的execute()方法,會阻塞當(dāng)前線程去獲取數(shù)據(jù),該方法返回一個Response對象。
執(zhí)行Call對象的enqueue()方法,不會阻塞當(dāng)前線程,該方法接收一個Callback對象,當(dāng)異步獲取到數(shù)據(jù)之后,會回調(diào)執(zhí)行Callback對象的相應(yīng)方法。如果請求成功,則執(zhí)行Callback對象的onResponse方法,并將Response對象傳入該方法中;如果請求失敗,則執(zhí)行Callback對象的onFailure方法。

- Response

Response類封裝了響應(yīng)報文信息:狀態(tài)嗎(200、404等)、響應(yīng)頭(Content-Type、Server等)以及可選的響應(yīng)體??梢酝ㄟ^Call對象的execute()方法獲得Response對象,異步回調(diào)執(zhí)行Callback對象的onResponse方法時也可以獲取Response對象。

0x03 OkHttp的一些使用方式

我們項(xiàng)目中的網(wǎng)絡(luò)請求實(shí)際上已經(jīng)封裝得非常深了,所以下面這些用法,相比之下還是略顯繁瑣。簡單過一下,具體用法請看原文。

  • 同步GET
    Response response = client.newCall(request).execute();

會阻塞線程。
既然網(wǎng)絡(luò)操作要放在子線程中,那同步(sync)請求又是怎么阻塞線程的?

  • 異步GET
    要想異步執(zhí)行網(wǎng)絡(luò)請求,需要執(zhí)行Call對象的enqueue方法,該方法接收一個okhttp3.Callback對象,enqueue方法不會阻塞當(dāng)前線程,會新開一個工作線程,讓實(shí)際的網(wǎng)絡(luò)請求在工作線程中執(zhí)行。

  • 用POST發(fā)送String
    要指定MIME類型

public static final MediaType MEDIA_TYPE_MARKDOWN
      = MediaType.parse("text/x-markdown; charset=utf-8");

還有發(fā)送各種東西。。不列出來了。

Ref:
http://www.itdecent.cn/p/2fa728c8b366
http://frodoking.github.io/2015/03/12/android-okhttp/

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容