Android网络请求开源库

Android网络编程(四)OKHttp使用及封装

2019-01-07  本文已影响164人  zskingking

前言

在当前的市场环境中,已经很少看到不需要连接网络的APP,网络也可以说是一个APP的"灵魂",所以对于一个Android开发者来说是很有必要学好网络这一块的,个人认为它的重要性是可以和View相比的。而OKHttp可以说是目前Android最流行的网络请求框架,所以我准备开几篇文章详细讲解OKHttp,包括基本使用和封装以及源码的解析。

1 Okhttp概述

在真正讲述Okhttp前我先澄清几个概念,在当前Android开发环境中,存在很多优秀的网络请求方案,从早期的HttpUrlConnection、HttpClient到Volley、AsyncHttp再到Okhttp、Retrofit,很多人问这些框架到底哪个最优秀,笔者个人认为是Okhttp,又有人问了,Retrofit现在不也是很火吗,那它跟Okhttp哪个更优秀呢?其实这二者是不能进行比较的,因为这两个框架根本就不在同一个量级,Okhttp是完全基于HTTP实现的一套网络框架,而Retrofit是基于Okhttp的,说白了Retrofit就是对Okhttp进行了一次封装,跟Okhttp相比的应该是HttpUrlConnection和HttpClient,这三者都是基于HTTP实现的网络框架,属同一量级,而Volley、AsyncHttp、Retrofit只是对以上三种网络框的一种封装,所以笔者认为最好的HTTP请求框架是Okhttp,最好用的网络框架是Retrofit。

Okhttp是由著名的Square公司所贡献的一个开源网络请求框架,它存在的优点有以下几种:

  • 支持SPDY
  • 对同一主机共享Socket
  • 对TCP连接进行复用
  • 对数据进行缓存
  • 自动处理GZip压缩

Okhttp相比于其他网络框架优势还是挺明显的,今天我先来带大家了解一下Okhttp的基本使用。

2 基本使用

使用前首先进行依赖加入:

    compile 'com.squareup.okhttp3:okhttp:3.8.1'
    compile 'com.squareup.okio:okio:1.7.0'
构建OkhttpClient
//缓存的文件夹
        File fileCache = new File(context.getExternalCacheDir(),"response");
        int cacheSize = 10*1024*1024;//缓存大小为10M
        Cache cache = new Cache(fileCache, cacheSize);
        //进行OkHttpClient的一些设置
        OkHttpClient okHttpClient = new OkHttpClient.Builder()
                .connectTimeout(10,TimeUnit.SECONDS)
                .readTimeout(30, TimeUnit.SECONDS)
                .writeTimeout(10,TimeUnit.SECONDS)
                .cache(cache)//设置缓存
                .build();

OkHttpClient 一般在一个应用中构建一次即可,所以推荐大家使用单例设计模式,同时OkHttpClient 可构建多种特性,比如上面的读取时间、缓存,还可以构建DNS、代理等等,感兴趣的同学可自行了解。

GET同步请求
Request request = new Request.Builder()
                .get()//请求方法
                .url("http://www.baidu.com")//url
                .build();
        final Call call = mOkHttpClient.newCall(request);
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Response response = call.execute();
                    if(response.isSuccessful()){
                        Log.i("okhttp",response.body().string());
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }

            }
        }).start();

由于Android中不能再主线程中进行网络IO操作,所以同步请求需要在子线程中进行。
请求结果为百度主页的html信息。

GET异步请求

OKHttp也给开发者提供了异步请求方法

Request request = new Request.Builder()
                .get()//请求方法
                .url("http://www.baidu.com")//url
                .build();
        Call call = mOkHttpClient.newCall(request);
        call.enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
                Log.i(TAG,"error  "+e.toString() +"threadName:"+Thread.currentThread().getName());
            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {
                Log.i(TAG,"success  "+response.body().string() +"threadName:"+Thread.currentThread().getName());
            }
        });

打印结果

百度主页html.....
threadName:OkHttp http://www.baidu.com/...

我们看到响应方法是在子线程中回调的,所以大家如果需要在主线程中操作响应结果需要进行一次线程切换。

POST表单

POST也分为同步请求和异步请求,使用方式和GET完全一致,为了减少篇幅以下例子我都只进行异步请求。

