深入解析OkHttp3

BlancaZqr 8年前
   <p>OkHttp是一个精巧的网络请求库,有如下特性:</p>    <p>1)支持http2,对一台机器的所有请求共享同一个socket</p>    <p>2)内置连接池,支持连接复用,减少延迟</p>    <p>3)支持透明的gzip压缩响应体</p>    <p>4)通过缓存避免重复的请求</p>    <p>5)请求失败时自动重试主机的其他ip,自动重定向</p>    <p>6)好用的API</p>    <p>其本身就是一个很强大的库,再加上Retrofit2、Picasso的这一套组合拳,使其愈发的受到开发者的关注。本篇博客,我将对Okhttp3进行分析(源码基于Okhttp3.4)。</p>    <h2>如何引入Okhttp3?</h2>    <p>配置Okhttp3非常简单,只需要在Android Studio 的gradle进行如下的配置:</p>    <pre>  <code class="language-java">compile 'com.squareup.okhttp3:okhttp:3.4.1'</code></pre>    <p>添加网络权限:</p>    <pre>  <code class="language-java"><uses-permission android:name="android.permission.INTERNET"/></code></pre>    <h2>Okhttp3的基本使用</h2>    <p>okHttp的get请求okHttp的一般使用如下,okHttp默认使用的就是get请求</p>    <pre>  <code class="language-java">String url = "http://write.blog.csdn.net/postlist/0/0/enabled/1";      mHttpClient = new OkHttpClient();        Request request = new Request.Builder().url(url).build();      okhttp3.Response response = null;      try {                response = mHttpClient.newCall(request).execute();              String json = response.body().string();              Log.d("okHttp",json);        } catch (IOException e) {          e.printStackTrace();      }        }</code></pre>    <p>我们试着将数据在logcat进行打印,发现会报错,原因就是不能在主线程中进行耗时的操作</p>    <p><img src="https://simg.open-open.com/show/afb86b0094e065fc392f295964e922e4.png"></p>    <p>说明mHttpClient.newCall(request).execute()是同步的,那有没有异步的方法呢,答案是肯定的,就是mHttpClient.newCall(request).enqueue()方法,里面需要new一个callback我们对代码进行修改,如下</p>    <pre>  <code class="language-java">public void requestBlog() {       String url = "http://write.blog.csdn.net/postlist/0/0/enabled/1";         mHttpClient = new OkHttpClient();         Request request = new Request.Builder().url(url).build();  /* okhttp3.Response response = null;*/             /*response = mHttpClient.newCall(request).execute();*/       mHttpClient.newCall(request).enqueue(new Callback() {           @Override           public void onFailure(Call call, IOException e) {             }             @Override           public void onResponse(Call call, Response response) throws IOException {               String json = response.body().string();               Log.d("okHttp", json);           }       });       }</code></pre>    <p><img src="https://simg.open-open.com/show/1474bfaef515e95d6d46c69b6138523a.png"></p>    <p>Okhttp的POST请求</p>    <p>POST提交Json数据</p>    <pre>  <code class="language-java">private void postJson() throws IOException {      String url = "http://write.blog.csdn.net/postlist/0/0/enabled/1";      String json = "haha";        OkHttpClient client = new OkHttpClient();        RequestBody body = RequestBody.create(JSON, json);      Request request = new Request.Builder()              .url(url)              .post(body)              .build();      client.newCall(request).enqueue(new Callback() {          @Override          public void onFailure(Call call, IOException e) {            }            @Override          public void onResponse(Call call, Response response) throws IOException {                Log.d(TAG, response.body().string());          }      });      }</code></pre>    <p>POST提交键值对很多时候我们会需要通过POST方式把键值对数据传送到服务器。 OkHttp提供了很方便的方式来做这件事情。</p>    <pre>  <code class="language-java">private void post(String url, String json) throws IOException {       OkHttpClient client = new OkHttpClient();       RequestBody formBody = new FormBody.Builder()               .add("name", "liming")               .add("school", "beida")               .build();         Request request = new Request.Builder()               .url(url)               .post(formBody)               .build();         Call call = client.newCall(request);       call.enqueue(new Callback() {           @Override           public void onFailure(Call call, IOException e) {             }             @Override           public void onResponse(Call call, Response response) throws IOException {               String str = response.body().string();               Log.i(TAG, str);             }         });   }</code></pre>    <p><strong>异步上传文件</strong></p>    <p>上传文件本身也是一个POST请求</p>    <p>定义上传文件类型</p>    <pre>  <code class="language-java">public static final MediaType MEDIA_TYPE_MARKDOWN          = MediaType.parse("text/x-markdown; charset=utf-8");</code></pre>    <p>将文件上传到服务器上:</p>    <pre>  <code class="language-java">private void postFile() {      OkHttpClient mOkHttpClient = new OkHttpClient();      File file = new File("/sdcard/demo.txt");      Request request = new Request.Builder()              .url("https://api.github.com/markdown/raw")              .post(RequestBody.create(MEDIA_TYPE_MARKDOWN, file))              .build();        mOkHttpClient.newCall(request).enqueue(new Callback() {          @Override          public void onFailure(Call call, IOException e) {            }            @Override          public void onResponse(Call call, Response response) throws IOException {              Log.i(TAG, response.body().string());          }      });  }</code></pre>    <p>添加如下权限:</p>    <pre>  <code class="language-java"><uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>  <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/></code></pre>    <p>提取响应头典型的HTTP头 像是一个 Map</p>    <pre>  <code class="language-java">private final OkHttpClient client = new OkHttpClient();    public void run() throws Exception {      Request request = new Request.Builder()              .url("https://api.github.com/repos/square/okhttp/issues")              .header("User-Agent", "OkHttp Headers.java")              .addHeader("Accept", "application/json; q=0.5")              .addHeader("Accept", "application/vnd.github.v3+json")              .build();        Response response = client.newCall(request).execute();      if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);        System.out.println("Server: " + response.header("Server"));      System.out.println("Date: " + response.header("Date"));      System.out.println("Vary: " + response.headers("Vary"));  }</code></pre>    <p>Post方式提交String使用HTTP POST提交请求到服务。这个例子提交了一个markdown文档到web服务,以HTML方式渲染markdown。因为整个请求体都在内存中,因此避免使用此api提交大文档(大于1MB)。</p>    <pre>  <code class="language-java">private void postString() throws IOException {          OkHttpClient client = new OkHttpClient();          String postBody = ""              + "Releases\n"              + "--------\n"              + "\n"              + " * zhangfei\n"              + " * guanyu\n"              + " * liubei\n";        Request request = new Request.Builder()              .url("https://api.github.com/markdown/raw")              .post(RequestBody.create(MEDIA_TYPE_MARKDOWN, postBody))              .build();        Call call = client.newCall(request);      call.enqueue(new Callback() {          @Override          public void onFailure(Call call, IOException e) {            }            @Override          public void onResponse(Call call, Response response) throws IOException {              System.out.println(response.body().string());            }        });      }</code></pre>    <p>Post方式提交流</p>    <p>以流的方式POST提交请求体。请求体的内容由流写入产生。这个例子是流直接写入Okio的BufferedSink。你的程序可能会使用OutputStream,你可以使用BufferedSink.outputStream()来获取。</p>    <pre>  <code class="language-java">public static final MediaType MEDIA_TYPE_MARKDOWN          = MediaType.parse("text/x-markdown; charset=utf-8");    private void postStream() throws IOException {      RequestBody requestBody = new RequestBody() {          @Override          public MediaType contentType() {              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);          }      };        Request request = new Request.Builder()              .url("https://api.github.com/markdown/raw")              .post(requestBody)              .build();        Call call = client.newCall(request);      call.enqueue(new Callback() {          @Override          public void onFailure(Call call, IOException e) {            }            @Override          public void onResponse(Call call, Response response) throws IOException {              System.out.println(response.body().string());            }        });  }</code></pre>    <p>Post方式提交表单</p>    <pre>  <code class="language-java">private void postForm() {      OkHttpClient client = new OkHttpClient();        RequestBody formBody = new FormBody.Builder()              .add("search", "Jurassic Park")              .build();        Request request = new Request.Builder()              .url("https://en.wikipedia.org/w/index.php")              .post(formBody)              .build();        Call call = client.newCall(request);      call.enqueue(new Callback() {          @Override          public void onFailure(Call call, IOException e) {            }            @Override          public void onResponse(Call call, Response response) throws IOException {              System.out.println(response.body().string());            }        });    }</code></pre>    <p>Post方式提交分块请求MultipartBody 可以构建复杂的请求体,与HTML文件上传形式兼容。多块请求体中每块请求都是一个请求体,可以定义自己的请求头。这些请求头可以用来描述这块请求,例如他的Content-Disposition。如果Content-Length和Content-Type可用的话,他们会被自动添加到请求头中。</p>    <pre>  <code class="language-java">private static final String IMGUR_CLIENT_ID = "...";  private static final MediaType MEDIA_TYPE_PNG = MediaType.parse("image/png");    private void postMultipartBody() {      OkHttpClient client = new OkHttpClient();          // Use the imgur image upload API as documented at https://api.imgur.com/endpoints/image      MultipartBody body = new MultipartBody.Builder("AaB03x")              .setType(MultipartBody.FORM)              .addPart(                      Headers.of("Content-Disposition", "form-data; name=\"title\""),                      RequestBody.create(null, "Square Logo"))              .addPart(                      Headers.of("Content-Disposition", "form-data; name=\"image\""),                      RequestBody.create(MEDIA_TYPE_PNG, new File("website/static/logo-square.png")))              .build();        Request request = new Request.Builder()              .header("Authorization", "Client-ID " + IMGUR_CLIENT_ID)              .url("https://api.imgur.com/3/image")              .post(body)              .build();        Call call = client.newCall(request);      call.enqueue(new Callback() {          @Override          public void onFailure(Call call, IOException e) {            }            @Override          public void onResponse(Call call, Response response) throws IOException {              System.out.println(response.body().string());            }        });  }</code></pre>    <p><strong>响应缓存</strong></p>    <p>为了缓存响应,你需要一个你可以读写的缓存目录,和缓存大小的限制。这个缓存目录应该是私有的,不信任的程序应不能读取缓存内容。</p>    <p>一个缓存目录同时拥有多个缓存访问是错误的。大多数程序只需要调用一次new OkHttpClient(),在第一次调用时配置好缓存,然后其他地方只需要调用这个实例就可以了。否则两个缓存示例互相干扰,破坏响应缓存,而且有可能会导致程序崩溃。</p>    <p>响应缓存使用HTTP头作为配置。你可以在请求头中添加Cache-Control: max-stale=3600 ,OkHttp缓存会支持。你的服务通过响应头确定响应缓存多长时间,例如使用Cache-Control: max-age=9600。</p>    <pre>  <code class="language-java">int cacheSize = 10 * 1024 * 1024; // 10 MiB  Cache cache = new Cache(cacheDirectory, cacheSize);    OkHttpClient.Builder builder = new OkHttpClient.Builder();  builder.cache(cache);  OkHttpClient client = builder.build();    Request request = new Request.Builder()          .url("http://publicobject.com/helloworld.txt")          .build();    Call call = client.newCall(request);  call.enqueue(new Callback() {      @Override      public void onFailure(Call call, IOException e) {        }        @Override      public void onResponse(Call call, Response response) throws IOException {          String response1Body = response.body().string();          System.out.println("Response 1 response:          " + response);          System.out.println("Response 1 cache response:    " + response.cacheResponse());          System.out.println("Response 1 network response:  " + response.networkResponse());      }    });</code></pre>    <p>超时没有响应时使用超时结束call。没有响应的原因可能是客户点链接问题、服务器可用性问题或者这之间的其他东西。OkHttp支持连接,读取和写入超时。</p>    <pre>  <code class="language-java">private void ConfigureTimeouts() {        OkHttpClient.Builder builder = new OkHttpClient.Builder();      OkHttpClient client = builder.build();        client.newBuilder().connectTimeout(10, TimeUnit.SECONDS);      client.newBuilder().readTimeout(10,TimeUnit.SECONDS);      client.newBuilder().writeTimeout(10,TimeUnit.SECONDS);        Request request = new Request.Builder()              .url("http://httpbin.org/delay/2") // This URL is served with a 2 second delay.              .build();        Call call = client.newCall(request);      call.enqueue(new Callback() {          @Override          public void onFailure(Call call, IOException e) {            }            @Override          public void onResponse(Call call, Response response) throws IOException {              System.out.println("Response completed: " + response);          }        });    }</code></pre>    <h2>简单封装okHttp框架</h2>    <p>新建一个工具类OkHttpUtils</p>    <p>OkHttpClient必须是单例的,所以这里我们需要使用到单例设计模式,私有化构造函数,提供一个方法给外界获取OkHttpUtils实例对象</p>    <pre>  <code class="language-java">public class OkHttpUtils {        private  static  OkHttpUtils mInstance;      private OkHttpClient mHttpClient;        private OkHttpUtils() {        };        public static  OkHttpUtils getInstance(){          return  mInstance;      }    }</code></pre>    <p>一般网络请求分为get和post请求两种,但无论哪种请求都是需要用到request的,所以我们首先封装一个request,创建一个doRequest方法,在其内先编写mHttpClient.newCall(request).enqueue(new Callback())相关逻辑</p>    <pre>  <code class="language-java">public  void doRequest(final Request request){        mHttpClient.newCall(request).enqueue(new Callback() {          @Override          public void onFailure(Call call, IOException e) {            }            @Override          public void onResponse(Call call, Response response) throws IOException {            }      });  }</code></pre>    <p>我们需要自定义一个callback,BaseCallback,并将其传入request方法中</p>    <pre>  <code class="language-java">public class BaseCallback  {    }</code></pre>    <p>在OkHttpUtils中编写get和post方法</p>    <pre>  <code class="language-java">public void get(String url){      }    public void post(String url,Map<String,Object> param){      }</code></pre>    <p>post方法中构建request对象,这里我们需要创建一个buildRequest方法,用于生成request对象</p>    <pre>  <code class="language-java">private  Request buildRequest(String url,HttpMethodType methodType,Map<String,Object> params){      return null;  }</code></pre>    <p>这里需要定一个枚举对象HttpMethodType,用于区分是get还是post</p>    <pre>  <code class="language-java">enum  HttpMethodType{        GET,      POST,    }</code></pre>    <p>buildRequest方法根据HttpMethodType不同有相应的逻辑处理</p>    <pre>  <code class="language-java">private  Request buildRequest(String url,HttpMethodType methodType,Map<String,Object> params){        Request.Builder builder = new Request.Builder()              .url(url);        if (methodType == HttpMethodType.POST){            builder.post(body);      }      else if(methodType == HttpMethodType.GET){                builder.get();      }          return builder.build();    }</code></pre>    <p>builder.post()方法中需要一个body,所以我们需要创建一个方法builderFormData()方法用于返回RequestBody,这里内部逻辑后面再进行完善</p>    <pre>  <code class="language-java">private RequestBody builderFormData(Map<String,Object> params){      return null;  }</code></pre>    <p>于是buildRequest方法变成了这样</p>    <pre>  <code class="language-java">private  Request buildRequest(String url,HttpMethodType methodType,Map<String,Object> params){        Request.Builder builder = new Request.Builder()              .url(url);        if (methodType == HttpMethodType.POST){            RequestBody body = builderFormData(params);            builder.post(body);      }      else if(methodType == HttpMethodType.GET){                builder.get();      }          return builder.build();    }</code></pre>    <p>get方法进行修改:</p>    <pre>  <code class="language-java">public void get(String url,BaseCallback callback){        Request request = buildRequest(url,HttpMethodType.GET,null);        doRequest(request,callback);    }</code></pre>    <p>post方法进行修改:</p>    <pre>  <code class="language-java">public void post(String url,Map<String,Object> params,BaseCallback callback){        Request request = buildRequest(url,HttpMethodType.POST,params);        doRequest(request,callback);  }</code></pre>    <p>完善builderFormData()方法</p>    <pre>  <code class="language-java">private RequestBody builderFormData(Map<String,String> params){      FormBody.Builder builder =  new FormBody.Builder();        if(params!=null){          for(Map.Entry<String,String> entry:params.entrySet()){              builder.add(entry.getKey(),entry.getValue());          }      }      return builder.build();  }</code></pre>    <p>BaseCallback中定义一个抽象方法onBeforeRequest,这样做的理由是我们在加载网络数据成功前,一般都有进度条等显示,这个方法就是用来做这些处理的</p>    <pre>  <code class="language-java">public abstract class BaseCallback  {        public  abstract void onBeforeRequest(Request request);    }</code></pre>    <p>OkHttpUtils的doRequest方法增加如下语句:</p>    <pre>  <code class="language-java">baseCallback.onBeforeRequest(request);</code></pre>    <p>BaseCallback中多定义2个抽象方法</p>    <pre>  <code class="language-java">public abstract  void onFailure(Request request, Exception e) ;      /**   *请求成功时调用此方法   * @param response   */  public abstract  void onResponse(Response response);</code></pre>    <p>由于Response的状态有多种,比如成功和失败,所以需要onResponse分解为3个抽象方法</p>    <pre>  <code class="language-java">/**   *   * 状态码大于200,小于300 时调用此方法   * @param response   * @param t   * @throws   */  public abstract void onSuccess(Response response,T t) ;    /**   * 状态码400,404,403,500等时调用此方法   * @param response   * @param code   * @param e   */  public abstract void onError(Response response, int code,Exception e) ;    /**   * Token 验证失败。状态码401,402,403 等时调用此方法   * @param response   * @param code     */  public abstract void onTokenError(Response response, int code);</code></pre>    <p>response.body.string()方法返回的都是String类型,而我们需要显示的数据其实是对象,所以我们就想抽取出方法,直接返回对象,由于我们不知道对象的类型是什么,所以我们在BaseCallback中使用范型</p>    <pre>  <code class="language-java">public abstract class BaseCallback<T></code></pre>    <p>BaseCallback中需要将泛型转换为Type,所以要声明Type类型</p>    <pre>  <code class="language-java">public   Type mType;</code></pre>    <p>BaseCallback中需要如下一段代码,将泛型T转换为Type类型</p>    <pre>  <code class="language-java">static Type getSuperclassTypeParameter(Class<?> subclass)  {      Type superclass = subclass.getGenericSuperclass();      if (superclass instanceof Class)      {          throw new RuntimeException("Missing type parameter.");      }      ParameterizedType parameterized = (ParameterizedType) superclass;      return $Gson$Types.canonicalize(parameterized.getActualTypeArguments()[0]);  }</code></pre>    <p>在BaseCallback的构造函数中进行mType进行赋值</p>    <pre>  <code class="language-java">public BaseCallback()  {      mType = getSuperclassTypeParameter(getClass());  }</code></pre>    <p>OkHttpUtils中doRequest方法的onFailure与onResponse方法会相应的去调用baseCallback的方法</p>    <pre>  <code class="language-java">mHttpClient.newCall(request).enqueue(new Callback() {      @Override      public void onFailure(Call call, IOException e) {          baseCallback.onFailure(request,e);      }        @Override      public void onResponse(Call call, Response response) throws IOException {            if(response.isSuccessful()) {                baseCallback.onSuccess(response,null);              }else {              baseCallback.onError(response,response.code(),null);          }          /*mGson.fromJson(response.body().string(),baseCallback.mType);*/      }      });</code></pre>    <p>onResponse方法中成功的情况又有区分,根据mType的类型不同有相应的处理逻辑,同时还要考虑Gson解析错误的情况</p>    <pre>  <code class="language-java">@Override  public void onResponse(Call call, Response response) throws IOException {        if(response.isSuccessful()) {            String resultStr = response.body().string();            if (baseCallback.mType == String.class){                baseCallback.onSuccess(response,resultStr);          }          else {              try {                    Object obj = mGson.fromJson(resultStr, baseCallback.mType);                  baseCallback.onSuccess(response,obj);              }              catch (com.google.gson.JsonParseException e){ // Json解析的错误                  baseCallback.onError(response,response.code(),e);              }          }          }else {          baseCallback.onError(response,response.code(),null);      }    }</code></pre>    <p>构造函数中进行一些全局变量的初始化的操作,还有一些超时的设计</p>    <pre>  <code class="language-java">private OkHttpUtils() {        mHttpClient = new OkHttpClient();      OkHttpClient.Builder builder = mHttpClient.newBuilder();      builder.connectTimeout(10, TimeUnit.SECONDS);      builder.readTimeout(10,TimeUnit.SECONDS);      builder.writeTimeout(30,TimeUnit.SECONDS);        mGson = new Gson();        };</code></pre>    <p>静态代码块初始化OkHttpUtils对象</p>    <pre>  <code class="language-java">static {      mInstance = new OkHttpUtils();  }</code></pre>    <p>在okHttpUtils内,需要创建handler进行UI界面的更新操作,创建callbackSuccess方法</p>    <pre>  <code class="language-java">private void callbackSuccess(final  BaseCallback callback , final Response response, final Object obj ){        mHandler.post(new Runnable() {          @Override          public void run() {              callback.onSuccess(response, obj);          }      });  }</code></pre>    <p>doRequest方法的onResponse方法也进行相应的改写</p>    <pre>  <code class="language-java">if (baseCallback.mType == String.class){        /*baseCallback.onSuccess(response,resultStr);*/      callbackSuccess(baseCallback,response,resultStr);  }</code></pre>    <p>创建callbackError方法</p>    <pre>  <code class="language-java">private void callbackError(final BaseCallback callback, final Response response, final Exception e) {        mHandler.post(new Runnable() {          @Override          public void run() {              callback.onError(response, response.code(), e);          }      });  }</code></pre>    <p>将doRequest方法的onResponse方法中的baseCallback.onError(response,response.code(),e);替换为callbackError(baseCallback,response,e);方法</p>    <pre>  <code class="language-java">@Override  public void onResponse(Call call, Response response) throws IOException {        if(response.isSuccessful()) {            String resultStr = response.body().string();            if (baseCallback.mType == String.class){                /*baseCallback.onSuccess(response,resultStr);*/              callbackSuccess(baseCallback,response,resultStr);          }          else {              try {                    Object obj = mGson.fromJson(resultStr, baseCallback.mType);                  /*baseCallback.onSuccess(response,obj);*/                  callbackSuccess(baseCallback,response,obj);              }              catch (com.google.gson.JsonParseException e){ // Json解析的错误                  /*baseCallback.onError(response,response.code(),e);*/                  callbackError(baseCallback,response,e);              }          }          }else {            callbackError(baseCallback,response,null);          /*baseCallback.onError(response,response.code(),null);*/      }    }</code></pre>    <p>至此,我们的封装基本完成。</p>    <h2>OkHttp3源码分析</h2>    <p>请求处理分析当我们要请求网络的时候我们需要用OkHttpClient.newCall(request)进行execute或者enqueue操作,当我们调用newCall时:</p>    <pre>  <code class="language-java">/**   * Prepares the {@code request} to be executed at some point in the future.   */  @Override public Call newCall(Request request) {    return new RealCall(this, request);  }</code></pre>    <p>实际返回的是一个RealCall类,我们调用enqueue异步请求网络实际上是调用了RealCall的enqueue方法:</p>    <pre>  <code class="language-java">@Override public void enqueue(Callback responseCallback) {    synchronized (this) {      if (executed) throw new IllegalStateException("Already Executed");      executed = true;    }    client.dispatcher().enqueue(new AsyncCall(responseCallback));  }</code></pre>    <p>最终的请求是dispatcher来完成的。</p>    <p>Dispatcher任务调度</p>    <p>Dispatcher的本质是异步请求的管理器,控制最大请求并发数和单个主机的最大并发数,并持有一个线程池负责执行异步请求。对同步的请求只是用作统计。他是如何做到控制并发呢,其实原理就在上面的2个execute代码里面,真正网络请求执行前后会调用executed和finished方法,而对于AsyncCall的finished方法后,会根据当前并发数目选择是否执行队列中等待的AsyncCall。并且如果修改Dispatcher的maxRequests或者maxRequestsPerHost也会触发这个过程。</p>    <p>Dispatcher主要用于控制并发的请求,它主要维护了以下变量:</p>    <pre>  <code class="language-java">/** 最大并发请求数*/  private int maxRequests = 64;  /** 每个主机最大请求数*/  private int maxRequestsPerHost = 5;  /** 消费者线程池 */  private ExecutorService executorService;  /** 将要运行的异步请求队列 */  private final Deque<AsyncCall> readyAsyncCalls = new ArrayDeque<>();  /**正在运行的异步请求队列 */  private final Deque<AsyncCall> runningAsyncCalls = new ArrayDeque<>();  /** 正在运行的同步请求队列 */  private final Deque<RealCall> runningSyncCalls = new ArrayDeque<>();</code></pre>    <p>构造函数</p>    <pre>  <code class="language-java">public Dispatcher(ExecutorService executorService) {    this.executorService = executorService;  }    public Dispatcher() {  }    public synchronized ExecutorService executorService() {    if (executorService == null) {      executorService = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS,          new SynchronousQueue<Runnable>(), Util.threadFactory("OkHttp Dispatcher", false));    }    return executorService;  }</code></pre>    <p>Dispatcher有两个构造函数,可以使用自己设定线程池,如果没有设定线程池则会在请求网络前自己创建线程池,这个线程池类似于CachedThreadPool比较适合执行大量的耗时比较少的任务。</p>    <p>异步请求</p>    <pre>  <code class="language-java">synchronized void enqueue(AsyncCall call) {    if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {      runningAsyncCalls.add(call);      executorService().execute(call);    } else {      readyAsyncCalls.add(call);    }  }</code></pre>    <p>当正在运行的异步请求队列中的数量小于64并且正在运行的请求主机数小于5时则把请求加载到runningAsyncCalls中并在线程池中执行,否则就再入到readyAsyncCalls中进行缓存等待。</p>    <p>AsyncCall线程池中传进来的参数就是AsyncCall它是RealCall的内部类,内部也实现了execute方法:</p>    <pre>  <code class="language-java">@Override protected void execute() {      boolean signalledCallback = false;      try {        Response response = getResponseWithInterceptorChain();        if (retryAndFollowUpInterceptor.isCanceled()) {          signalledCallback = true;          responseCallback.onFailure(RealCall.this, new IOException("Canceled"));        } else {          signalledCallback = true;          responseCallback.onResponse(RealCall.this, response);        }      } catch (IOException e) {        if (signalledCallback) {          // Do not signal the callback twice!          Platform.get().log(INFO, "Callback failure for " + toLoggableString(), e);        } else {          responseCallback.onFailure(RealCall.this, e);        }      } finally {        client.dispatcher().finished(this);      }    }  }</code></pre>    <p>首先我们来看看最后一行, 无论这个请求的结果如何都会执行client.dispatcher().finished(this);</p>    <pre>  <code class="language-java">/** Used by {@code AsyncCall#run} to signal completion. */  void finished(AsyncCall call) {    finished(runningAsyncCalls, call, true);  }    /** Used by {@code Call#execute} to signal completion. */  void finished(RealCall call) {    finished(runningSyncCalls, call, false);  }    private <T> void finished(Deque<T> calls, T call, boolean promoteCalls) {    int runningCallsCount;    Runnable idleCallback;    synchronized (this) {      if (!calls.remove(call)) throw new AssertionError("Call wasn't in-flight!");      if (promoteCalls) promoteCalls();      runningCallsCount = runningCallsCount();      idleCallback = this.idleCallback;    }      if (runningCallsCount == 0 && idleCallback != null) {      idleCallback.run();    }  }  finished方法将此次请求从runningAsyncCalls移除后还执行了promoteCalls方法:  private void promoteCalls() {    if (runningAsyncCalls.size() >= maxRequests) return; // Already running max capacity.    if (readyAsyncCalls.isEmpty()) return; // No ready calls to promote.      for (Iterator<AsyncCall> i = readyAsyncCalls.iterator(); i.hasNext(); ) {      AsyncCall call = i.next();        if (runningCallsForHost(call) < maxRequestsPerHost) {        i.remove();        runningAsyncCalls.add(call);        executorService().execute(call);      }        if (runningAsyncCalls.size() >= maxRequests) return; // Reached max capacity.    }  }</code></pre>    <p>可以看到最关键的点就是会从readyAsyncCalls取出下一个请求,并加入runningAsyncCalls中并交由线程池处理。好了让我们再回到上面的AsyncCall的execute方法,我们会发getResponseWithInterceptorChain方法返回了Response,很明显这是在请求网络。</p>    <p><strong>Interceptor拦截器</strong></p>    <p>在回到RealCall中,我们看到无论是execute还是enqueue,真正的Response是通过这个函数getResponseWithInterceptorChain获取的,其他的代码都是用作控制与回调。而这里就是真正请求的入口,也是到了OkHttp的一个很精彩的设计:Interceptor与Chain</p>    <p>看一下RealCall中的getResponseWithInterceptorChain方法</p>    <pre>  <code class="language-java">private Response getResponseWithInterceptorChain() throws IOException {    // Build a full stack of interceptors.    List<Interceptor> interceptors = new ArrayList<>();    interceptors.addAll(client.interceptors());    interceptors.add(retryAndFollowUpInterceptor);    interceptors.add(new BridgeInterceptor(client.cookieJar()));    interceptors.add(new CacheInterceptor(client.internalCache()));    interceptors.add(new ConnectInterceptor(client));    if (!retryAndFollowUpInterceptor.isForWebSocket()) {      interceptors.addAll(client.networkInterceptors());    }    interceptors.add(new CallServerInterceptor(        retryAndFollowUpInterceptor.isForWebSocket()));      Interceptor.Chain chain = new RealInterceptorChain(        interceptors, null, null, null, 0, originalRequest);    return chain.proceed(originalRequest);  }</code></pre>    <p>这也是与旧版本不一致的地方,在3.4.x以前,没有这些内部的这些拦截器,只有用户的拦截器与网络拦截器。而Request和Response是通过HttpEngine来完成的。在RealCall实现了用户拦截器与RetryAndFollowUp的过程,而在HttpEngine内部处理了请求转换、Cookie、Cache、网络拦截器、连接网络的过程。值得一提的是,在旧版是获取到Response后调用网络拦截器的拦截。</p>    <p>而在这里,RealInterceptorChain会递归的创建并以此调用拦截器,去掉诸多异常,简化版代码如下:</p>    <pre>  <code class="language-java">public Response proceed(Request request, StreamAllocation streamAllocation, HttpStream httpStream,      Connection connection) throws IOException {    if (index >= interceptors.size()) throw new AssertionError();      calls++;      // If we already have a stream, confirm that the incoming request will use it.    if (this.httpStream != null && !sameConnection(request.url())) {      throw new IllegalStateException("network interceptor " + interceptors.get(index - 1)          + " must retain the same host and port");    }      // If we already have a stream, confirm that this is the only call to chain.proceed().    if (this.httpStream != null && calls > 1) {      throw new IllegalStateException("network interceptor " + interceptors.get(index - 1)          + " must call proceed() exactly once");    }      // Call the next interceptor in the chain.    RealInterceptorChain next = new RealInterceptorChain(        interceptors, streamAllocation, httpStream, connection, index + 1, request);    Interceptor interceptor = interceptors.get(index);    Response response = interceptor.intercept(next);      // Confirm that the next interceptor made its required call to chain.proceed().    if (httpStream != null && index + 1 < interceptors.size() && next.calls != 1) {      throw new IllegalStateException("network interceptor " + interceptor          + " must call proceed() exactly once");    }      // Confirm that the intercepted response isn't null.    if (response == null) {      throw new NullPointerException("interceptor " + interceptor + " returned null");    }      return response;  }</code></pre>    <p>Chain与Interceptor会互相递归调用,直到链的尽头。</p>    <p>我们看到,通过职责链模式,清楚地切开了不同的逻辑,每个拦截器完成自己的职责,从而完成用户的网络请求。</p>    <p>大概流程是:</p>    <p>1)先经过用户拦截器</p>    <p>2)RetryAndFollowUpInterceptor负责自动重试和进行必要的重定向</p>    <p>3)BridgeIntercetor负责将用户Request转换成一个实际的网络请求的Request,再调用下层的拦截器获取Response,最后再将网络Response转换成用户的Reponse</p>    <p>4)CacheInterceptor负责控制缓存</p>    <p>5)ConnectInterceptor负责进行连接主机</p>    <p>6)网络拦截器进行拦截</p>    <p>7)CallServerInterceptor是真正和服务器通信,完成http请求</p>    <p>连接与通信在RetryAndFollowUpInterceptor中,会创建StreamAllocation,然后交给下游的ConnectInterceptor</p>    <pre>  <code class="language-java">@Override public Response intercept(Chain chain) throws IOException {    RealInterceptorChain realChain = (RealInterceptorChain) chain;    Request request = realChain.request();    StreamAllocation streamAllocation = realChain.streamAllocation();      // We need the network to satisfy this request. Possibly for validating a conditional GET.    boolean doExtensiveHealthChecks = !request.method().equals("GET");    HttpStream httpStream = streamAllocation.newStream(client, doExtensiveHealthChecks);    RealConnection connection = streamAllocation.connection();      return realChain.proceed(request, streamAllocation, httpStream, connection);  }</code></pre>    <p>这里会创建一个HttpStream,并且取到一个RealConnection,继续交给下游的CallServerInterceptor。</p>    <p>我们跟踪进去看看,StreamAllocation里面做了什么</p>    <pre>  <code class="language-java">public HttpStream newStream(OkHttpClient client, boolean doExtensiveHealthChecks) {    int connectTimeout = client.connectTimeoutMillis();    int readTimeout = client.readTimeoutMillis();    int writeTimeout = client.writeTimeoutMillis();    boolean connectionRetryEnabled = client.retryOnConnectionFailure();      try {      RealConnection resultConnection = findHealthyConnection(connectTimeout, readTimeout,          writeTimeout, connectionRetryEnabled, doExtensiveHealthChecks);        HttpStream resultStream;      if (resultConnection.framedConnection != null) {        resultStream = new Http2xStream(client, this, resultConnection.framedConnection);      } else {        resultConnection.socket().setSoTimeout(readTimeout);        resultConnection.source.timeout().timeout(readTimeout, MILLISECONDS);        resultConnection.sink.timeout().timeout(writeTimeout, MILLISECONDS);        resultStream = new Http1xStream(            client, this, resultConnection.source, resultConnection.sink);      }        synchronized (connectionPool) {        stream = resultStream;        return resultStream;      }    } catch (IOException e) {      throw new RouteException(e);    }  }</code></pre>    <p>这里的代码逻辑是这样的,找一个健康的连接,设置超时时间,然后根据协议创建一个HttpStream并返回。</p>    <p>继续跟进去看findHealthyConnection:</p>    <pre>  <code class="language-java">private RealConnection findHealthyConnection(int connectTimeout, int readTimeout,      int writeTimeout, boolean connectionRetryEnabled, boolean doExtensiveHealthChecks)      throws IOException {    while (true) {      RealConnection candidate = findConnection(connectTimeout, readTimeout, writeTimeout,          connectionRetryEnabled);        // If this is a brand new connection, we can skip the extensive health checks.      synchronized (connectionPool) {        if (candidate.successCount == 0) {          return candidate;        }      }        // Do a (potentially slow) check to confirm that the pooled connection is still good. If it      // isn't, take it out of the pool and start again.      if (!candidate.isHealthy(doExtensiveHealthChecks)) {        noNewStreams();        continue;      }        return candidate;    }  }</code></pre>    <p>上面的逻辑也很简单,在findConnection中找一个连接,然后做健康检查,如果不健康就回收,并再次循环,那么真正寻找连接的代码就在findConnection里面了:</p>    <pre>  <code class="language-java">/**   * Returns a connection to host a new stream. This prefers the existing connection if it exists,   * then the pool, finally building a new connection.   */  private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,      boolean connectionRetryEnabled) throws IOException {    Route selectedRoute;    synchronized (connectionPool) {      if (released) throw new IllegalStateException("released");      if (stream != null) throw new IllegalStateException("stream != null");      if (canceled) throw new IOException("Canceled");        RealConnection allocatedConnection = this.connection;      if (allocatedConnection != null && !allocatedConnection.noNewStreams) {        return allocatedConnection;      }        // Attempt to get a connection from the pool.      RealConnection pooledConnection = Internal.instance.get(connectionPool, address, this);      if (pooledConnection != null) {        this.connection = pooledConnection;        return pooledConnection;      }        selectedRoute = route;    }      if (selectedRoute == null) {      selectedRoute = routeSelector.next();      synchronized (connectionPool) {        route = selectedRoute;        refusedStreamCount = 0;      }    }    RealConnection newConnection = new RealConnection(selectedRoute);    acquire(newConnection);      synchronized (connectionPool) {      Internal.instance.put(connectionPool, newConnection);      this.connection = newConnection;      if (canceled) throw new IOException("Canceled");    }      newConnection.connect(connectTimeout, readTimeout, writeTimeout, address.connectionSpecs(),        connectionRetryEnabled);    routeDatabase().connected(newConnection.route());      return newConnection;  }</code></pre>    <p>这里大概分成分成3大步:</p>    <p>1)如果当前有连接并且符合要求的话,就直接返回</p>    <p>2)如果线程池能取到一个符合要求的连接的话,就直接返回</p>    <p>3)如果Route为空,从RouteSelector取一个Route,然后新建一个RealConnection,并放入ConnectionPool,随后调用connect,再返回</p>    <p>也就是说不管当前走的是步骤1还是2,一开始一定是从3开始的,也就是在RealConnection的connect中真正完成了socket连接。</p>    <p>connect里面代码比较长,真正要做的就是一件事,如果是https请求并且是http代理,则建立隧道连接,隧道连接请参考RFC2817,否则建立普通连接。</p>    <p>这两者都调用了2个函数:connectSocket(connectTimeout, readTimeout); establishProtocol(readTimeout, writeTimeout, connectionSpecSelector);</p>    <p>但是隧道连接则多了一个代理认证的过程,可能会反复的connectSocket和构造请求。</p>    <p>看一下connectSocket:</p>    <pre>  <code class="language-java">private void connectSocket(int connectTimeout, int readTimeout) throws IOException {    Proxy proxy = route.proxy();    Address address = route.address();      rawSocket = proxy.type() == Proxy.Type.DIRECT || proxy.type() == Proxy.Type.HTTP        ? address.socketFactory().createSocket()        : new Socket(proxy);      rawSocket.setSoTimeout(readTimeout);    try {      Platform.get().connectSocket(rawSocket, route.socketAddress(), connectTimeout);    } catch (ConnectException e) {      throw new ConnectException("Failed to connect to " + route.socketAddress());    }    source = Okio.buffer(Okio.source(rawSocket));    sink = Okio.buffer(Okio.sink(rawSocket));  }</code></pre>    <p>就是根据Route来创建socket,在connect,随后将rawSocket的InputStream与OutputStream包装成Source与Sink。这里提一下,OkHttp是依赖Okio的,Okio封装了Java的IO API,如这里的Source与Sink,非常简洁实用。</p>    <p>而establishProtocol里,如果是https则走TLS协议,生成一个SSLSocket,并进行握手和验证,同时如果是HTTP2或者SPDY3的话,则生成一个FrameConnection。这里不再多提,HTTP2和HTTP1.X大相径庭,我们这里主要是分析HTTP1.X的连接,后面有机会我们会单独开篇讲HTTP2。同时TLS相关的话题这里也一并略过,想了解的朋友可以看一看相应的Java API和HTTPS连接的资料。</p>    <p>再回到StreamAllcation.newStream的代码resultStream = new Http1xStream( client, this, resultConnection.source, resultConnection.sink);实质上HttpStream其实就是Request和Response读写Socket的抽象,我们看到Http1xStream取到了Socket输入输出流,随后在CallServerInterceptor可以拿来做读写。</p>    <p>我们看CallServerInterceptor做了什么:</p>    <pre>  <code class="language-java">@Override public Response intercept(Chain chain) throws IOException {    HttpStream httpStream = ((RealInterceptorChain) chain).httpStream();    StreamAllocation streamAllocation = ((RealInterceptorChain) chain).streamAllocation();    Request request = chain.request();      long sentRequestMillis = System.currentTimeMillis();    httpStream.writeRequestHeaders(request);      if (HttpMethod.permitsRequestBody(request.method()) && request.body() != null) {      Sink requestBodyOut = httpStream.createRequestBody(request, request.body().contentLength());      BufferedSink bufferedRequestBody = Okio.buffer(requestBodyOut);      request.body().writeTo(bufferedRequestBody);      bufferedRequestBody.close();    }      httpStream.finishRequest();      Response response = httpStream.readResponseHeaders()        .request(request)        .handshake(streamAllocation.connection().handshake())        .sentRequestAtMillis(sentRequestMillis)        .receivedResponseAtMillis(System.currentTimeMillis())        .build();      if (!forWebSocket || response.code() != 101) {      response = response.newBuilder()          .body(httpStream.openResponseBody(response))          .build();    }      if ("close".equalsIgnoreCase(response.request().header("Connection"))        || "close".equalsIgnoreCase(response.header("Connection"))) {      streamAllocation.noNewStreams();    }      int code = response.code();    if ((code == 204 || code == 205) && response.body().contentLength() > 0) {      throw new ProtocolException(          "HTTP " + code + " had non-zero Content-Length: " + response.body().contentLength());    }      return response;  }</code></pre>    <p>CallServerInterceptor顾名思义,就是真正和Server进行通信的地方。这里也是按照HTTP协议,依次写入请求头,还有根据情况决定是否写入请求体。随后读响应头闭构造一个Response。</p>    <p>里面具体是如何实现呢,我们看Http1xStream:</p>    <p>首先是写头:</p>    <pre>  <code class="language-java">@Override public void writeRequestHeaders(Request request) throws IOException {    String requestLine = RequestLine.get(        request, streamAllocation.connection().route().proxy().type());    writeRequest(request.headers(), requestLine);  }</code></pre>    <p>构造好请求行,进入writeRequest:</p>    <pre>  <code class="language-java">/** Returns bytes of a request header for sending on an HTTP transport. */  public void writeRequest(Headers headers, String requestLine) throws IOException {    if (state != STATE_IDLE) throw new IllegalStateException("state: " + state);    sink.writeUtf8(requestLine).writeUtf8("\r\n");    for (int i = 0, size = headers.size(); i < size; i++) {      sink.writeUtf8(headers.name(i))          .writeUtf8(": ")          .writeUtf8(headers.value(i))          .writeUtf8("\r\n");    }    sink.writeUtf8("\r\n");    state = STATE_OPEN_REQUEST_BODY;  }</code></pre>    <p>这里就一目了然了,就是一行行的写请求行和请求头到sink中</p>    <p>再看readResponse:</p>    <pre>  <code class="language-java">/** Parses bytes of a response header from an HTTP transport. */  public Response.Builder readResponse() throws IOException {    if (state != STATE_OPEN_REQUEST_BODY && state != STATE_READ_RESPONSE_HEADERS) {      throw new IllegalStateException("state: " + state);    }      try {      while (true) {        StatusLine statusLine = StatusLine.parse(source.readUtf8LineStrict());          Response.Builder responseBuilder = new Response.Builder()            .protocol(statusLine.protocol)            .code(statusLine.code)            .message(statusLine.message)            .headers(readHeaders());          if (statusLine.code != HTTP_CONTINUE) {          state = STATE_OPEN_RESPONSE_BODY;          return responseBuilder;        }      }    } catch (EOFException e) {      // Provide more context if the server ends the stream before sending a response.      IOException exception = new IOException("unexpected end of stream on " + streamAllocation);      exception.initCause(e);      throw exception;    }  }</code></pre>    <p>也是一样的,从source中读请求行和请求头</p>    <p>最后看openResponseBody:</p>    <pre>  <code class="language-java">@Override public ResponseBody openResponseBody(Response response) throws IOException {    Source source = getTransferStream(response);    return new RealResponseBody(response.headers(), Okio.buffer(source));  }</code></pre>    <p>这里说一下就是根据请求的响应把包裹InputStream的source再次封装,里面做一些控制逻辑,然后再封装成ResponseBody。</p>    <p>例如FiexdLengthSource,就是期望获取到byte的长度是固定的值:</p>    <pre>  <code class="language-java">/** An HTTP body with a fixed length specified in advance. */  private class FixedLengthSource extends AbstractSource {    private long bytesRemaining;      public FixedLengthSource(long length) throws IOException {      bytesRemaining = length;      if (bytesRemaining == 0) {        endOfInput(true);      }    }      @Override public long read(Buffer sink, long byteCount) throws IOException {      if (byteCount < 0) throw new IllegalArgumentException("byteCount < 0: " + byteCount);      if (closed) throw new IllegalStateException("closed");      if (bytesRemaining == 0) return -1;        long read = source.read(sink, Math.min(bytesRemaining, byteCount));      if (read == -1) {        endOfInput(false); // The server didn't supply the promised content length.        throw new ProtocolException("unexpected end of stream");      }        bytesRemaining -= read;      if (bytesRemaining == 0) {        endOfInput(true);      }      return read;    }      @Override public void close() throws IOException {      if (closed) return;        if (bytesRemaining != 0 && !Util.discard(this, DISCARD_STREAM_TIMEOUT_MILLIS, MILLISECONDS)) {        endOfInput(false);      }        closed = true;    }  }</code></pre>    <p>当读完期望的长度时就把这个RealConnection回收,如果少于期望的长度则抛异常。</p>    <p><strong>ConnectionPool</strong></p>    <p>到了OkHttp3时代,ConnectionPool就是每个Client独享的了,我们刚才提到了ConnectionPool,那么他到底是如何运作呢。</p>    <p>ConnectionPool持有一个静态的线程池。</p>    <p>StreamAllocation不管通过什么方式,在获取到RealConnection后,RealConnection会添加一个对StreamAllocation的引用。</p>    <p>在每个RealConnection加入ConnectionPool后,如果当前没有在清理,就会把cleanUpRunnable加入线程池。</p>    <p>cleanUpRunnable里面是一个while(true),一个循环包括:</p>    <p>调用一次cleanUp方法进行清理并返回一个long, 如果是-1则退出,否则调用wait方法等待这个long值的时间</p>    <p>cleanUp代码如下:</p>    <pre>  <code class="language-java">ong cleanup(long now) {    int inUseConnectionCount = 0;    int idleConnectionCount = 0;    RealConnection longestIdleConnection = null;    long longestIdleDurationNs = Long.MIN_VALUE;      // Find either a connection to evict, or the time that the next eviction is due.    synchronized (this) {      for (Iterator<RealConnection> i = connections.iterator(); i.hasNext(); ) {        RealConnection connection = i.next();          // If the connection is in use, keep searching.        if (pruneAndGetAllocationCount(connection, now) > 0) {          inUseConnectionCount++;          continue;        }          idleConnectionCount++;          // If the connection is ready to be evicted, we're done.        long idleDurationNs = now - connection.idleAtNanos;        if (idleDurationNs > longestIdleDurationNs) {          longestIdleDurationNs = idleDurationNs;          longestIdleConnection = connection;        }      }        if (longestIdleDurationNs >= this.keepAliveDurationNs          || idleConnectionCount > this.maxIdleConnections) {        // We've found a connection to evict. Remove it from the list, then close it below (outside        // of the synchronized block).        connections.remove(longestIdleConnection);      } else if (idleConnectionCount > 0) {        // A connection will be ready to evict soon.        return keepAliveDurationNs - longestIdleDurationNs;      } else if (inUseConnectionCount > 0) {        // All connections are in use. It'll be at least the keep alive duration 'til we run again.        return keepAliveDurationNs;      } else {        // No connections, idle or in use.        cleanupRunning = false;        return -1;      }    }      closeQuietly(longestIdleConnection.socket());      // Cleanup again immediately.    return 0;  }</code></pre>    <p>遍历每一个RealConnection,通过引用数目确定哪些是空闲的,哪些是在使用中,同时找到空闲时间最长的RealConnection。</p>    <p>如果空闲数目超过最大空闲数或者空闲时间超过最大空闲时间,则清理掉这个RealConnection,并返回0,表示需要立刻再次清理</p>    <p>否则如果空闲的数目大于0个,则等待最大空闲时间-已有的最长空闲时间</p>    <p>否则如果使用中的数目大于0,则等待最大空闲时间</p>    <p>否则 返回 -1,并标识退出清除状态</p>    <p>同时如果某个RealConnection空闲后,会进入ConnectionPool.connectionBecameIdle方法,如果不可被复用,则被移除,否则立刻唤醒上面cleanUp的wait,再次清理,因为可能超过了最大空闲数目</p>    <p>这样通过一个静态的线程池,ConnectionPool做到了每个实例定期清理,保证不会超过最大空闲时间和最大空闲数目的策略。</p>    <p>OkHttp3分析就到此结束了。</p>    <p> </p>    <p>来自:http://blog.csdn.net/u012124438/article/details/54236967</p>    <p> </p>