[Android]网络库的封装/OkHttp

*[Disclaimer] 本文大量参考了这里

0x00前言

每个团队都会有一套网络库。网络库无外乎是对HttpURLConnection,HttpClient,OkHttp,Volley等已有框架的封装。
最早我开发的时候由于并不涉及很复杂的网络操作,直接在AsyncTask里用了手写了URLConnection的同步请求(默认get方式)。

进入公司后发现团队封装了非常成熟的网络框架。底层用的是HttpClient。包含了缓存,重连等。支持同步,异步,但我问了下,说并没有用get,而是所有请求都用了post。

一些已有框架,摘自这里

  • HttpURLConnection
    HttpURLConnection是一种多用途、轻量极的HTTP客户端,使用它来进行HTTP操作可以适用于大多数的应用程序。虽然HttpURLConnection的API提供的比较简单,但是同时这也使得我们可以更加容易地去使用和扩展它。从Android4.4开始HttpURLConnection的底层实现采用的是okHttp。

  • HttpClient
    Apache HttpClient早就不推荐httpclient,5.0之后干脆废弃,后续会删除。6.0删除了HttpClient。

  • OkHttp
    okhttp是高性能的http库,支持同步、异步,而且实现了spdy、http2、websocket协议,api很简洁易用,和volley一样实现了http协议的缓存。picasso就是利用okhttp的缓存机制实现其文件缓存,实现的很优雅,很正确,反例就是UIL(universal image loader),自己做的文件缓存,而且不遵守http缓存机制。

  • volley
    volley是一个简单的异步http库,仅此而已。缺点是不支持同步,这点会限制开发模式。自带缓存,支持自定义请求。不适合大文件上传和下载。
    Volley在Android 2.3及以上版本,使用的是HttpURLConnection,而在Android 2.2及以下版本,使用的是HttpClient。
    Volley自己的定位是轻量级网络交互,适合大量的,小数据传输。
    不过再怎么封装Volley在功能拓展性上始终无法与OkHttp相比。Volley停止了更新,而OkHttp得到了官方的认可,并在不断优化。

0x01 对HttpURLConnection进行简单封装

从文章开始的链接里找到的例子,在本地试了下:

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

public class NetUtils {
    public static String post(String url, String content) {
        HttpURLConnection conn = null;
        try {
            // 创建一个URL对象
            URL mURL = new URL(url);
            // 调用URL的openConnection()方法,获取HttpURLConnection对象
            conn = (HttpURLConnection) mURL.openConnection();

            conn.setRequestMethod("POST");// 设置请求方法为post
            conn.setReadTimeout(5000);// 设置读取超时为5秒
            conn.setConnectTimeout(10000);// 设置连接网络超时为10秒
            conn.setDoOutput(true);// 设置此方法,允许向服务器输出内容

            // post请求的参数
            String data = content;
            // 获得一个输出流,向服务器写数据,默认情况下,系统不允许向服务器输出内容
            OutputStream out = conn.getOutputStream();// 获得一个输出流,向服务器写数据
            out.write(data.getBytes());
            out.flush();
            out.close();

            int responseCode = conn.getResponseCode();// 调用此方法就不必再使用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();// 关闭连接
            }
        }

        return null;
    }

    public static String get(String url) {
        HttpURLConnection conn = null;
        try {
            // 利用string url构建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();// 把流中的数据转换成字符串,采用的编码是utf-8(模拟器默认编码)
        os.close();
        return state;
    }
}

可以看到,post方法用到OutputStream 和InputStream ,get方法仅用到InputStream 。
直接调用上面的util里的两个方法,那就是同步请求了(注意要放在子线程中啊)。
调试的话,在请求过程中会卡在:
int responseCode = conn.getResponseCode();
这一行,直至返回200的code。我发现访问很多大网站都出现了302, 301的code。找了个gov的网站,返回了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排队,轮到了之后就通过回调通知到主线程
                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();
    }
}

关于同步和异步,我看例子中提到,异步的好基友是回调。这里利用了handler,
我看了下曾经写过的一段代码:


同步和异步

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

那对于上面的那两个util,怎么实现同步和异步呢。例子里给出了异步的写法,在AsyncNetUtils中比较清楚了。那如果是同步的话,其实也是要放在子线程中的,但是像我下面这样写的话,就体现得不是很清楚了:


