okhttp源码分析

如需转载请评论或简信,并注明出处,未经允许不得转载

目录

前言

okhttp是square开源的轻量级网络框架

依赖方式

"com.squareup.okhttp3:okhttp:4.2.1"

使用步骤

先来看看okhttp的基本使用方法。本文主要介绍原理,更多的使用方式可参考okhttp官网

GET请求

//1.创建OKHttpClient对象
OkHttpClient client = new OkHttpClient();

String get(String url) throws IOException {
  //2.创建Request对象
  Request request = new Request.Builder()
      .url(url)
      .build();
    //3.创建NewCall对象
  //4.执行newCall.execute(),生成response对象
  try (Response response = client.newCall(request).execute()) {
    return response.body().string();
  }
}

POST请求

//1.创建OKHttpClient对象
OkHttpClient client = new OkHttpClient();
//2.设置mediaType
public static final MediaType JSON
    = MediaType.get("application/json; charset=utf-8");

String post(String url, String json) throws IOException {
  //3.创建请求体
  RequestBody body = RequestBody.create(JSON, json);
  //4.创建Request对象
  Request request = new Request.Builder()
      .url(url)
      .post(body)
      .build();
  //5.创建NewCall对象
  //6.执行newCall.execute(),生成response对象
  try (Response response = client.newCall(request).execute()) {
    return response.body().string();
  }
}

可以看出GET请求和POST请求的步骤其实都大同小异,主要分为如下几步

  1. 创建okHttpClient对象

OkHttpClient对象有两种创建方式,一种是直接new OKHttpClient(),还有一种可以通过建造者模式new OkHttpClient.Builder()添加其他属性。实际上okhttp在默认的构造方法中就已经赋值了很多属性的默认值

  1. 创建request对象

