OkHttp的使用

PREVIEW

  • 导入
  • 简单使用
  • 使用HttpUrl
  • Header处理
  • Post提交String
  • Post提交表单
  • Post提交文件
  • Post提交流
  • 使用Gson解析response
  • 设置超时
  • 配置新client
  • 拦截器
      应用拦截器
      网络拦截器
      两者区别
  • 缓存处理
      缓存策略
      缓存流程
      缓存总结

导入

在build.gradle下添加依赖,具体版本号参考OkHttp更新日志

dependencies {
    ...
    implementation 'com.squareup.okhttp3:okhttp:4.7.2'
}

简单使用

本文使用的OkHttp版本为4.7.2。OkHttp的核心类主要有OkHttpClientDispatcherCallRequestResponseInterceptorChain。其中OkHttpClient是负责管理多个Call的组织者,而每个Call又包含一个RequestResponse,并且Call中的回调用于提供响应结果。要完成一次网络请求,我们需要告诉Call需要处理的Request是什么样的,例如它的URL是什么,然后将Call交给OkHttpClientOkHttpClient仅对本次请求做一些配置,例如指定缓存路径,它会让Dispatcher去决定何时执行Call。而Dispatcher的底层实现就是一个由OkHttp默认实现的线程池,它将最终执行Call中的.run()方法。最后的InterceptorChain将用于数据的拦截处理。OkHttp提供两种方式提交网络请求,分别是Call.execute()Call.enqueue(Callback),前者会阻塞线程,后者加入队列异步执行。通过调用response.body().string()我们可以得到响应的body部分并以String形式返回,但值得注意的是.string()只能调用一次。

  • 同步调用

一般来说要在得到结果的第一时间修改UI,我们可能会使用Call.execute()AsyncTask完成提交请求。但AsyncTask通常会导致context内存泄漏,因为它是非静态嵌套类,所以不推荐使用同步调用。以下例子使用https://reqres.in测试请求:

public class MainActivity extends AppCompatActivity {
    private TextView mTextView;
    private OkHttpClient mClient = new OkHttpClient();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mTextView = findViewById(R.id.textView);

        String url = "https://reqres.in/api/users/2";

        OkHttpHandler okHttpHandler = new OkHttpHandler();
        okHttpHandler.execute(url);
    }

    private class OkHttpHandler extends AsyncTask<String, String, String> {
        @Override
        protected String doInBackground(String... params) {
            Request request = new Request.Builder()
                    .url(params[0])
                    .build();
            try {
                Response response = mClient.newCall(request).execute();
                return response.body().string();
            } catch (Exception e) {
                e.printStackTrace();
            }
            return null;
        }

        @Override
        protected void onPostExecute(String s) {
            super.onPostExecute(s);
            mTextView.setText(s);
        }
    }
}
  • 异步调用

抛开同步调用,使用Call.enqueue(Callback)Activity.runOnUiThread(Runnable)的方式是提交请求的最佳方案。其中Activity.runOnUiThread(Runnable)方法传入Runnable,这个Runnable将插入到UI线程的事件队列末尾,等待执行run()方法。以下例子使用https://reqres.in测试请求:

public class MainActivity extends AppCompatActivity {
    private TextView mTextView;
    private OkHttpClient mClient = new OkHttpClient();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mTextView = findViewById(R.id.textView);

        String url = "https://reqres.in/api/users/2";

        try {
            get(url);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private void get(String url) {
        Request request = new Request.Builder()
                .url(url)
                .build();

        mClient.newCall(request).enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
                call.cancel();
            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {
                final String responseString = response.body().string();

                MainActivity.this.runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        mTextView.setText(responseString);
                    }
                });
            }
        });
    }
}

使用HttpUrl

HttpUrl用于生成含参的URL,以下例子使用https://resttesttest.com测试请求:

HttpUrl.Builder urlBuilder = HttpUrl.parse("https://httpbin.org/get").newBuilder();
urlBuilder.addQueryParameter("category", "android");
urlBuilder.addQueryParameter("title", "okhttp");
String url2 = urlBuilder.build().toString();

