使用 okHttp

MVP+okHttp+Retrofit+RxJava+Glide+Dagger 是现在最流行的一套技术框架, MVP 现在没心思去弄,所以先把后面的都给学习了再说。 okHttp 的版本现在已经到3了,综合来说是比较好的一个网络请求框架,相比Volley,它可以上传大数据就是个优胜了,而且 okHttp 还处理了代理服务器问题和 SSL 握手失败等等很多问题,反正就是选它了。

用Android Studio开发,当然要导入 okHttp 库,而 okHttp 内部依赖 okio ,所以最好也导入 okio 库(下面两个库是我刚刚在 AS 里面搜索到的最新的版本):

compile 'com.squareup.okhttp3:okhttp:3.5.0'
compile 'com.squareup.okio:okio:1.11.0'

使用 okHttp 请求网络,主要是请求( Request )和响应( Response )两部分,请求方式有 get、post 两种,又可以分为同步和异步两种方式。但说起来主要流程有三步:
1.获得 okHttpClient 对象;
2.构建 Request 对象,利用 Request 对象获得 Call 对象;
3.执行请求(同步/异步),返回并处理响应结果。

获得 okHttpClient 对象

简单写法:

OkHttpClient mOkHttpClient = new OkHttpClient();

添加一些设置的较为复杂的写法:

// 获得 OkHttpClient 对象
OkHttpClient mOkHttpClient = new OkHttpClient();
// 获得 OkHttpClient.Builder 对象
OkHttpClient.Builder builder = mOkHttpClient.newBuilder();
// 设置
builder.connectTimeout(10, TimeUnit.SECONDS)//设置超时时间
        .readTimeout(10, TimeUnit.SECONDS)//设置读取超时时间
        .writeTimeout(10, TimeUnit.SECONDS)//设置写入超时时间
        .build();

这样就可以做一些设置,OKHttp 十分强大,还可以设置拦截器、缓存等。

构建 Request 对象

到这一步, Get 请求和 Post 请求就有区别了,但大体上是一样的。先来看 Get 请求。

** Get 请求构建 Request 对象:**

// 1.获得 Request.Builder 对象
Request.Builder requestBuilder = new Request.Builder();
// 2.添加 url、header 等必要信息
requestBuilder.url("www.baidu.com").addHeader("User-Agent","android").
header("Content-Type","text/html; charset=utf-8");
// 3.构建 Request 对象
Request request = requestBuilder.build();

** Post 请求构建 Request 对象:**

// 1.获得 Request.Builder 对象
Request.Builder requestBuilder = new Request.Builder();
// 2.添加 url、header 等必要信息
requestBuilder.url("www.baidu.com").addHeader("User-Agent", "android")
        .header("Content-Type", "text/html; charset=utf-8");
// 3.构建 RequestBody 对象(这里以表单形式作为例子)
FormBody requestBody = new FormBody.Builder().add("name", "value").build();
// 4.加入 RequestBody 对象构建 Request 对象
Request request = requestBuilder.post(requestBody).build();

注意:

  1. addHeader(String key, String value) 调用了 headers.add(String key, String value) ,而 header(String key, String value) 调用了 headers.set(String key, String value) ,所以虽然都是键值对形式添加请求头,但是, addHeader(...) 可以添加相同 key 的 header ,不会移除;而 header(...) 会移除相同 key 的 header ,只保留一个
  2. Get 请求,如果有请求参数,是直接拼接在 url 之后的,以?key1=value&key2=value2的结构出现;而 post 则是把请求参数通过构建 RequestBody 对象放在了 Request 的 body 部分。
  3. RequestBody 对象可通过多种方式构建,以提供表单字符串文件多块请求等各种 post 上传方式。

获得 Call 对象

Call call = mOkHttpClient.newCall(request);
执行请求,返回并处理响应结果

同步 Get / Post 请求

Response response = call.execute();

可以调用 isSuccessful() 判断请求是否成功

if (response.isSuccessful()){
    // 同步请求成功的处理逻辑
} else {
    // 同步请求失败的处理逻辑
}

异步 Get / Post 请求

call.enqueue(new Callback() {
   @Override
   public void onFailure(Call call, IOException e) {
        // 请求失败处理逻辑
   }

   @Override
   public void onResponse(Call call, Response response) throws IOException {
       // 请求成功处理逻辑
       // 可以从Response对象调用相应方法获得字符串、流、byte[]等,譬如:
       // String str = response.body().string();
       // byte[] bytes = response.body().bytes();
       // InputStream is = response.body().byteStream();
   }
});

