Android网络编程:OkHttp的使用和源码分析

一、简介

OkHttp是一个高效的HTTP客户端,它有以下默认特性:

  • 支持HTTP/2,允许所有同一个主机地址的请求共享同一个socket连接
  • 连接池减少请求延时
  • 透明的GZIP压缩减少响应数据的大小
  • 缓存响应内容,避免一些完全重复的请求

当网络出现问题的时候OkHttp依然坚守自己的职责,它会自动恢复一般的连接问题,如果你的服务有多个IP地址,当第一个IP请求失败时,OkHttp会交替尝试你配置的其他IP。

它的请求/响应 API 使用构造器模式builders来设计,它支持阻塞式的同步请求和带回调的异步请求。

二、基本使用方式

在OKHttp,每次网络请求就是一个Request,我们在Request里填写我们需要的url,header等其他参数,再通过Request构造出Call,Call内部去请求服务器,得到回复,并将结果告诉调用者。同时okhttp提供了同步和异步两种方式进行网络操作。

1、同步Get

private void executeRequest() {
        String url = "http://wwww.baidu.com";
        OkHttpClient okHttpClient = new OkHttpClient();
        final Request request
                = new Request.Builder()
                .url(url)
                .build();
        final Call call = okHttpClient.newCall(request);
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Response response = call.execute();
                    Log.d("HttpExecuteRequest", "run: " + response.body().string());
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }

2、异步Get

private void getAsynHttp() {
        okHttpClient = new OkHttpClient();
        Request.Builder requestBuilder = new Request.Builder().url("http://www.baidu.com");
        requestBuilder.method("GET", null);
        Request request = requestBuilder.build();
        Call call = okHttpClient.newCall(request);
        call.enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {

            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {
                if (null != response.cacheResponse()) {
                    String str = response.cacheResponse().toString();
                    Log.i("OkHttp getAsyn", "cache---" + str);
                } else {
                    response.body().string();
                    String str = response.networkResponse().toString();
                    Log.i("OkHttp getAsyn", "network---" + str);
                }
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        Toast.makeText(getApplicationContext(), "请求成功", Toast.LENGTH_SHORT).show();
                    }
                });
            }
        });
    }

3、异步Post

private void postAsynHttp() {
        okHttpClient = new OkHttpClient();
        RequestBody formBody = new FormBody.Builder()
                .add("size", "10")
                .build();
        Request request = new Request.Builder()
                .url("http://api.1-blog.com/biz/bizserver/article/list.do")
                .post(formBody)
                .build();
        Call call = okHttpClient.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();
                resopnseString = str;
                Log.i("OkHttp postAsyn", str);
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        Toast.makeText(getApplicationContext(), "请求成功", Toast.LENGTH_SHORT).show();
                    }
                });
            }
        });
    }

4、异步上传文件

private void postAsynFile() {
        okHttpClient = new OkHttpClient();
        File file = new File("/sdcard/kwm.txt");
        Request request = new Request.Builder()
                .url("https://api.github.com/markdown/raw")
                .post(RequestBody.create(MEDIA_TYPE_MARKDOWN, file))
                .build();
        okHttpClient.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("OkHttp postAsynFile", response.body().string());
                resopnseString = response.body().string();
            }
        });
    }

5、异步下载文件

 private void downAsynFile() {
        okHttpClient = new OkHttpClient();
        String url = "https://img-my.csdn.net/uploads/201603/26/1458988468_5804.jpg";
        Request request = new Request.Builder().url(url).build();
        okHttpClient.newCall(request).enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
            }

            @Override
            public void onResponse(Call call, Response response) {
                InputStream inputStream = response.body().byteStream();
                FileOutputStream fileOutputStream = null;
                try {
                    fileOutputStream = new FileOutputStream(new File("/sdcard/wangshu.jpg"));
                    byte[] buffer = new byte[2048];
                    int len = 0;
                    while ((len = inputStream.read(buffer)) != -1) {
                        fileOutputStream.write(buffer, 0, len);
                    }
                    fileOutputStream.flush();
                } catch (IOException e) {
                    Log.i("OkHttp downAsynFile", "IOException");
                    e.printStackTrace();
                }
                Log.d("OkHttp downAsynFile", "文件下载成功");
            }
        });
    }

6、异步上传Multipart文件

private void sendMultipart() {
        okHttpClient = new OkHttpClient();
        RequestBody requestBody = new MultipartBody.Builder()
                .setType(MultipartBody.FORM)
                .addFormDataPart("title", "this is a title")
                .addFormDataPart("image", "image.jpg",
                        RequestBody.create(MEDIA_TYPE_PNG, new File("/sdcard/image.jpg")))
                .build();
        Request request = new Request.Builder()
                .header("Authorization", "Client-ID " + "...")
                .url("https://api.imgur.com/3/image")
                .post(requestBody)
                .build();
        okHttpClient.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("OkHttp downAsynFile", response.body().string());
            }
        });
    }