同步get

我应该再封装一个SyncNetUtils,然后在子线程实现NetUtils的方法,并且返回一个response,这样就是同步了,而不是每次自己搞一个子线程和handler。

事实上我感觉同步的使用场景确实远不如异步。毕竟异步也可以通过显示不能取消的圆形ProgressBar来模拟同步效果,而且可以同时执行多个请求。

这样一个简单的「网络库」的不足之处:

  • 每次都new Thread,new Handler消耗过大(用线程池、重用handler)
  • 没有异常处理机制
  • 没有缓存机制
  • 没有完善的API(请求头,参数,编码,拦截器等)与调试模式
  • 没有Https

关于缓存机制,是把url和请求参数保存下来,匹配返回内容(有「新鲜度」概念)。对于实时性比较高的APP,缓存通常是打开的。


缓存

0x02 OkHttp四大核心类

OkHttpClient、Request、Call 和 Response。

- OkHttpClient

关键词是Client。也就是请求的客户端。封装成一个类,那么所有的请求都可以共用response缓存,线程池,连接池

可以配置OkHttpClient的一些参数,比如超时时间、缓存目录、代理、Authenticator等,那么就需要用到内部类OkHttpClient.Builder,设置如下所示:

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

看下它的documentation吧:


OkHttpClient文档

文档提到了3点,

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

下面简单看下第一点。

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.

只创建一个OkHttpClient实例的话,性能最好。这是因为每个client都有自己的连接池和线程池。Reuse连接和线程可以减少等待时间(这正是线程池的作用)和内存消耗。

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

创建了一个线程池,用来在后台清理过期的连接。注意看它的参数,其实就是常用的CachedThreadPool

CachedThreadPool 是通过 java.util.concurrent.Executors 创建的 ThreadPoolExecutor 实例。这个实例会根据需要,在线程可用时,重用之前构造好的池中线程。这个线程池在执行 大量短生命周期的异步任务时(many short-lived asynchronous task),可以显著提高程序性能。调用 execute 时,可以重用之前已构造的可用线程,如果不存在可用线程,那么会重新创建一个新的线程并将其加入到线程池中。如果线程超过 60 秒还未被使用,就会被中止并从缓存中移除。因此,线程池在长时间空闲后不会消耗任何资源。

稍微看下内部类Builder:


OkHttpClient的无参构造函数就会new一个Builder

- Request

Request类封装了请求报文信息:请求的Url地址、请求的方法(如GET、POST等)、各种请求头(如Content-Type、Cookie)以及可选的请求体。一般通过内部类Request.Builder的链式调用生成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代表了一个实际的HTTP请求,它是连接Request和Response的桥梁,通过Request对象的newCall()方法可以得到一个Call对象。Call对象既支持同步获取数据,也可以异步获取数据。
执行Call对象的execute()方法,会阻塞当前线程去获取数据,该方法返回一个Response对象。
执行Call对象的enqueue()方法,不会阻塞当前线程,该方法接收一个Callback对象,当异步获取到数据之后,会回调执行Callback对象的相应方法。如果请求成功,则执行Callback对象的onResponse方法,并将Response对象传入该方法中;如果请求失败,则执行Callback对象的onFailure方法。

- Response

Response类封装了响应报文信息:状态吗(200、404等)、响应头(Content-Type、Server等)以及可选的响应体。可以通过Call对象的execute()方法获得Response对象,异步回调执行Callback对象的onResponse方法时也可以获取Response对象。

0x03 OkHttp的一些使用方式

我们项目中的网络请求实际上已经封装得非常深了,所以下面这些用法,相比之下还是略显繁琐。简单过一下,具体用法请看原文。

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

会阻塞线程。
既然网络操作要放在子线程中,那同步(sync)请求又是怎么阻塞线程的?

  • 异步GET
    要想异步执行网络请求,需要执行Call对象的enqueue方法,该方法接收一个okhttp3.Callback对象,enqueue方法不会阻塞当前线程,会新开一个工作线程,让实际的网络请求在工作线程中执行。

  • 用POST发送String
    要指定MIME类型

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

还有发送各种东西。。不列出来了。

Ref:
https://www.jianshu.com/p/2fa728c8b366
http://frodoking.github.io/2015/03/12/android-okhttp/

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

推荐阅读更多精彩内容