注意:
1.上传下载功能必须得用异步的方式进行,所以调用 call.enqueue,将 call 加入调度队列,然后等待任务执行完成,在 Callback 中即可得到结果;
2.把流形式转化为字符串用 string() ,把 Object 对象转化为字符串用 toString();
3.以流形式操作就可以通过 IO 的方式写文件。这也说明了 onResponse(...) 并不是在UI线程里面执行(异步嘛),所以如果希望操作控件,还是需要使用 handler 等。

例子

同步请求

    /**
     * 同步Post请求
     */
    public void postSyncHttp() {
        // 获得OkHttpClient对象
        OkHttpClient mOkHttpClient = new OkHttpClient().newBuilder()
                .connectTimeout(10, TimeUnit.SECONDS)//设置超时时间
                .readTimeout(10, TimeUnit.SECONDS)//设置读取超时时间
                .writeTimeout(10, TimeUnit.SECONDS)//设置写入超时时间
                .build();

        // 获得Request对象(表单形式)
        Request request = new Request.Builder()
                .url("www.baidu.com")
                .addHeader("User-Agent", "android")
                .header("Content-Type", "text/html; charset=utf-8")
                .post(new FormBody.Builder()
                        .add("name", "value")
                        .build())
                .build();

        // 流形式RequestBody
//        RequestBody requestBody = new RequestBody() {
//            @Override
//            public MediaType contentType() {
//                MediaType MEDIA_TYPE_MARKDOWN = MediaType.parse("text/x-markdown; charset=utf-8")
//                return MEDIA_TYPE_MARKDOWN;
//            }
//
//            @Override
//            public void writeTo(BufferedSink sink) throws IOException {
//                sink.writeUtf8("Numbers\n");
//                sink.writeUtf8("-------\n");
//                for (int i = 2; i <= 997; i++) {
//                    sink.writeUtf8(String.format(" * %s = %s\n", i, factor(i)));
//                }
//            }
//
//            private String factor(int n) {
//                for (int i = 2; i < n; i++) {
//                    int x = n / i;
//                    if (x * i == n) return factor(x) + " × " + i;
//                }
//                return Integer.toString(n);
//            }
//        };

        // 文件形式RequestBody
//        MediaType MEDIA_TYPE_MARKDOWN
//                = MediaType.parse("text/x-markdown; charset=utf-8");
//        File file = new File("README.md");
//        RequestBody requestBody = RequestBody.create(MEDIA_TYPE_MARKDOWN, file);

        // 获得Call对象
        Call call = mOkHttpClient.newCall(request);

        try {
            Response response = call.execute();
            if (response.isSuccessful()) {
                String responseStr = response.body().string();
                Log.d("", "Post同步请求响应结果:" + responseStr);
            } else {
                Log.d("", "Post同步请求失败");
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

异步请求

    /**
     * 异步Post请求
     */
    public void postSyncHttp() {
        // 获得OkHttpClient对象
        ...
        同上

        // 获得Request对象
       ...
       同上

        // 获得Call对象
        Call call = mOkHttpClient.newCall(request);

        call.enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
                Log.d("", "Post同步请求失败");
            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {
                final String responseStr = response.body().string();
                Log.d("", "Post同步请求响应结果:" + responseStr);
                // UI线程更新控件
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        mTextView.setText("Post同步请求响应结果:" + responseStr);
                    }
                });
            }
        });
    }

Get 请求忽略 .post(RequestBody requestBody) 就好。

暂时记录到这里。这就是okHttp的基本使用,缓存、Gson解析JSON响应、处理验证、每个Cell配置等等,后面再继续。


2016.12.13更新

Interceptor

Interceptor 也就是拦截器,这是一个很强大的机制,在 okHttp 中有两种拦截器:

  1. Application Interceptor:可缓存,对于每个 HTTP 响应都只会调用一次,可以通过不调用 Chain.proceed 方法来终止请求,也可以通过多次调用 Chain.proceed 方法来进行重试;
  2. Network Interceptor: 不可缓存,调用执行中的自动重定向和重试所产生的响应也会被调用,而如果响应来自缓存,则不会被调用。

在 okHttpClient 建造的时候,使用 addInterceptor() 添加拦截器,拦截器执行顺序等于添加顺序。

实现 Interceptor 接口
Interceptor 接口只包含一个方法 intercept,其参数是 Chain 对象。Chain 对象表示的是当前的拦截器链条。
1.通过 Chain 的 request 方法可以获取到当前的 Request 对象;
2.在使用完 Request 对象之后,通过 Chain 对象的 proceed 方法来继续拦截器链条的执行;
3.当执行完成之后,可以对得到的 Response 对象进行额外的处理。

看一个例子:

/**
 * 实现拦截器,查看请求的 url 和响应结果
 */
public class CustomInterceptor implements Interceptor {
    public Response intercept(Chain chain) throws IOException {
        // 获取到当前的 Request 对象
        Request request = chain.request();

        // 查看发送请求的url
        String url = request.url();
        System.out.println("发送请求url:" + url);
 
        // 执行,获得Response
        Response response = chain.proceed(request);
        
        // 查看响应结果
        System.out.println("响应结果:" + response.body().string());
 
        return response;
    }
}