三、核心源码分析

Okhttp四个核心点:任务,拦截器,缓存,复用连接池

在OkHttp框架下一个网络请求需要经历的过程:

1、任务

查看源码,进行execute或者enqueue操作,当我们调用newCall实际返回的是一个RealCall类。

@Override public Call newCall(Request request) {
    return new RealCall(this, request);
  }

我们调用enqueue异步请求网络实际上是调用了RealCall的enqueue方法

 void enqueue(Callback responseCallback, boolean forWebSocket) {
    synchronized (this) {
      if (executed) throw new IllegalStateException("Already Executed");
      executed = true;
    }
    client.dispatcher().enqueue(new AsyncCall(responseCallback, forWebSocket));
  }

可以看到,请求是通过Dispatcher实现任务调度的

Dispatcher主要用于控制并发的请求,它主要维护了以下变量:

/** 最大并发请求数*/
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<>();

Dispatcher有两个构造函数,可以使用自己设定线程池,如果没有设定线程池则会在请求网络前自己创建线程池,这个线程池类似于CachedThreadPool比较适合执行大量的耗时比较少的任务。

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;
 }

Dispatcher执行enqueue方法:

synchronized void enqueue(AsyncCall call) {
    if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {
      runningAsyncCalls.add(call);
      executorService().execute(call);
    } else {
      readyAsyncCalls.add(call);
    }
  }

当正在运行的异步请求队列中的数量小于64并且正在运行的请求主机数小于5时则把请求加载到runningAsyncCalls中并在线程池中执行,否则就再入到readyAsyncCalls中进行缓存等待。

线程池中传进来的参数就是AsyncCall它是RealCall的内部类,内部也实现了execute方法:

@Override protected void execute() {
      boolean signalledCallback = false;
      try {
        Response response = getResponseWithInterceptorChain(forWebSocket);
        if (canceled) {
          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!
          logger.log(Level.INFO, "Callback failure for " + toLoggableString(), e);
        } else {
          responseCallback.onFailure(RealCall.this, e);
        }
      } finally {
        client.dispatcher().finished(this);
      }
    }

最后一行, 无论这个请求的结果如何都会执行client.dispatcher().finished(this)

/** Used by {@code AsyncCall#run} to signal completion. */
  synchronized void finished(AsyncCall call) {
    if (!runningAsyncCalls.remove(call)) throw new AssertionError("AsyncCall wasn't running!");
    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.
    }
  }

从promoteCalls方法,我们可以看到从readyAsyncCalls取出下一个请求,并加入runningAsyncCalls中并交由线程池处理。

2、拦截器

从上面我们可以看到,同步和异步都会殊途同归,执行execute中的getResponseWithInterceptorChain方法。

private Response getResponseWithInterceptorChain(boolean forWebSocket) throws IOException {
    Interceptor.Chain chain = new ApplicationInterceptorChain(0, originalRequest, forWebSocket);
    return chain.proceed(originalRequest);
  }

ApplicationInterceptorChain,它是一个拦截器链,这个类也是RealCall的内部类,接下来执行了它的proceed方法:

@Override public Response proceed(Request request) throws IOException {
      // If there's another interceptor in the chain, call that.
      if (index < client.interceptors().size()) {
        Interceptor.Chain chain = new ApplicationInterceptorChain(index + 1, request, forWebSocket);
        Interceptor interceptor = client.interceptors().get(index);
        Response interceptedResponse = interceptor.intercept(chain);

        if (interceptedResponse == null) {
          throw new NullPointerException("application interceptor " + interceptor
              + " returned null");
        }

        return interceptedResponse;
      }

      // No more interceptors. Do HTTP.
      return getResponse(request, forWebSocket);
    }

proceed方法每次从拦截器列表中取出拦截器,当存在多个拦截器时都会在第七行阻塞,并等待下一个拦截器的调用返回。拦截器主要用来观察,修改以及可能短路的请求输出和响应的回来。通常情况下拦截器用来添加,移除或者转换请求或者响应的头部信息。

3、缓存

getResponse方法比较长,其中创建了HttpEngine并调用了sendRequest和readResponse方法。

	...