Header处理

  • 设置请求头

.header()设置唯一的请求头,旧值会被替换。.addHeader()新增请求头,可以添加多值

Request request = new Request.Builder()
    .url(url)
    .addHeader("Accept","application/json; charset=utf-8")
    .header("Accept","application/json; charset=utf-8")
    .post(requestBody)
    .build();
  • 获得响应头

.header()返回单值,.headers()返回多值的响应头

String headerString=response.header("Server");
List<String> headerStrings=response.headers("Vary");

Log.i(TAG,headerString);
Iterator<String> it=headerStrings.iterator();
while (it.hasNext()) {
    Log.i(TAG,it.next());
}

Post提交String

以下例子使用https://reqres.in测试请求:

private static final MediaType JSON = MediaType.parse("application/json; charset=utf-8");
String url = "https://reqres.in/api/users/";
String jsonString = "{\n" +
        "    \"name\": \"morpheus\",\n" +
        "    \"job\": \"leader\"\n" +
        "}";
private void post(String url, final String requestString) {
    RequestBody requestBody = RequestBody.create(JSON, requestString);

    Request request = new Request.Builder()
            .url(url)
            .post(requestBody)
            .build();

    mClient.newCall(request).enqueue(new Callback() {
        @Override
        public void onFailure(Call call, IOException e) {
            call.cancel();
        }

        @Override
        public void onResponse(Call call, Response response) throws IOException {
            final String responseString = response.body().string();
            MainActivity.this.runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    mTextView.setText(responseString);
                }
            });
        }
    });
}

Post提交表单

final String url = "https://tieba.baidu.com/f"; 
RequestBody requestBody = new FormBody.Builder()
    .add("ie", "utf-8")
    .add("kw", "minecraft")
    .build();

Request request = new Request.Builder()
    .url(url)
    .post(requestBody)
    .build();

Post提交文件

public static final MediaType JPEG = MediaType.parse("image/jpeg");
File file = new File(Environment.getExternalStoragePublicDirectory(
    Environment.DIRECTORY_DCIM), "building.jpg");

RequestBody requestBody = RequestBody.create(JPEG, file);

Request request = new Request.Builder()
    .url(url)
    .post(requestBody)
    .build();

Post提交流

RequestBody requestBody = new RequestBody() {
    @Nullable
    @Override
    public MediaType contentType() {
        return null;
    }

    @Override
    public void writeTo(@NotNull BufferedSink bufferedSink) throws IOException {
        bufferedSink.writeUtf8(requestString);
    }
};

Request request = new Request.Builder()
    .url(url)
    .post(requestBody)
    .build();

使用Gson解析response

String url = "https://api.github.com/gists/c2a7c39532239ff261be";
class Gist{
    Map<String,GistFile> files;
}

class GistFile{
    String content;
}
Gson gson = new Gson();
Gist gist = gson.fromJson(response.body().charStream(),Gist.class);
for(Map.Entry<String,GistFile> entry:gist.files.entrySet()){
    Log.i(TAG,entry.getKey()+ " "+entry.getValue().content);
}

设置超时

OkHttpClient client = new OkHttpClient.Builder()
    .connectTimeout(3, TimeUnit.SECONDS)
    .build();

配置新client

.newBuilder()会返回一个配置相同的buidler

OkHttpClient client = new OkHttpClient.Builder()
        .connectTimeout(3, TimeUnit.SECONDS)
        .build();

OkHttpClient client2 = client.newBuilder()
        .connectTimeout(5, TimeUnit.SECONDS)
        .build();

拦截器

拦截器(Interceptor)是OkHttp的概念,也是核心功能。OkHttp有两种拦截器,分别是应用拦截器和网络拦截器。拦截器的主要目的在于重写requestresponse,可以在发出request前修改headers或body,也可以在收到response前修改headers或body。我们完全可以在用户收到reponse前将其修改成一个完全不一样的新response,这一功能使得我们可以进行后续的缓存策略修改或是使用gzip压缩requestBody等操作。应用拦截器在用户发出一次请求后的全过程中仅调用一次,而网络拦截器可能因为重定向等问题多次调用,例如有一次重定向就会调用两次。拦截器可以设置多个,并按添加顺序进行拦截。下图来自OkHttp文档:

拦截器图解

两种拦截器区别如下,参考OkHttp文档原文:

Application interceptors

  • Don’t need to worry about intermediate responses like redirects and retries.
  • Are always invoked once, even if the HTTP response is served from the cache.
  • Observe the application’s original intent. Unconcerned with OkHttp-injected headers like If-None-Match.
  • Permitted to short-circuit and not call Chain.proceed().
  • Permitted to retry and make multiple calls to Chain.proceed().
  • Can adjust Call timeouts using withConnectTimeout, withReadTimeout,withWriteTimeout.

Network Interceptors

  • Able to operate on intermediate responses like redirects and retries.
  • Not invoked for cached responses that short-circuit the network.
  • Observe the data just as it will be transmitted over the network.
  • Access to the Connection that carries the request.

个人翻译如下:

应用拦截器

  • 使用时不需要考虑例如重定向、重试等中转请求带来的影响
  • 全过程只拦截一次,即使拦截的response来自缓存
  • 可处理来自Applcation(参考拦截器图解)的本意。(例如no-cache)不涉及OkHttp的头部注入例如If-None-Match头部(这是在core注入的)
  • 可以不调用Chain.proceed()(例如return一个来自缓存的response,但不能return null
  • 可以重试和多次调用Chain.proceed()
  • 可通过withConnectTimeout,withReadTimeout, withWriteTimeout调整Call的超时时间.

网络拦截器

  • 可处理例如重定向、重试等中转请求
  • 不涉及缓存的调用
  • 可处理来自服务器的原始响应
  • 可对最终发出前的请求做读写
  • 实现拦截器

以上所说拦截器可对处于中间时期的requestresponse做修改,就是在chain.proceed(request)的前后完成的。
chain.proceed(request)会返回通过core或服务器处理后得到的response,这个方法会阻塞线程。

String url = "http://publicobject.com/helloworld.txt";
class LoggingInterceptor implements Interceptor {
    @Override public Response intercept(Interceptor.Chain chain) throws IOException {
        Request request = chain.request();

        //do something to rewrite request

        long t1 = System.nanoTime();
        Log.i(TAG,String.format("Sending request %s on %s%n%s",
                request.url(), chain.connection(), request.headers()));

        Response response = chain.proceed(request);

        long t2 = System.nanoTime();
        Log.i(TAG,String.format("Received response for %s in %.1fms%n%s",
                response.request().url(), (t2 - t1) / 1e6d, response.headers()));

        //do something to rewrite response

        return response;
    }
}
  • 设置应用拦截器

两种拦截器在实现的时候没有区别,充当那种拦截器取决于调用的方法是.addInterceptor()或是.addNetworkInterceptor().addInterceptor()表示设置应用拦截器,.addNetworkInterceptor()则是网络拦截器。

OkHttpClient client = new OkHttpClient.Builder()
    .addInterceptor(new LoggingInterceptor())
    .build();
  • 设置网络拦截器
OkHttpClient client = new OkHttpClient.Builder()
    .addNetworkInterceptor(new LoggingInterceptor())
    .build();

缓存处理

OkHttp默认不使用缓存,可以调用.cache()开启,但.cache()仅能设置缓存区大小和缓存读写的位置。Cache-Control头部是Http协议定义的,而OkHttp完全遵循Http协议,所以OkHttp的缓存策略是由请求头或响应头中的Cache-Control头部而定的。如果服务器返回的response已经带有Cache-Control响应头,在buidler中调用.cache()即可使用缓存。反之当收到的response没有设置Cache-Control时,可以在拦截器里手动添加,不同参数对应不同的缓存策略。不论response是否有Cache-Control,始终可以在发出request时添加例如Cache-control: no-cache来控制缓存使用与否。

启用缓存

String url = "http://publicobject.com/helloworld.txt";
int _10MB = 10 * 1024 * 1024;
File cacheDir = getCacheDir();
Cache cache = new Cache(cacheDir, _10MB);

OkHttpClient client = new OkHttpClient.Builder()
    .cache(cache)
    .build();

缓存策略

Http协议的Cache-Control的参数有很多,可设置多个参数,多个参数间用逗号分隔开。以下主要介绍其中几种的含义

  • Cache-Control: max-age=3600
    设置缓存过期时间为一小时。单位为秒,用于response

  • Cache-Control: max-stale=3600
    表示接受使用过期的缓存,最长时间为过期后的一小时,单位为秒。用于request

  • Cache-control: no-cache
    先不使用本地缓存,向服务器验证缓存是否过期后决定缓存使用与否,且并不取消本次response的缓存。用于request

  • Cache-control: no-store
    本次请求不缓存得到的response,也表示本次请求不读取缓存。用于request

  • Cache-control: only-if-cached
    仅尝试使用缓存。用于request

  • Cache-control: public
    表明响应可以被任何对象(包括:发送请求的客户端,代理服务器,等等)缓存,即使是通常不可缓存的内容。(例如该响应没有max-age指令或Expires消息头, 该响应对应的请求方法是 POST )

  • Cache-control: private
    表明响应只能被单个用户缓存,不能作为共享缓存(即代理服务器不能缓存它)。私有缓存可以缓存响应内容,比如:对应用户的本地浏览器。

  • Cache-Control: min-fresh=3600
    表示客户端希望获取一个能在指定的秒数内保持其最新状态的响应。

  • Cache-control: must-revalidate
    一旦资源过期(比如已经超过max-age),在成功向原始服务器验证之前,缓存不能用该资源响应后续请求。

此外与缓存有关的header可能还有ExpiresPragma,这里暂不介绍

  • 直接修改Cache-Control头部定义缓存策略
class CacheInterceptor implements Interceptor {
    @Override
    public Response intercept(Interceptor.Chain chain) throws IOException {
        Request request = chain.request();

        request = request.newBuilder()
                .header("Cache-Control", "max-stale=3600")
                .build();

        return chain.proceed(request);
    }
}
Interceptor interceptor = new CacheInterceptor();

mClient = new OkHttpClient.Builder()
        .cache(cache)
        .addInterceptor(interceptor)
        .build();
  • 使用CacheControl.Builder()定义缓存策略

CacheControl类只能在拦截器中使用,其实质只是在请求头或响应头为Cache-Control添加不同的参数而已,并没有其他作用

class ForceCacheInterceptor implements Interceptor {
    @Override
    public Response intercept(Interceptor.Chain chain) throws IOException {
        Request request = chain.request();

        CacheControl cacheControl = new CacheControl.Builder()
                .onlyIfCached()
                .build();

        request = request.newBuilder()
                .cacheControl(cacheControl)
                .build();

        return chain.proceed(request);
    }
}
Interceptor interceptor = new ForceCacheInterceptor();

mClient = new OkHttpClient.Builder()
        .cache(cache)
        .addInterceptor(interceptor)
        .build();

CacheControl.Builder的常用方法

  • .maxAge(3600, TimeUnit.SECONDS);
  • .maxStale(3600, TimeUnit.SECONDS);
  • .noCache();
  • .noStore();
  • .onlyIfCached();

含义参考Cache-Control参数介绍

  • 使用CacheControl的伴生对象定义缓存策略

CacheControl的伴生对象有两个,CacheControl.FORCE_CACHECacheControl.FORCE_NETWORK,分别表示强制使用缓存和强制使用网络。

public class ForceNetworkInterceptor implements Interceptor {
    @Override
    public Response intercept(Chain chain) throws IOException {
        Request.Builder builder = chain.request().newBuilder();
        if (!NetworkUtils.internetAvailable()) {
            builder.cacheControl(CacheControl.FORCE_NETWORK);
        }
        
        return chain.proceed(builder.build());
    }
}
Interceptor interceptor = new ForceNetworkInterceptor();

mClient = new OkHttpClient.Builder()
        .cache(cache)
        .addInterceptor(interceptor)
        .build();

CacheControl.FORCE_CACHE的本质是将Cache-Control请求头设为"max-stale=2147483647, only-if-cached"
重新查看上图就可以知道,Application发出的请求是被OkHttp core处理,而OkHttp core发出的请求将提交给服务器,如果我们希望本次请求强制使用缓存,就应该使用应用拦截器而不是网络拦截器,这段请求头告诉OkHttp本次请求仅使用缓存的响应。

CacheControl.FORCE_NETWORK的本质是将Cache-Control请求头设为"no-cache"。与FORCE_CACHE同理,它也应该使用应用拦截器,这段请求头告诉OkHttp本次请求仅使用来自网络的响应。

缓存流程

决定一次请求是否使用缓存的流程,主要的几个步骤如下(任何一步决定使用网络时将不再检查后续步骤)

  1. 检查response是否包含DateExpiresLast-ModifiedETagAge这些响应头。若都不包含则使用网络。

  2. 检查requestCache-Control请求头,"no-cache"使用网络,"only-if-cached"使用缓存。

  3. 检查responseETag响应头,若存在则使用网络,并且本次请求会带有与ETag值相同的If-None-Match请求头。若实际数据没有变化,服务器处理后会给出304 Not Modified状态码,表示资源没有修改,并且不会返回body,指示客户端使用缓存,所以此时OkHttp也会使用缓存。

  4. 检查responseLast-Modified响应头,若存在则使用网络,并且本次请求会带有与Last-Modified值相同的If-Modified-Since请求头。后续同ETag

  5. 检查responseDate响应头,若存在则使用网络,并且本次请求会带有与Date值相同的If-Modified-Since请求头。后续同ETag

  6. 检查responsemax-age,如果过期则使用网络,否则使用缓存。但也可能因为其他参数如max-stale等影响最终计算结果。

缓存总结

一次完整的涉及缓存的网络请求大致如下图,其中成功的结果有两个(绿框),分别是使用缓存和使用服务器的新数据。在Force cache后找不到缓存就会失败(红框)。从初始阶段向下看,第一步判断是否调用.cache()开启了缓存功能。第二步检查之前是否缓存过,两者任意一者不满足则使用网络。第三步判断是否需要验证,与ETag等有关,存在则使用网络向服务器验证,服务器若返回304则response完全从缓存中取出。这步操作同普通请求一样,可能涉及无网络问题。当无网络时可以Force cache进行处理,最后则是成功或失败时的异常处理。下图来自Medium

网络请求完整流程图
  • 当存储缓存时

如果此时要修改response的头部,应该使用网络拦截器修改response

只要在构建client的时候调用了.cache(),那么通过这个client得到的响应一定会被缓存,但之后不一定会被使用。存储缓存时与Cache-Control请求头或响应头都无关,Cache-Control只有当读取缓存时才会用到。

  • 当读取缓存时

应该使用应用拦截器修改request

想要强制使用缓存,有以下3种方式:

  • 使用CacheControl.FORCE_CACHE
  • 调用CacheControl.Builder().onlyIfCached()
  • 直接添加Cache-Control: "only-if-cached"请求头

但如果缓存不存在,这次请求就会失败并抛出IOException,并且得到一个带有504 Gateway Timeout的response

想要强制使用网络,有以下3种方式:

  • 使用CacheControl.FORCE_NETWORK
  • 调用CacheControl.Builder().noCache()
  • 直接添加Cache-Control: "no-cache"请求头

如果你在请求头没有指定任何有关缓存的参数,OkHttp将按照缓存中response的数个响应头进行不同的处理,可能使用缓存,也可能向服务器验证response后决定是否使用缓存,或是进行一次普通的请求。

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