而 okHttp 提供了一个网络日志拦截器,使用的时候先要导入依赖:

compile 'com.squareup.okhttp3:logging-interceptor:3.5.0'

HttpLoggingInterceptor 提供了四种控制打印信息类型的等级,分别是:

  1. NONE:没有任何日志信息;
  2. BASIC:打印请求类型,URL,请求体大小,返回值状态以及返回值的大小;
  3. HEADERS:打印返回请求和返回值头部信息,请求类型,URL和返回值状态码;
  4. BODY:打印请求和返回值的头部和body信息。

创建一个 HttpLoggingInterceptor 对象,然后调用 setLevel() ,再将 HttpLoggingInterceptor 对象添加到 okHttpClient 即可。eg:

// 新建 HttpLoggingInterceptor 对象
HttpLoggingInterceptor logging = new HttpLoggingInterceptor(); 
// 设置日志等级
logging.setLevel(HttpLoggingInterceptor.Level.BODY); 
// 通过 addInterceptor() 添加到 okHttpClient 中
OkHttpClient okHttpClient =  new OkHttpClient.Builder()
    .addInterceptor(new CustomInterceptor()) 
    .addInterceptor(logging) 
    ...
    .build();

上面的例子是应用拦截器的,拦截器真的很强大,拦截器更多的相关知识请看:Okhttp-wiki 之 Interceptors 拦截器

Cache
重要概念

Cache-Control
Cache-Control 是由服务器返回的 Response 中添加的 Header 信息,是请求和响应遵循的缓存机制。在请求消息或响应消息中设置Cache-Control并不会修改另一个消息处理过程中的缓存处理过程。目的是告诉客户端是要从本地读取缓存,还是从服务器获取消息,它有不同的值,每个值有不同的作用:

  1. Public:响应可被任何缓存区缓存,告诉缓存服务器,即使是对于不该缓存的内容也缓存起来;
  2. Private:对于单个用户的整个或部分响应消息,不能被共享缓存处理。这允许服务器仅仅描述当用户的部分响应消息,此响应消息对于其他用户的请求无效;
  3. no-cache:请求或响应消息不能缓存;
  4. no-store:用于防止重要的信息被无意的发布。在请求消息中发送将使得请求和响应消息都不使用缓存;
  5. max-age:可以接收生存期不大于指定时间(以秒为单位)的响应;
  6. min-fresh:可以接收响应时间小于当前时间加上指定时间的响应;
  7. max-stale:可以接收超出超时期间的响应消息。如果指定 max-stale 消息的值,那么客户机可以接收超出超时期指定值之内的响应消息。

实现缓存
1.创建缓存文件夹;
2.设置缓存大小;
3.创建缓存对象,添加到 okHttpClient 对象中。
eg:

OkHttpClient okHttpClient = new OkHttpClient();
// 创建缓存文件夹,为安全起见,一般是私密数据空间;
File cacheFile = new File(context.getExternalCacheDir(), "cacheDir");
// 设置缓存大小
int cacheSize = 1024 * 1024 * 100;
// 创建缓存对象
Cache cache = new Cache(cacheFile, cacheSize); 
// 添加到 okHttpClient 对象中
okHttpClient.setCache(cache);

// OkHttpClient okHttpClient = new OkHttpClient().Builder().cache(cache).build();

这样在允许缓存的时候,就可以看到 Response 缓存了,Response 对象调用 cacheResponse() 就可以获得缓存内容了。

但有个问题就是,服务端在返回 Response 的时候,不一定添加好了 Cache-Control 相关的内容(譬如就算添加了,也是 no-cache 不允许缓存这种情况),那么怎么办?

1.使用拦截器拦截 Response 修改 Header 信息
eg:

/**
 * 拦截响应结果,实现缓存
 */
private Interceptor cacheInterceptor() { 
    Interceptor cacheInterceptor = new Interceptor() { 
        @Override 
        public Response intercept(Chain chain) throws IOException {
            // 获得请求  
            Request request = chain.request();
            // 执行请求,获得响应结果 
            Response response = chain.proceed(request);
            // 获得响应结果的 Cache-Control 信息 
            String cacheControl = request.cacheControl().toString();
            // 修改 Cache-Control 信息,譬如定位缓存时间60秒
            if (TextUtils.isEmpty(cacheControl)) { 
                cacheControl = "public, max-age=60"; 
            }
            // 返回新构造的响应结果 
            return response.newBuilder()
                           .header("Cache-Control", cacheControl)
                           .removeHeader("Pragma")
                           .build(); 
            } 
        }; 
}