FormBody.Builder builder = new FormBody.Builder();
        //将表单信息添加到FormBody中
        builder.add("key1","value1");
        builder.add("key2","value2");
        builder.add("key3","value3");
        //构建请求body
        RequestBody body = builder.build();
        Request request = new Request.Builder()
                .url(url)
                .post(body)
                .build();
        Call call = mOkHttpClient.newCall(request);
        call.enqueue(new Callback() {
            @Override
            public void onFailure(Call call, final IOException e) {
                Log.i(TAG,"error:"+e.toString());
            }

            @Override
            public void onResponse(Call call, final Response response) throws IOException {
                Log.i(TAG,"success:"+response.body());
            }
        });

通过FormBody.Builder添加表单信息,最后build()构建请求body,实际的键值对跟url根据自己需求传入即可。

POST传JSON
MediaType mediaType = MediaType.parse("application/json;charset=utf-8");
        //传入mediaType和json字符串
        RequestBody body = RequestBody.create(mediaType,json);
        Request request = new Request.Builder()
                .url(url)
                .post(body)
                .build();
        Call call = mOkHttpClient.newCall(request);
        call.enqueue(new Callback() {
            @Override
            public void onFailure(Call call, final IOException e) {
                Log.i(TAG,"error:"+e.toString());
            }

            @Override
            public void onResponse(Call call, final Response response) throws IOException {
                Log.i(TAG,"error:"+response.body());
            }
        });

将MediaType 设置为json,自己封装好准备上传的json字符串即可完成请求

GET下载文件

我们通过GET请求下载一张图片并增加进度提示

