OkHttp使用详解

作者: 冰鉴IT | 来源:发表于2016-12-18 18:02 被阅读2553次

    今天学习了一下OkHttp,在这里做个总结,希望可以帮助到有需要的人,好了,废话不多说,进入正题。

    一、OkHttp介绍

    OkHttp是一个优秀的网络请求框架,可能一说到网络请求框架,可能很多人都会想到volley,volley是一个Google提供的网络请求框架,我的博客里也有一篇专门介绍volley的博客,博客地址在此Android网络请求 ------ Volley的使用 那么既然Google提供了网络请求的框架,我们为什么还要使用OkHttp呢,原来是volley是要依靠HttpCient的,而Google在Android6.0的SDK中去掉了HttpCient,所以OkHttp就开始越来越受大家的欢迎.

    今天我们主要介绍OkHttpGet请求、Post请求、上传下载文件上传下载图片等功能

    当然在开始之前,我们还要先在项目中添加OkHttp的依赖库,至于怎么在AndroidStudio中给项目添加OkHTTP依赖,这里将不再赘述。另外,OkHttp中使用了建造者模式,如果对建造者模式不了解,可以看看这篇博客设计模式之建造者模式**

    添加OkHttp的依赖

    在对应的Module的gradle中添加
    compile 'com.squareup.okhttp3:okhttp:3.5.0'   
    然后同步一下项目即可
    

    二、OkHttp进行Get请求

    使用OkHttp进行Get请求只需要四步即可完成。

    1 . 拿到OkHttpClient对象

    OkHttpClient client = new OkHttpClient();
    

    2 . 构造Request对象

    Request request = new Request.Builder()
                    .get()
                    .url("https:www.baidu.com")
                    .build();
    

    这里我们采用建造者模式和链式调用指明是进行Get请求,并传入Get请求的地址

    如果我们需要在get请求时传递参数,我们可以以下面的方式将参数拼接在url之后

    https:www.baidu.com?username=admin&password=admin
    

    3 . 将Request封装为Call

    Call call = client.newCall(request);
    

    4 . 根据需要调用同步或者异步请求方法

    //同步调用,返回Response,会抛出IO异常
    Response response = call.execute();
    
    //异步调用,并设置回调函数
    call.enqueue(new Callback() {
        @Override
        public void onFailure(Call call, IOException e) {
            Toast.makeText(OkHttpActivity.this, "get failed", Toast.LENGTH_SHORT).show();
        }
    
        @Override
        public void onResponse(Call call, final Response response) throws IOException {
            final String res = response.body().string();
            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    contentTv.setText(res);
                }
            });
        }
    });
    

    第四步有一些需要注意的地方

    1. 同步调用会阻塞主线程,一般不适用
    2. 异步调用的回调函数是在子线程,我们不能在子线程更新UI,需要借助于runOnUiThread()方法或者Handler来处理

    是不是以为上面就结束了,对的,OkHttp的Get请求步骤就这么4步,但是当你试图打开应用加载数据,可是发现并没有加载到数据,这是一个简单但是我们常犯的错误.
    在AndroidManifest.xml中加入联网权限

    <uses-permission android:name="android.permission.INTERNET" />
    

    三、OkHttp进行Post请求提交键值对

    使用OkHttp进行Post请求和进行Get请求很类似,只需要五步即可完成。

    1 . 拿到OkHttpClient对象

    OkHttpClient client = new OkHttpClient();
    

    2 . 构建FormBody,传入参数

    FormBody formBody = new FormBody.Builder()
                    .add("username", "admin")
                    .add("password", "admin")
                    .build();
    

    3 . 构建Request,将FormBody作为Post方法的参数传入

    final Request request = new Request.Builder()
                    .url("http://www.jianshu.com/")
                    .post(formBody)
                    .build();
    

    4 . 将Request封装为Call

    Call call = client.newCall(request);
    

    5 . 调用请求,重写回调方法

    call.enqueue(new Callback() {
        @Override
        public void onFailure(Call call, IOException e) {
            Toast.makeText(OkHttpActivity.this, "Post Failed", Toast.LENGTH_SHORT).show();
        }
    
        @Override
        public void onResponse(Call call, Response response) throws IOException {
            final String res = response.body().string();
            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    contentTv.setText(res);
                }
            });
        }
    });
    

    经过上面的步骤一个post请求就完成了,当然上面的url参数和需要传入的参数大家就要根据实际情况来传入,你会发现get和post请求的步骤非常像。

    四、OkHttp进行Post请求提交字符串

    如果你已经掌握了上面的两种基本的步骤,那下面的内容就比较简单了

    上面我们的post的参数是通过构造一个FormBody通过键值对的方式来添加进去的,其实post方法需要传入的是一个RequestBody对象,FormBodyRequestBody的子类,但有时候我们常常会遇到要传入一个字符串的需求,比如客户端给服务器发送一个json字符串,那这种时候就需要用到另一种方式来构造一个RequestBody如下所示:

    RequestBody requestBody = RequestBody.create(MediaType.parse("text/plain;charset=utf-8"), "{username:admin;password:admin}");
    

    上面的MediaType我们指定传输的是纯文本,而且编码方式是utf-8,通过上面的方式我们就可以向服务端发送json字符串啦。

    注:关于MidiaType的类型你可以百度搜索mime type查看相关的内容,这里不再赘述

    五、OkHttp进行Post请求上传文件

    理解了上面一个,下面这个就更简单了,这里我们以上传一张图片为例,当然你也可以上传一个txt什么的文件,都是可以的

    其实最主要的还是构架我们自己的RequestBody,如下图构建

    File file = new File(Environment.getExternalStorageDirectory(), "1.png");
    if (!file.exists()){
        Toast.makeText(this, "文件不存在", Toast.LENGTH_SHORT).show();
    }else{
        RequestBody requestBody2 = RequestBody.create(MediaType.parse("application/octet-stream"), file);
    }
    

    这里我们将手机SD卡根目录下的1.png图片进行上传。代码中的application/octet-stream表示我们的文件是任意二进制数据流,当然你也可以换成更具体的image/png

    注:最后记得最重要的一点:添加存储卡写权限,在AndroidManifest.xml文件中添加如下代码:

    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    

    六、OkHttp进行Post请求提交表单

    我们在网页上经常会遇到用户注册的情况,需要你输入用户名,密码,还有上传头像,这其实就是一个表单,那么接下来我们看看如何利用OkHttp来进行表单提交。经过上面的学习,大家肯定也懂,主要的区别就在于构造不同的RequestBody传递给post方法即可.

    由于我们使用的是OkHttp3所以我们还需要再导入一个包okio.jar才能继续下面的内容,我们需要在模块的Gradle文件中添加如下代码,然后同步一下项目即可

    compile 'com.squareup.okio:okio:1.11.0'
    

    这里我们会用到一个MuiltipartBody,这是RequestBody的一个子类,我们提交表单就是利用这个类来构建一个RequestBody,下面的代码我们会发送一个包含用户民、密码、头像的表单到服务端

    File file = new File(Environment.getExternalStorageDirectory(), "1.png");
    if (!file.exists()){
        Toast.makeText(this, "文件不存在", Toast.LENGTH_SHORT).show();
        return;
    }
    RequestBody muiltipartBody = new MultipartBody.Builder()
            //一定要设置这句
            .setType(MultipartBody.FORM)
            .addFormDataPart("username", "admin")//
            .addFormDataPart("password", "admin")//
            .addFormDataPart("myfile", "1.png", RequestBody.create(MediaType.parse("application/octet-stream"), file))
            .build();
    

    上面添加用户民和密码的部分和我们上面学习的提交键值对的方法很像,我们关键要注意以下几点:

    (1)如果提交的是表单,一定要设置setType(MultipartBody.FORM)这一句

    (2)提交的文件addFormDataPart()的第一个参数,就上面代码中的myfile就是类似于键值对的键,是供服务端使用的,就类似于网页表单里面的name属性,例如下面:

    <input type="file" name="myfile">
    

    (3)提交的文件addFormDataPart()的第二个参数文件的本地的名字,第三个参数是RequestBody,里面包含了我们要上传的文件的路径以及MidiaType

    (4)记得在AndroidManifest.xml文件中添加存储卡读写权限

    七、OkHttp进行get请求下载文件

    除了上面的功能,我们最常用的功能该有从网路上下载文件,我们下面的例子将演示下载一个文件存放在存储卡根目录,从网络下载一张图片并显示到ImageView中

    1 . 从网络下载一个文件(此处我们以下载一张图片为例)

    public void downloadImg(View view){
        OkHttpClient client = new OkHttpClient();
        final Request request = new Request.Builder()
                .get()
                .url("https://www.baidu.com/img/bd_logo1.png")
                .build();
        Call call = client.newCall(request);
        call.enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
                Log.e("moer", "onFailure: ");;
            }
    
            @Override
            public void onResponse(Call call, Response response) throws IOException {
                //拿到字节流
                InputStream is = response.body().byteStream();
    
                int len = 0;
                File file  = new File(Environment.getExternalStorageDirectory(), "n.png");
                FileOutputStream fos = new FileOutputStream(file);
                byte[] buf = new byte[128];
    
                while ((len = is.read(buf)) != -1){
                    fos.write(buf, 0, len);
                }
    
                fos.flush();
                //关闭流
                fos.close();
                is.close();
            }
        });
    }
    

    你会发现步骤与进行一般的Get请求差别不大,唯一的区别在于我们在回调函数中所做的事,我们拿到了图片的字节流,然后保存为了本地的一张图片

    2 . 从网络下载一张图片并设置到ImageView中

    其实学会了上面的步骤你完全可以将图片下载到本地后再设置到ImageView中,当然下面是另一种方法
    这里我们使用BitmapFactorydecodeStream将图片的输入流直接转换为Bitmap,然后设置到ImageView中,下面只给出onResponse()中的代码.

    @Override
    public void onResponse(Call call, Response response) throws IOException {
        InputStream is = response.body().byteStream();
    
        final Bitmap bitmap = BitmapFactory.decodeStream(is);
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                imageView.setImageBitmap(bitmap);
            }
        });
    
        is.close();
    }
    

    八、给文件的上传和下载加上进度条

    我们一直都说,用户体验很重要,当我们下载的文件比较大,而网速又比较慢的时候,如果我们只是在后台下载或上传,没有给用户显示一个进度,那将是非常差的用户体验,下面我们就将简单做一下进度的显示,其实非常简单的

    1 . 显示文件下载进度

    这里只是演示,我只是把进度显示在一个TextView中,至于进度的获取当然是在我们的回调函数onResponse()中去获取

    (1)使用response.body().contentLength()拿到文件总大小

    (2)在while循环中每次递增我们读取的buf的长度

    @Override
    public void onResponse(Call call, Response response) throws IOException {
        InputStream is = response.body().byteStream();
        long sum = 0L;
        //文件总大小
        final long total = response.body().contentLength();
        int len = 0;
        File file  = new File(Environment.getExternalStorageDirectory(), "n.png");
        FileOutputStream fos = new FileOutputStream(file);
        byte[] buf = new byte[128];
    
        while ((len = is.read(buf)) != -1){
            fos.write(buf, 0, len);
            //每次递增
            sum += len;
    
            final long finalSum = sum;
            Log.d("pyh1", "onResponse: " + finalSum + "/" + total);
            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    //将进度设置到TextView中
                    contentTv.setText(finalSum + "/" + total);
                }
            });
        }
        fos.flush();
        fos.close();
        is.close();
    }
    

    2 . 显示文件上传进度

    对于上传的进度的处理会比较麻烦,因为具体的上传过程是在RequestBody中由OkHttp帮我们处理上传,而且OkHttp并没有给我们提供上传进度的接口,这里我们的做法是自定义类继承RequestBody,然后重写其中的方法,将其中的上传进度通过接口回调暴露出来供我们使用。

    public class CountingRequestBody extends RequestBody {
        //实际起作用的RequestBody
        private RequestBody delegate;
        //回调监听
        private Listener listener;
    
        private CountingSink countingSink;
    
        /**
         * 构造函数初始化成员变量
         * @param delegate
         * @param listener
         */
        public CountingRequestBody(RequestBody delegate, Listener listener){
            this.delegate = delegate;
            this.listener = listener;
        }
        @Override
        public MediaType contentType() {
            return delegate.contentType();
        }
    
        @Override
        public void writeTo(BufferedSink sink) throws IOException {
            countingSink = new CountingSink(sink);
            //将CountingSink转化为BufferedSink供writeTo()使用
            BufferedSink bufferedSink = Okio.buffer(countingSink);
            delegate.writeTo(bufferedSink);
            bufferedSink.flush();
        }
    
        protected final class CountingSink extends ForwardingSink{
            private long byteWritten;
            public CountingSink(Sink delegate) {
                super(delegate);
            }
    
            /**
             * 上传时调用该方法,在其中调用回调函数将上传进度暴露出去,该方法提供了缓冲区的自己大小
             * @param source
             * @param byteCount
             * @throws IOException
             */
            @Override
            public void write(Buffer source, long byteCount) throws IOException {
                super.write(source, byteCount);
                byteWritten += byteCount;
                listener.onRequestProgress(byteWritten, contentLength());
            }
        }
    
        /**
         * 返回文件总的字节大小
         * 如果文件大小获取失败则返回-1
         * @return
         */
        @Override
        public long contentLength(){
            try {
                return delegate.contentLength();
            } catch (IOException e) {
                return -1;
            }
        }
    
        /**
         * 回调监听接口
         */
        public static interface Listener{
            /**
             * 暴露出上传进度
             * @param byteWritted  已经上传的字节大小
             * @param contentLength 文件的总字节大小
             */
            void onRequestProgress(long byteWritted, long contentLength);
        }
    }
    

    上面的代码注释非常详细,这里不再解释,然后我们在写具体的请求时还需要做如下变化

    File file = new File(Environment.getExternalStorageDirectory(), "1.png");
    if (!file.exists()){
        Toast.makeText(this, "文件不存在", Toast.LENGTH_SHORT).show();
    }else{
        RequestBody requestBody2 = RequestBody.create(MediaType.parse("application/octet-stream"), file);
    }
    
    //使用我们自己封装的类
    CountingRequestBody countingRequestBody = new CountingRequestBody(requestBody2, new CountingRequestBody.Listener() {
        @Override
        public void onRequestProgress(long byteWritted, long contentLength) {
            //打印进度
            Log.d("pyh", "进度 :" + byteWritted + "/" + contentLength);
        }
    });
    

    上面其实就是在原有的RequestBody上包装了一层,最后在我们的使用中在post()方法中传入我们的CountingRequestBody对象即可。

    九、后记

    以上就是一些OkHttp常用的总结,希望可以帮助到需要的人

    相关文章

      网友评论

      • 放纵的卡尔:写的很好,特别是最后面的上传,下载!条理清晰,nice!
      • smartapple:这是我读到的写的最清晰的介绍。顺便请问一下 可以带着讲讲 一步一步来 如何封装这个东西吗?
        smartapple:@冰鉴IT 希望尽快出来 跟着大神的脚步学习封装这个东西 之前一直就是按照这篇文章写的方式简单使用 或者直接使用别人封装好的
        冰鉴IT: @四条眼的小胖子 谢谢肯定,封装是我下一步的计划,到时候会发出来的😁
      • SmartSean:如果能结合异步请求框架就好了:smiley:
        冰鉴IT: @Coding_css 我说的是网络请求的异步okhttp已经帮我们实现了,你说的RXjava是整个事件操作的异步😁
        SmartSean:@冰鉴IT 那能指导下为什么还要用RxJava来执行异步请求么?谢谢
        冰鉴IT: @Coding_css okhttp本身就可以提供异步请求

      本文标题:OkHttp使用详解

      本文链接:https://www.haomeiwen.com/subject/etkzmttx.html