try {
        engine.sendRequest();
        engine.readResponse();
        releaseConnection = false;
      } catch (RequestException e) {
        // The attempt to interpret the request failed. Give up.
        throw e.getCause();
      } catch (RouteException e) {
        // The attempt to connect via a route failed. The request will not have been sent.
        HttpEngine retryEngine = engine.recover(e.getLastConnectException(), null);
        if (retryEngine != null) {
          releaseConnection = false;
          engine = retryEngine;
          continue;
        }
        // Give up; recovery is not possible.
        throw e.getLastConnectException();
      } catch (IOException e) {
        // An attempt to communicate with a server failed. The request may have been sent.
        HttpEngine retryEngine = engine.recover(e, null);
        if (retryEngine != null) {
          releaseConnection = false;
          engine = retryEngine;
          continue;
        }

        // Give up; recovery is not possible.
        throw e;
      } finally {
        // We're throwing an unchecked exception. Release any resources.
        if (releaseConnection) {
          StreamAllocation streamAllocation = engine.close();
          streamAllocation.release();
        }
      }
	  ...

sendRequest显然是在发送请求,查看代码会发现,里面做了缓存的策略。根据cacheStrategy的处理来判断缓存是否可用或者需要最新数据。

当请求失败时,会执行HttpEngine的recover方法,完成重连.

4、复用连接池

连接池的类位于okhttp3.ConnectionPool:

private static final Executor executor = new ThreadPoolExecutor(0 /* corePoolSize */,
     Integer.MAX_VALUE /* maximumPoolSize */, 60L /* keepAliveTime */, TimeUnit.SECONDS,
     new SynchronousQueue<Runnable>(), Util.threadFactory("OkHttp ConnectionPool", true));

 /** The maximum number of idle connections for each address. */
 //空闲的socket最大连接数
 private final int maxIdleConnections;
 //socket的keepAlive时间
 private final long keepAliveDurationNs;
 // 双向队列
 private final Deque<RealConnection> connections = new ArrayDeque<>();
 final RouteDatabase routeDatabase = new RouteDatabase();
 boolean cleanupRunning;

executor线程池,类似于CachedThreadPool,采用没有容量的SynchronousQueue

Deque,双向队列,双端队列同时具有队列和栈性质,经常在缓存中被使用,里面维护了RealConnection也就是socket物理连接的包装。

RouteDatabase,它用来记录连接失败的Route的黑名单,当连接失败的时候就会把失败的线路加进去。

构造函数:

public ConnectionPool() {
    this(5, 5, TimeUnit.MINUTES);
  }

通过构造函数可以看出ConnectionPool默认的空闲的socket最大连接数为5个,socket的keepAlive时间为5分钟。

在okhttp中,使用了类似于引用计数的方式跟踪Socket流的调用,计数对象是StreamAllocation,它被反复执行aquire与release操作。增加和减少引用的计数。如果计数为0的话,说明此连接没有被使用就是空闲的,通过算法实现回收,如果计数不为0,则表示上层代码仍然引用,就不需要关闭连接。

在ConnectionPool中:put、get、connectionBecameIdle和evictAll分别对应放入连接、获取连接、移除连接和移除所有连接.

/** Returns a recycled connection to {@code address}, or null if no such connection exists. */
  RealConnection get(Address address, StreamAllocation streamAllocation) {
    assert (Thread.holdsLock(this));
    for (RealConnection connection : connections) {
      if (connection.allocations.size() < connection.allocationLimit
          && address.equals(connection.route().address)
          && !connection.noNewStreams) {
        streamAllocation.acquire(connection);
        return connection;
      }
    }
    return null;
  }

  void put(RealConnection connection) {
    assert (Thread.holdsLock(this));
    if (!cleanupRunning) {
      cleanupRunning = true;
      executor.execute(cleanupRunnable);
    }
    connections.add(connection);
  }

移除连接,则是从队列对realconnettion 进行remove。put之前清理空闲的线程(回收连接),get方法会遍历connections缓存列表,当某个连接计数的次数小于限制的大小并且request的地址和缓存列表中此连接的地址完全匹配。则直接复用缓存列表中的connection作为request的连接。

回收主要是要cleanup线程去实现的,核心代码如下:

// 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;
      }
    }

根据连接中的引用计数来计算空闲连接数和活跃连接数,然后标记出空闲的连接,如果空闲连接keepAlive时间超过5分钟,或者空闲连接数超过5个,则从Deque中移除此连接。接下来根据空闲连接或者活跃连接来返回下次需要清理的时间数:如果空闲连接大于0则返回此连接即将到期的时间,如果都是活跃连接并且大于0则返回默认的keepAlive时间5分钟,如果没有任何连接则跳出循环并返回-1。

在上述代码中的第13行,通过pruneAndGetAllocationCount方法来判断连接是否闲置的,如果pruneAndGetAllocationCount方法返回值大于0则是空闲连接,否则就是活跃连接。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

KWMax

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值