Request request = new Request.Builder()
                .url(url)
                .get()
                .build();
        Call call = mOkHttpClient.newCall(request);
        call.enqueue(new Callback() {
            @Override
            public void onFailure(Call call, final IOException e) {
                Log.i(TAG,"error:"+e.toString());
            }

            @Override
            public void onResponse(Call call, final Response response) throws IOException {
                //下载文件进度条可以直接在onResponse中实现
                InputStream is = response.body().byteStream();
                //文件的总大小(单位字节)
                final long contentLength = response.body().contentLength();

                long sum = 0;//当前下载到的字节量
                File file  = new File(Environment.getExternalStorageDirectory(), "girl.png");
                FileOutputStream fos;
                try {
                    fos = new FileOutputStream(file);
                    //数组越小进度的密度越高
                    byte[] bytes = new byte[128];
                    int len = 0;
                    while ((len = is.read(bytes))!=-1){
                        fos.write(bytes,0,len);
                        sum+=len;
                        Log.i(TAG,"progress="+(float)sum/contentLength);
                    }
                } catch (FileNotFoundException e) {
                    e.printStackTrace();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        });

首先获取到字节流和总长度,然后创建一个图片文件,最后字节写入到图片文件中。在循环写入字节的过程中可以做一个进度提示,byte[]长度越小进度的密度越高。来看一下我的本地结果:


Taylor .PNG

可爱的霉霉被我保存在了本地。

POST上传文件

我们来实现一个基于OKhttp实现一个文件上传功能并显示上传进度。
上传文件的进度显示不像下载中那么简单,我们需要重写RequestBody才能实现,先来看一下代码:

public class ProgressRequestBody extends RequestBody {

    private RequestBody mBody;
    private ProgressListener mListener;
    private ProgressSink mProgressSink;
    private BufferedSink mBufferedSink;
    public ProgressRequestBody(RequestBody body, ProgressListener listener){
        this.mBody = body;
        mListener = listener;
    }

    @Override
    public MediaType contentType() {
        return mBody.contentType();
    }

    //写入数据的方法
    @Override
    public void writeTo(BufferedSink sink) throws IOException {
        //将Sink重新构造
        mProgressSink = new ProgressSink(sink);
        //创建输出流体系
        mBufferedSink = Okio.buffer(mProgressSink);
        //进行流输出操作
        mBody.writeTo(mBufferedSink);
        mBufferedSink.flush();
    }

    @Override
    public long contentLength(){
        try {
            return mBody.contentLength();
        } catch (IOException e) {
            return -1;
        }
    }

    class ProgressSink extends ForwardingSink {
        private long byteWrite;//当前写入的字节
        public ProgressSink(Sink delegate) {
            super(delegate);
        }

        @Override
        public void write(Buffer source, long byteCount) throws IOException {
            //必须执行父类方法,否则无法上传
            super.write(source, byteCount);
            byteWrite+= byteCount;
            if(mListener!=null) {
                //更新进度
                mListener.onProgress(byteWrite, contentLength());
            }
        }
    }

    public interface ProgressListener{
        void onProgress(long byteWrite, long contentLength);
    }

ProgressSink类中的write()方法中累加已上传的字节量,然后通过接口mListener进行进度的更新。然后在ProgressRequestBody中的writeTo()方法中进行流的写操作,BufferedSink属于OKhttp封装的IO流体系,在之后OKHttp源码分析的文章我会提到,此处大家只需了解即可。写了这么多,其实就只是加了一个进度的回调接口。

上传代码实现:

        RequestBody requestBody = RequestBody.//表示任意二进制流
                //参数:MediaType类型,需要上传的文件
                create(MediaType.parse("application/octet-stream"), file);
        //因为是文件参数混合上传,所以要分开构建
        MultipartBody.Builder builder = new MultipartBody.Builder();
        builder.setType(MultipartBody.FORM);
        //params为表单键值对Map
        if(params!=null) {
            for (Map.Entry<String, String> entry : params.entrySet()) {
                //将键值对添加到表单中
                builder.addFormDataPart(entry.getKey(), entry.getValue());
            }
        }
        RequestBody multipartBody = builder
                //key需要服务器提供,相当于键值对的键
                .addFormDataPart("image",file.getName(),requestBody)
                .build();
        ProgressRequestBody countingRequestBody
                = new ProgressRequestBody(multipartBody, new ProgressRequestBody.ProgressListener() {
            @Override
            public void onProgress(long byteWrite, long contentLength) {
                //更新进度
                Log.i(TAG,"progress:"+byteWrite/contentLength);
            }
        });
        Request request = new Request.Builder()
                .url(url)
                .post(countingRequestBody)
                .build();
        Call call = mOkHttpClient.newCall(request);

        call.enqueue(new Callback() {
            @Override
            public void onFailure(Call call, final IOException e) {
                Log.i(TAG,"error:"+e.toString());
            }

            @Override
            public void onResponse(Call call, final Response response) throws IOException {
                Log.i(TAG,"success "+response);
            }
        });

一般上传文件的实际需求中需要附加几个键值对参数,所以代码中我实现了键值对和文件混合上传,其他地方注释写的很清楚,就不多做解释。需要注意的是这些回调方法都是在子线程中进行的,如果要进行UI交互需要切换到主线程。

取消请求操作

Okhttp中也支持请求的取消操作,代码如下:

    //根据tag取消单个请求
    //最终的取消时通过拦截器RetryAndFollowUpInterceptor进行的
    public void cancel(Call call){
        //queuedCalls()代表所有准备运行的异步任务
        for(Call dispatcherCal1:mOkHttpClient.dispatcher().queuedCalls()){
            if(call.request().tag().equals(call.request().tag())){
                call.cancel();
            }
        }
        //runningCalls()代表所有正在运行的任务(包括同步和异步)
        for(Call dispatcherCal1:mOkHttpClient.dispatcher().runningCalls()){
            if(call.request().tag().equals(call.request().tag())){
                call.cancel();
            }
        }

    }

    //取消全部请求
    public void cancelAll(){
        mOkHttpClient.dispatcher().cancelAll();
    }

请求取消的原理我会在OKHttp源码分析的文章中详细讲解,此处大家会用即可。

Okhttp封装

OKhttp的书写还是挺繁琐的,所以真正使用的时候我需要对其进行封装以便使用,笔者也对OKHttp进行了进行了一个简单的封装,由于代码较长所以就托管在了Github,感兴趣的同学可以进去了解一下。

总结

由于Retrofit的简洁、低耦合并能配合RxJava使用,所以很少有开发者去直接使用OKHttp,但毕竟Retrofit是完全基于OKHttp的,所以还是有必要去掌握一下OKHttp的使用的。本篇文章内容较为简单,旨在让不懂OKHttp的同学对其有一个基本的了解,以方便于我讲述后面的OKHttp源码分析,

上一篇下一篇

猜你喜欢

热点阅读