每一个http请求都包含请求url、请求方法(GET/POST)、请求头(Header),同时也可能包含请求体(Body

  1. 创建response对象

response就是对request请求的一个回复,通过执行newCall中的execute()生成

源码分析

okhttp支持同步请求和异步请求,execute()是同步请求,enqueue()是异步请求。下面我们通过源码来看一下response是如何创建的(本人当前使用的okhttp版本的源码是基于kotlin)

同步请求

RealCall.kt

  override fun execute(): Response {
    synchronized(this) {
      //1.先检查newcall是否被执行过,被执行过会抛出异常
      check(!executed) { "Already Executed" }
      executed = true
    }
    transmitter.timeoutEnter()
    transmitter.callStart()
    try {
      //2.通过dispatcher调度器执行
      client.dispatcher.executed(this)
      //3.通过执行一系列拦截器链,最终返回response
      //☆关于拦截器的内容下文会重点分析
      return getResponseWithInterceptorChain()
    } finally {
      //4.执行完请求后,将请求从dispatcher调度器移除
      client.dispatcher.finished(this)
    }
  }

Dispather.kt

/** Running synchronous calls. Includes canceled calls that haven't finished yet. */
//正在运行的同步请求队列
private val runningSyncCalls = ArrayDeque<RealCall>()
  
@Synchronized internal fun executed(call: RealCall) {
    //将call请求放到双向队列中
  runningSyncCalls.add(call)
}

异步请求

RealCall.kt

override fun enqueue(responseCallback: Callback) {
  synchronized(this) {
    //1.和同步请求一样,先检查newcall是否被执行过,被执行过会抛出异常
    check(!executed) { "Already Executed" }
    executed = true
  }
  transmitter.callStart()
  //2.执行dispatcher.enqueue()
  client.dispatcher.enqueue(AsyncCall(responseCallback))
}

Dispather.kt

/** Ready async calls in the order they'll be run. */
//缓存等待的异步请求队列
private val readyAsyncCalls = ArrayDeque<AsyncCall>()

/** Running asynchronous calls. Includes canceled calls that haven't finished yet. */
//正在执行的异步请求队列
private val runningAsyncCalls = ArrayDeque<AsyncCall>()

@get:Synchronized
@get:JvmName("executorService") val executorService: ExecutorService
  get() {
    if (executorServiceOrNull == null) {
      //执行异步任务的线程池创建
      //SynchronousQueue是一个内部只能包含一个元素的队列。
      //插入元素到队列的线程被阻塞,直到另一个线程从队列中获取了队列中存储的元素。
      //同样,如果线程尝试获取元素并且当前不存在任何元素,则该线程将被阻塞,直到线程将元素插入队列。
      //类似生产者和消费者模型
      executorServiceOrNull = ThreadPoolExecutor(0, Int.MAX_VALUE, 60, TimeUnit.SECONDS,
          SynchronousQueue(), threadFactory("OkHttp Dispatcher", false))
    }
    return executorServiceOrNull!!
  }

internal fun enqueue(call: AsyncCall) {
  synchronized(this) {
    //先将请求添加到缓存等待队列
    readyAsyncCalls.add(call)
    if (!call.get().forWebSocket) {
      val existingCall = findExistingCallWithHost(call.host())
      if (existingCall != null) call.reuseCallsPerHostFrom(existingCall)
    }
  }
  //执行请求
  promoteAndExecute()
}

  private fun promoteAndExecute(): Boolean {
    assert(!Thread.holdsLock(this))

    val executableCalls = mutableListOf<AsyncCall>()
    val isRunning: Boolean
    synchronized(this) {
      val i = readyAsyncCalls.iterator()
      while (i.hasNext()) {
        val asyncCall = i.next()
    //请求最大运行数不能超过64(maxRequests=64),最大主机连接数不能超过5(maxRequestsPerHost=5)
        if (runningAsyncCalls.size >= this.maxRequests) break // Max capacity.
        if (asyncCall.callsPerHost().get() >= this.maxRequestsPerHost) continue // Host max capacity.
    //从缓存等待队列中移除
        i.remove()
        asyncCall.callsPerHost().incrementAndGet()
        executableCalls.add(asyncCall)
        //添加到正在执行的异步请求队列
        runningAsyncCalls.add(asyncCall)
      }
      //运行数>0说明正在运行
      isRunning = runningCallsCount() > 0
    }

    for (i in 0 until executableCalls.size) {
      val asyncCall = executableCalls[i]
      //executorService是一个线程池ThreadPoolExecutor对象
      asyncCall.executeOn(executorService)
    }

    return isRunning
  }

Dispather功能

  1. 维护请求的状态

一个Call只能被执行一次,否则抛出异常

  1. 维护请求队列
  • runningSyncCalls:正在运行的同步请求队列
  • readyAsyncCalls:正在执行的异步请求队列,请求最大运行数不能超过64,最大主机连接数不能超过5
  • runningAsyncCalls:缓存等待的异步请求队列
  1. 维护线程池

创建了一个阀值是Integer.MAX_VALUE的线程池,它不保留任何最小线程,随时创建更多的线程数,而且如果线程空闲后,只能多活60秒。所以也就说如果收到20个并发请求,线程池会创建20个线程,当完成后的60秒后会自动关闭所有20个线程。他这样设计成不设上限的线程,以保证I/O任务中高阻塞低占用的过程,不会长时间卡在阻塞上

拦截器

通过response的创建过程的分析,我们发现okhttp在返回response的过程中,会经过一系列的拦截器

拦截器是okhttp提供的一种强大的机制,可以监视,重写和重试网络请求。下面是一个简单的拦截器,用于记录请求和相应的日志

public class LoggingInterceptor implements Interceptor {
    private static final String TAG = "LoggingInterceptor";

    @Override
    public Response intercept(Interceptor.Chain chain) throws IOException {
        Request request = chain.request();
                
            //1.请求前--打印请求信息
        long t1 = System.nanoTime();
        Log.i(TAG, String.format("Sending request %s on %s%n%s",
                request.url(), chain.connection(), request.headers()));
                //网络请求,并继续执行拦截器链
        Response response = chain.proceed(request);

        //3.网络响应后--打印响应信息
        long t2 = System.nanoTime();
        Log.i(TAG, String.format("Received response for %s in %.1fms%n%s",
                response.request().url(), (t2 - t1) / 1e6d, response.headers()));

        return response;
    }
}

拦截器可以链接。假设同时具有压缩拦截器和校验拦截器,则确定是先压缩数据然后对校验和进行校验,还是先对数据进行校验和然后进行压缩。okhttp使用ArrayList来管理拦截器列表,并通过责任链模式按顺序执行拦截器。用户可传入的 interceptor 分为两类:Application IntercetorNetwork Interceptor

拦截器.png

Application interceptors 应用拦截器
okClient.addInterceptor(new LoggingInterceptor())
Application Interceptor 是第一个 Interceptor 因此它会被第一个执行,因此这里的 request 还是最原始的。而对于 response 而言呢,因为整个过程是递归的调用过程,因此它会在 CallServerInterceptor 执行完毕之后才会将 response 进行返回,因此在 Application Interceptor 这里得到的 response 就是最终的响应,虽然中间有重定向,但是这里只关心最终的 response

  1. 不需要去关心中发生的重定向和重试操作。因为它处于第一个拦截器,会获取到最终的响应
  2. 只会被调用一次,即使这个响应是从缓存中获取的
  3. 只关注最原始的请求,不去关系请求的资源是否发生了改变,我只关注最后的 response 结果而已
  4. 因为是第一个被执行的拦截器,因此它有权决定了是否要调用其他拦截,也就是 Chain.proceed() 方法是否要被执行
  5. 因为是第一个被执行的拦截器,因此它有可以多次调用 Chain.proceed() 方法,其实也就是相当与重新请求的作用了

Network Interceptors 网络拦截器
okClient.addNetworkInterceptor(new LoggingInterceptor())
NetwrokInterceptor 处于第 6 个拦截器中,它会经过 RetryAndFollowIntercptor 进行重定向并且也会通过 BridgeInterceptor 进行 request 请求头和 响应 resposne 的处理,因此这里可以得到的是更多的信息。在打印结果可以看到它内部是发生了一次重定向操作,所以NetworkInterceptor 可以比 Application Interceptor 得到更多的信息了

  1. 因为 NetworkInterceptor 是排在第 6 个拦截器中,因此可以操作经过 RetryAndFollowup 进行失败重试或者重定向之后得到的resposne
  2. 为响应直接从 CacheInterceptor 返回了
  3. 观察数据在网络中的传输
  4. 可以获得装载请求的连接。

注意事项

  1. 推荐让 OkHttpClient 保持单例,用同一个 OkHttpClient 实例来执行你的所有请求,因为每一个 OkHttpClient 实例都拥有自己的连接池和线程池,重用这些资源可以减少延时和节省资源,如果为每个请求创建一个 OkHttpClient 实例,显然就是一种资源的浪费。
  2. response.body().string()只调用一次
  3. 每一个CallRealCall)只能执行一次,否则会报异常
  4. 子线程加载数据后,主线程刷新数据

总结

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,142评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,298评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,068评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,081评论 1 291
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,099评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,071评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,990评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,832评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,274评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,488评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,649评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,378评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,979评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,625评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,796评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,643评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,545评论 2 352