然后在设置了缓存区域的 okHttpClient 中添加上这个拦截器即可。

但是,使用拦截器实现缓存的方案存在一些缺点:
APP 里面最好只有一个 okHttpClient,也只有一个缓存文件夹,很好理解,单例模式,而使用拦截器实现缓存,拦截器是添加到 okHttpClient 中的,那么岂不是 okHttpClient 处理的网络请求都只有一个缓存时间? 问题就在于很多种请求需求的缓存时间不一定是一致的(譬如请求的图片希望缓存7天,而网络新闻只缓存1分钟)。

2.使用官方推荐的方法:CacheControl 类实现缓存
CacheControl 有两个静态常量:

  1. FORCE_CACHE:强制使用缓存,如果用了这个常量但还去做网络请求的话,okHttp会提示504 code;
  2. FORCE_NETWORK:强制使用网络。

使用这两个常量返回的都是 CacheControl 对象,那么如果想设置更多的情况怎么做呢?,CacheControl 也有使用建造者的方式,eg:

// 获得 CacheControl 的 Builder 对象
CacheControl.Builder builder = new CacheControl.Builder();
// 设置各种情况: 
builder.noCache();//不使用缓存,全部走网络,其实 FORCE_NETWORK 常量获得的 CacheControl 对象就是调用了这个方法
builder.noStore();//不使用缓存,也不存储缓存 
builder.onlyIfCached();//只使用缓存 
builder.noTransform();//禁止转码 
builder.maxAge(10, TimeUnit.MILLISECONDS);//可以接收生存期不大于指定时间的响应,设为0,不会缓存,直接走网络
builder.maxStale(10, TimeUnit.SECONDS);//可以接收超出超时期间的响应消息,这个方法加上 onlyIfCached() 就是 FORCE_CACHE 调用的方法
builder.minFresh(10, TimeUnit.SECONDS);//可以接收响应时间小于当前时间加上指定时间的响应
// 设置好之后,获得 CacheControl 对象
CacheControl cache = builder.build();//cacheControl

怎么给 Request 设置缓存?
1.创建一个设置好的 CacheControl 对象;
2.Request 的 Builder 对象调用 cacheControl(CacheControl cacheControl)。
eg:

....// 创建 okHttpClient 对象并已经设置好缓存文件夹 
// 获得 CacheControl 的 Builder 对象
CacheControl.Builder builder = new CacheControl.Builder();
// 设置好缓存情况
builder.maxAge(10, TimeUnit.MILLISECONDS)
...; 
// 生成 CacheControl 对象
CacheControl cache = builder.build();

// 或者使用两个静态常量中的一个,直接生产 CacheControl 对象 
// CacheControl cache = CacheControl.FORCE_CACHE;
// CacheControl cache = CacheControl.FORCE_NETWORK

// 获得 Request 的 Builder 对象
Request.Builder requestBuilder = new Request.Builder();
// 将 CacheControl 对象 添加给 Request 的 Builder 对象,并设置好其它请求设置
requestBuilder.cacheControl(cache)
...;
// 获得 Request 对象
Request request = requestBuilder.build();
// 发出请求
...

解决504问题
上面说到504的问题,会出现在只使用缓存的情况,譬如使用 FORCE_CACHE 常量或者同时调用 onlyIfCached() 和 maxStale()。
1.判断网络情况,在无网情况下才发出只使用缓存的 Request:

if(non_NetWork) {
    request = request.newBuilder().cacheControl(CacheControl.FORCE_CACHE).build();
}

2.判断返回的 Response 是否已经在本地有缓存了:

Response response = okHttpClient.newCall(request).excute();
if(response.code() != 504) {
    // 已经有缓存,直接使用
} else {
    // 没有缓存,做其它处理
}

缓存部分内容参考OKHTTP之缓存配置详解

推荐阅读更多精彩内容

  • 参考Android网络请求心路历程Android Http接地气网络请求(HttpURLConnection) 一...
    合肥懒皮阅读 13,063评论 7 56
  • Okhttp使用指南与源码分析 标签(空格分隔): Android 使用指南篇# 为什么使用okhttp### A...
    背影杀手不太冷阅读 6,955评论 2 122
  • 前言 用了那么久的OkHttp,解决了不少的联网问题。对于热门的好轮子,总不能一直停留在会用这个层面上吧,是时候动...
    Goo_Yao阅读 1,427评论 3 9
  • 这篇文章主要讲 Android 网络请求时所使用到的各个请求库的关系,以及 OkHttp3 的介绍。(如理解有误,...
    小庄bb阅读 642评论 0 4
  • 小河的变化 在我们小区楼下,有一条河,河水干净清澈,河边常常有人散步或者钓鱼。我很喜欢这里,经常来这里玩。 可是几...
    江山吴阅读 35评论 0 2