[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,
我看了下曾经写过的一段代码:


[Android]网络库的封装/OkHttp_第1张图片
同步和异步

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

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


[Android]网络库的封装/OkHttp_第2张图片
同步get

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

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

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

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

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


[Android]网络库的封装/OkHttp_第3张图片
缓存

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吧:


[Android]网络库的封装/OkHttp_第4张图片
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的开头:

[Android]网络库的封装/OkHttp_第5张图片
ConnectionPool

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

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

稍微看下内部类Builder:


[Android]网络库的封装/OkHttp_第6张图片
OkHttpClient的无参构造函数就会new一个Builder

- Request

Request类封装了请求报文信息:请求的Url地址、请求的方法(如GET、POST等)、各种请求头(如Content-Type、Cookie)以及可选的请求体。一般通过内部类Request.Builder的链式调用生成Request对象。

[Android]网络库的封装/OkHttp_第7张图片
GET A URL

[Android]网络库的封装/OkHttp_第8张图片
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/

你可能感兴趣的:([Android]网络库的封装/OkHttp)