OkHttp使用详解

chengtd 8年前
   <p>今天学习了一下 OkHttp ,在这里做个总结,希望可以帮助到有需要的人,好了,废话不多说,进入正题。</p>    <h3>一、OkHttp介绍</h3>    <p>OkHttp是一个优秀的网络请求框架,可能一说到网络请求框架,可能很多人都会想到 volley , volley 是一个Google提供的网络请求框架,我的博客里也有一篇专门介绍 volley 的博客,博客地址在此 Android网络请求 ------ Volley的使用 那么既然Google提供了网络请求的框架,我们为什么还要使用 OkHttp 呢,原来是 volley 是要依靠 HttpCient 的,而Google在 Android6.0 的SDK中去掉了 HttpCient ,所以 OkHttp 就开始越来越受大家的欢迎.</p>    <p>今天我们主要介绍 OkHttp 的 Get 请求、 Post 请求、 上传下载文件 、 上传下载图片等功能 。</p>    <p>当然在开始之前,我们还要先在项目中添加 OkHttp 的依赖库,至于怎么在AndroidStudio中给项目添加OkHTTP依赖,这里将不再赘述。另外,OkHttp中使用了建造者模式</p>    <p>添加OkHttp的依赖</p>    <pre>  <code class="language-java">在对应的Module的gradle中添加  compile 'com.squareup.okhttp3:okhttp:3.5.0'     然后同步一下项目即可</code></pre>    <h3>二、OkHttp进行Get请求</h3>    <p>使用OkHttp进行 Get 请求只需要四步即可完成。</p>    <p>1 . 拿到OkHttpClient对象</p>    <pre>  <code class="language-java">OkHttpClient client = new OkHttpClient();</code></pre>    <p>2 . 构造Request对象</p>    <pre>  <code class="language-java">Request request = new Request.Builder()                  .get()                  .url("https:www.baidu.com")                  .build();</code></pre>    <p>这里我们采用建造者模式和链式调用指明是进行Get请求,并传入Get请求的地址</p>    <p>如果我们需要在get请求时传递参数,我们可以以下面的方式将参数拼接在url之后</p>    <pre>  <code class="language-java">https:www.baidu.com?username=admin&password=admin</code></pre>    <p>3 . 将Request封装为Call</p>    <pre>  <code class="language-java">Call call = client.newCall(request);</code></pre>    <p>4 . 根据需要调用同步或者异步请求方法</p>    <pre>  <code class="language-java">//同步调用,返回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);              }          });      }  });</code></pre>    <p>第四步有一些需要注意的地方</p>    <ol>     <li>同步调用会阻塞主线程,一般不适用</li>     <li>异步调用的回调函数是在子线程,我们不能在子线程更新UI,需要借助于 runOnUiThread() 方法或者 Handler 来处理</li>    </ol>    <p>是不是以为上面就结束了,对的,OkHttp的Get请求步骤就这么4步,但是当你试图打开应用加载数据,可是发现并没有加载到数据,这是一个简单但是我们常犯的错误.</p>    <p>在AndroidManifest.xml中加入联网权限</p>    <pre>  <code class="language-java"><uses-permission android:name="android.permission.INTERNET" /></code></pre>    <h3>三、OkHttp进行Post请求提交键值对</h3>    <p>使用OkHttp进行 Post 请求和进行 Get 请求很类似,只需要五步即可完成。</p>    <p>1 . 拿到OkHttpClient对象</p>    <pre>  <code class="language-java">OkHttpClient client = new OkHttpClient();</code></pre>    <p>2 . 构建FormBody,传入参数</p>    <pre>  <code class="language-java">FormBody formBody = new FormBody.Builder()                  .add("username", "admin")                  .add("password", "admin")                  .build();</code></pre>    <p>3 . 构建Request,将FormBody作为Post方法的参数传入</p>    <pre>  <code class="language-java">final Request request = new Request.Builder()                  .url("http://www.jianshu.com/")                  .post(formBody)                  .build();</code></pre>    <p>4 . 将Request封装为Call</p>    <pre>  <code class="language-java">Call call = client.newCall(request);</code></pre>    <p>5 . 调用请求,重写回调方法</p>    <pre>  <code class="language-java">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);              }          });      }  });</code></pre>    <p>经过上面的步骤一个post请求就完成了,当然上面的 url 参数和需要传入的参数大家就要根据实际情况来传入,你会发现get和post请求的步骤非常像。</p>    <h3>四、OkHttp进行Post请求提交字符串</h3>    <p>如果你已经掌握了上面的两种基本的步骤,那下面的内容就比较简单了</p>    <p>上面我们的post的参数是通过构造一个 FormBody 通过键值对的方式来添加进去的,其实post方法需要传入的是一个 RequestBody 对象, FormBody 是 RequestBody 的子类,但有时候我们常常会遇到要传入一个字符串的需求,比如客户端给服务器发送一个json字符串,那这种时候就需要用到另一种方式来构造一个 RequestBody 如下所示:</p>    <pre>  <code class="language-java">RequestBody requestBody = RequestBody.create(MediaType.parse("text/plain;charset=utf-8"), "{username:admin;password:admin}");</code></pre>    <p>上面的MediaType我们指定传输的是纯文本,而且编码方式是utf-8,通过上面的方式我们就可以向服务端发送json字符串啦。</p>    <p>注:关于MidiaType的类型你可以百度搜索 mime type 查看相关的内容,这里不再赘述</p>    <h3>五、OkHttp进行Post请求上传文件</h3>    <p>理解了上面一个,下面这个就更简单了,这里我们以上传一张图片为例,当然你也可以上传一个txt什么的文件,都是可以的</p>    <p>其实最主要的还是构架我们自己的 RequestBody ,如下图构建</p>    <pre>  <code class="language-java">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);  }</code></pre>    <p>这里我们将手机SD卡根目录下的 1.png 图片进行上传。代码中的 application/octet-stream 表示我们的文件是 任意二进制数据流 ,当然你也可以换成更具体的 image/png</p>    <p>注:最后记得最重要的一点:添加存储卡写权限,在 AndroidManifest.xml 文件中添加如下代码:</p>    <pre>  <code class="language-java"><uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/></code></pre>    <h3>六、OkHttp进行Post请求提交表单</h3>    <p>我们在网页上经常会遇到用户注册的情况,需要你输入用户名,密码,还有上传头像,这其实就是一个表单,那么接下来我们看看如何利用OkHttp来进行表单提交。经过上面的学习,大家肯定也懂,主要的区别就在于构造不同的 RequestBody 传递给 post 方法即可.</p>    <p>由于我们使用的是OkHttp3所以我们还需要再导入一个包 okio.jar 才能继续下面的内容,我们需要在模块的Gradle文件中添加如下代码,然后同步一下项目即可</p>    <pre>  <code class="language-java">compile 'com.squareup.okio:okio:1.11.0'</code></pre>    <p>这里我们会用到一个 MuiltipartBody ,这是 RequestBody 的一个子类,我们提交表单就是利用这个类来构建一个 RequestBody ,下面的代码我们会发送一个包含用户民、密码、头像的表单到服务端</p>    <pre>  <code class="language-java">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();</code></pre>    <p>上面添加用户民和密码的部分和我们上面学习的提交键值对的方法很像,我们关键要注意以下几点:</p>    <p>(1)如果提交的是表单,一定要设置 setType(MultipartBody.FORM) 这一句</p>    <p>(2)提交的文件 addFormDataPart() 的第一个参数,就上面代码中的 myfile 就是类似于键值对的键,是供服务端使用的,就类似于网页表单里面的 name 属性,例如下面:</p>    <pre>  <code class="language-java"><input type="file" name="myfile"></code></pre>    <p>(3)提交的文件 addFormDataPart() 的第二个参数文件的本地的名字,第三个参数是 RequestBody ,里面包含了我们要上传的文件的路径以及 MidiaType</p>    <p>(4)记得在 AndroidManifest.xml 文件中添加存储卡读写权限</p>    <h3>七、OkHttp进行get请求下载文件</h3>    <p>除了上面的功能,我们最常用的功能该有从网路上下载文件,我们下面的例子将演示下载一个文件存放在存储卡根目录,从网络下载一张图片并显示到ImageView中</p>    <p>1 . 从网络下载一个文件(此处我们以下载一张图片为例)</p>    <pre>  <code class="language-java">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();          }      });  }</code></pre>    <p>你会发现步骤与进行一般的 Get 请求差别不大,唯一的区别在于我们在回调函数中所做的事,我们拿到了图片的字节流,然后保存为了本地的一张图片</p>    <p>2 . 从网络下载一张图片并设置到ImageView中</p>    <p>其实学会了上面的步骤你完全可以将图片下载到本地后再设置到ImageView中,当然下面是另一种方法</p>    <p>这里我们使用 BitmapFactory 的 decodeStream 将图片的输入流直接转换为 Bitmap ,然后设置到 ImageView 中,下面只给出 onResponse() 中的代码.</p>    <pre>  <code class="language-java">@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();  }</code></pre>    <h3>八、给文件的上传和下载加上进度条</h3>    <p>我们一直都说,用户体验很重要,当我们下载的文件比较大,而网速又比较慢的时候,如果我们只是在后台下载或上传,没有给用户显示一个进度,那将是非常差的用户体验,下面我们就将简单做一下进度的显示,其实非常简单的</p>    <p>1 . 显示文件下载进度</p>    <p>这里只是演示,我只是把进度显示在一个TextView中,至于进度的获取当然是在我们的回调函数 onResponse() 中去获取</p>    <p>(1)使用 response.body().contentLength() 拿到文件总大小</p>    <p>(2)在 while 循环中每次递增我们读取的buf的长度</p>    <pre>  <code class="language-java">@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();  }</code></pre>    <p>2 . 显示文件上传进度</p>    <p>对于上传的进度的处理会比较麻烦,因为具体的上传过程是在 RequestBody 中由 OkHttp 帮我们处理上传,而且 OkHttp 并没有给我们提供上传进度的接口,这里我们的做法是自定义类继承 RequestBody ,然后重写其中的方法,将其中的上传进度通过接口回调暴露出来供我们使用。</p>    <pre>  <code class="language-java">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);      }  }</code></pre>    <p>上面的代码注释非常详细,这里不再解释,然后我们在写具体的请求时还需要做如下变化</p>    <pre>  <code class="language-java">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);      }  });</code></pre>    <p>上面其实就是在原有的 RequestBody 上包装了一层,最后在我们的使用中在 post() 方法中传入我们的 CountingRequestBody 对象即可。</p>    <h3>九、后记</h3>    <p>以上就是一些 OkHttp 常用的总结,希望可以帮助到需要的人</p>    <p> </p>    <p>来自:http://www.jianshu.com/p/9aa969dd1b4d</p>    <p> </p>