基于OkHttp的Http指標(biāo)監(jiān)控

Http請求過程

image

指標(biāo)數(shù)據(jù)

1.入隊(duì)到請求結(jié)束耗時
2.dns查詢耗時
3.socket connect耗時
4.tls連接的耗時
5.請求發(fā)送耗時
6.響應(yīng)傳輸耗時
7.首包耗時
8.響應(yīng)解析耗時
9.Http錯誤,區(qū)分業(yè)務(wù)錯誤和請求錯誤

采集到以上指標(biāo)抚岗,結(jié)合數(shù)據(jù)可視化的工具反砌,可以對Http個階段的耗時和錯誤分布有直觀的感受载荔,同時對優(yōu)化業(yè)務(wù)Http請求提供數(shù)據(jù)支持凹联。

如何獲取指標(biāo)

獲取指標(biāo)數(shù)據(jù)首先需要找到產(chǎn)生指標(biāo)數(shù)據(jù)的關(guān)鍵代碼,然后插入收集代碼即可悦冀。
如果業(yè)務(wù)中使用的框架沒有源碼或者不重新打包源碼的情況下贤徒,如何插入代碼?
這個就需要使用到能夠?qū)崿F(xiàn)AOP的工具窟绷,在前面分享的Monitor中的提供注解和配置文件的方式锯玛,在指定函數(shù)中插入相關(guān)代碼的功能。這樣的實(shí)現(xiàn)方式也可以使監(jiān)控代碼和業(yè)務(wù)代碼分離兼蜈。

OkHttp框架

OkHttp是Android上最常用的Http請求框架攘残,OkHttp的最新版本已經(jīng)升級到4.0.x,實(shí)現(xiàn)也全部由java替換到了Kotlin为狸,API的一些使用也會有些不同歼郭。由于4.x的設(shè)備不是默認(rèn)支持TLSV1.2版本,OkHttp 3.13.x以上的版本需要在Android 5.0+(API level 21+)和Java 1.8的環(huán)境開發(fā)辐棒,不過OkHttp也為了支持4.x設(shè)備單獨(dú)創(chuàng)立了3.12.x分支病曾,本文中使用的OkHttp版本為3.12.3版本。

OkHttp整體流程

先引用個別人畫流程圖(原圖來自

image

請求過程分析

1.創(chuàng)建 OkHttpClient

new OkHttpClient()中會調(diào)用new OkHttpClient.Builder()方法,Builder()會設(shè)置一些的默認(rèn)值漾根。OkHttpClient()會把OkHttpClient.Builder()產(chǎn)生的對象中字段復(fù)制到對應(yīng)OkHttpClient對象的字段上泰涂,其中sslSocketFactory如果沒有在Builder中設(shè)置,OkHttp會獲取系統(tǒng)默認(rèn)的sslSocketFactory辐怕。

public OkHttpClient.Builder(){
  /**
   *異步請求的分發(fā)器逼蒙,其中使用 不限制線程數(shù),存活時間為60s的線程池來執(zhí)行異步請求
  * 默認(rèn)限制同時執(zhí)行的異步請求不操過64個寄疏,每個host的同時執(zhí)行的異步請求不操過5個
    *超過限制新的請求需要等待是牢。
    * */
   dispatcher = new Dispatcher();
  //支持的協(xié)議類型 Http1.0/1.1/2,QUIC 
   protocols = DEFAULT_PROTOCOLS;
   //支持TLS和ClearText
   connectionSpecs = DEFAULT_CONNECTION_SPECS;
   //請求事件通知器,大部分指標(biāo)數(shù)據(jù)都可以度過EventListener來獲取
   eventListenerFactory = EventListener.factory(EventListener.NONE);
  //系統(tǒng)級代理服務(wù)器
   proxySelector = ProxySelector.getDefault();
   if(proxySelector == null){
     proxySelector = new NullProxySelector();
   }
   //默認(rèn)不使用Cookie
   cookieJar = CookieJar.NO_COOKIES;
  //socket工廠
   socketFactory = SocketFactory.getDefault();
  //用于https主機(jī)名驗(yàn)證
   hostnameVerifier = OkHostnameVerifier.INSTANCE;
  //用于約束哪些證書是可信的陕截,可以用來證書固定
   certificatePinner = CertificatePinner.DEFAULT;
  //實(shí)現(xiàn)HTTP協(xié)議的信息認(rèn)證
   proxyAuthenticator = Authenticator.NONE;
   //實(shí)現(xiàn)HTTP協(xié)議的信息認(rèn)證
   authenticator = Authenticator.NONE;
   /**
    *實(shí)現(xiàn)多路復(fù)用機(jī)制連接池妖泄,最多保持5個空閑連接
   *每個空閑連接最多保持五分鐘
    * */
   connectionPool = new ConnectionPool();
  //dns解析,默認(rèn)使用系統(tǒng)InetAddress.getAllByName(hostname)
   dns = Dns.SYSTEM;
  //支持ssl重定向
   followSslRedirects = true;
  //支持重定向
   followRedirects = true;
  //連接失敗是否重試
   retryOnConnectionFailure = true;
  //請求超時時間艘策,0為不超時
   callTimeout = 0;
  //連接超時時間
   connectTimeout = 10_000;
  //socket讀超時時間
   readTimeout = 10_000;
  //socket寫超時時間
   writeTimeout = 10_000;
  //websocket 心跳間隔
   pingInterval = 0;
}

//獲取默認(rèn)的sslSocketFactory
X509TrustManager trustManager = Util.platformTrustManager();
this.sslSocketFactory = newSslSocketFactory(trustManager);
this.certificateChainCleaner = CertificateChainCleaner.get(trustManager);

2.Request執(zhí)行過程

從上面的流程圖可以看出,不管同步請求還是異步請求渊季,最終都會調(diào)用到 RealCall.getResponseWithInterceptorChain(),getResponseWithInterceptorChain() 再調(diào)用RealInterceptorChain.proceed(Request request)方法發(fā)起最終請求朋蔫,下面我們來分析一下這兩個方法的具體代碼。

 Response RealCall.getResponseWithInterceptorChain() throws IOException {
 //組裝所有的Interceptors
  List<Interceptor> interceptors = new ArrayList<>();
  //業(yè)務(wù)自定的Interceptors,通過OkHttpClient.Builid.addInterceptor添加
  interceptors.addAll(client.interceptors());
 //其他功能interceptors
  interceptors.add(retryAndFollowUpInterceptor);
  interceptors.add(new BridgeInterceptor(client.cookieJar()));
  interceptors.add(new CacheInterceptor(client.internalCache()));
  interceptors.add(new ConnectInterceptor(client));
 //如果不是forWebSocket請求却汉,添加通過OkHttpClient.Builid.addNetworkInterceptor添加的Interceptor
  if (!forWebSocket) {
    interceptors.addAll([client.networkInterceptors](http://client.networkinterceptors/)());
  }
 //真正發(fā)起網(wǎng)絡(luò)請求的Interceptor
  interceptors.add(new CallServerInterceptor(forWebSocket));
 //創(chuàng)建RealInterceptorChain
  Interceptor.Chain chain = new RealInterceptorChain(interceptors, null, null, null, 0,
      originalRequest, this, eventListener, client.connectTimeoutMillis(),
      client.readTimeoutMillis(), client.writeTimeoutMillis());
  //開始執(zhí)行請求
  Response response = chain.proceed(originalRequest);
 //如果請求被取消
  if (retryAndFollowUpInterceptor.isCanceled()) {
    closeQuietly(response);
    throw new IOException("Canceled");
  }
  return response;
}

接下來再看RealInterceptorChain.proceed(Request request)代碼

public Response proceed(Request request, StreamAllocation streamAllocation, HttpCodec httpCodec, 
    RealConnection connection) throws IOException { 
  /*
    *index代表當(dāng)前應(yīng)該執(zhí)行的Interceptor在Interceptor列表中的位置驯妄,如果超過 
    *Interceptor列表size,報錯
   *在RealCall.getResponseWithInterceptorChain()第一次調(diào)用proceed方法時傳遞的index值為0
  */
  if (index >= interceptors.size()) throw new AssertionError(); 
 //執(zhí)行次數(shù)+1
  calls++; 
 //httpCodec時在ConnectInterceptor創(chuàng)建的合砂,會對應(yīng)一個socket連接
  if (this.httpCodec != null && !this.connection.supportsUrl(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.httpCodec != null && calls > 1) { 
    throw new IllegalStateException("network interceptor " + interceptors.get(index - 1) 
        + " must call proceed() exactly once"); 
  } 
  //創(chuàng)建新的RealInterceptorChain青扔,通過改變index的值來實(shí)現(xiàn)調(diào)用Interceptor列表下一個位置的Interceptor
  RealInterceptorChain next = new RealInterceptorChain(interceptors, streamAllocation, httpCodec, 
      connection, index + 1, request, call, eventListener, connectTimeout, readTimeout, 
      writeTimeout); 
  //獲取當(dāng)前index位置的Interceptor
  Interceptor interceptor = interceptors.get(index); 
 //執(zhí)行當(dāng)前位置的interceptor,同時傳遞新創(chuàng)建的RealInterceptorChain,新的RealInterceptorChain中的index值是當(dāng)前Interceptor列表中下一個位置
  //interceptor.intercept中會調(diào)用新的RealInterceptorChain的proceed方法微猖,實(shí)現(xiàn)向后調(diào)用
  Response response = interceptor.intercept(next); 
  // 如果當(dāng)前RealInterceptorChain的httpCodec不為空谈息,確保下一個位置的Interceptor只被調(diào)用一次,httpCodec是在ConnectInterceptor中被賦值
  if (httpCodec != null && index + 1 < interceptors.size() && next.calls != 1) { 
    throw new IllegalStateException("network interceptor " + interceptor 
        + " must call proceed() exactly once"); 
  } 
  // response為空報錯
  if (response == null) { 
    throw new NullPointerException("interceptor " + interceptor + " returned null"); 
  } 
 // response.body為空報錯
  if (response.body() == null) { 
    throw new IllegalStateException( 
        "interceptor " + interceptor + " returned a response with no body"); 
  } 
  //返回response
  return response; 
}

從代碼可以看出 Interceptor 是 OkHttp 最核心的功能類凛剥,Interceptor 把實(shí)際的網(wǎng)絡(luò)請求侠仇、緩存、透明壓縮等功能都統(tǒng)一了起來犁珠,每一個功能都實(shí)現(xiàn)為一個Interceptor逻炊,它們最終組成一個了Interceptor.Chain的責(zé)任鏈,其中每個 Interceptor 都可能完成 Request到 Response 轉(zhuǎn)變的任務(wù)犁享,循著責(zé)任鏈讓每個 Interceptor 自行決定能否完成任務(wù)以及怎么完成任務(wù)余素,完成網(wǎng)絡(luò)請求這件事也從 RealCall 類中剝離了出來,簡化了各自的責(zé)任和邏輯炊昆,代碼變得優(yōu)雅桨吊。
這些Interceptor最為關(guān)鍵的兩個Interceptor是ConnectInterceptor和CallServerInterceptor,ConnectInterceptor的主要功能是在連接池里找到可復(fù)用連接窑眯,如果沒有屏积,就創(chuàng)建新的socket,進(jìn)行tls握手磅甩,將socket用Okio進(jìn)行包裹炊林,創(chuàng)建HttpCodec。CallServerInterceptor使用HttpCodec進(jìn)行相關(guān)協(xié)議的傳輸和解析卷要。下面對ConnectInterceptor中findConnect過程和CallServerInterceptor請求過程做一個分析渣聚。

3.ConnectInterceptor findConnection過程分析

在ConnectInterceptor中,會為本次請求創(chuàng)建可用的RealConnection,首先會從連接池中找到能夠復(fù)用的連接僧叉,如果沒有就創(chuàng)建新的socket奕枝,然后使用RealConnection創(chuàng)建HttpCodec。創(chuàng)建RealConnection的方法調(diào)用鏈路為StreamAllocation.newStream()-> StreamAllocation.findHealthyConnection()->StreamAllocation.findConnection()瓶堕,findConnection()是創(chuàng)建連接的主要代碼隘道。

private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout, int pingIntervalMillis, boolean connectionRetryEnabled) throws IOException {
    boolean foundPooledConnection = false;
    //最終找到的connection
    RealConnection result = null;
    Route selectedRoute = null;
    //需要釋放的connection
    Connection releasedConnection;
    //需要關(guān)閉的socket
    Socket toClose;
    synchronized (connectionPool) {
        if (released) throw new IllegalStateException("released");
        if (codec != null) throw new IllegalStateException("codec != null");
        if (canceled) throw new IOException("Canceled");
        /**
         *如果遇到重定向到同一個地址的情況下,在RetryAndFollowUpInterceptor中會使用已經(jīng)分配的StreamAllocation
         *進(jìn)行重定向請求郎笆,這個時候的connection不為空,但是這個connection不一定時有效的連接谭梗。
         * **/
        releasedConnection = this.connection;
        /**
         *如果已經(jīng)存在RealConnection,但是不能用來創(chuàng)建新的Stream
         *就設(shè)置this.connection=null,同時返回要關(guān)閉的socket
         *當(dāng)前請求第一次執(zhí)行時releaseIfNoNewStreams()不進(jìn)行任何操作
         * */
        toClose = releaseIfNoNewStreams();
        /**
         *
         * 這時候this.connection不為空
         * 表示原來的connection可以用來創(chuàng)建新的Stream
         * 當(dāng)前請求第一次執(zhí)行時this.connection=null
         *
         * */
        if (this.connection != null) {
            // We had an already-allocated connection and it's good.
            result = this.connection;
            releasedConnection = null;
        }
        /**
         * reportedAcquired會在新建socket或者從連接池
         * 獲取到有效RealConnection時賦值為true
         * 當(dāng)前請求第一次執(zhí)行時reportedAcquired=fasle
         * */
        if (!reportedAcquired) {
            // If the connection was never reported acquired, don't report it as released!
            releasedConnection = null;
        }
        /**
         * 如果還沒找到目標(biāo)RealConnection
         * 嘗試從連接池中獲取
         * */
        if (result == null) {
            /**
             * 對于非http/2協(xié)議宛蚓,如果已經(jīng)存在不超過過RealConnection復(fù)用的最大值且協(xié)議激捏,證書都一致
             * 這個RealConnection可以用來復(fù)用
             * 如果從連接池中獲取RealConnection,會調(diào)用
             * streamAllocation.acquire()設(shè)置connection為新值
             * */
            Internal.instance.get(connectionPool, address, this, null);
            /**
             * connection != null表示從連接池獲取到合適的
             * RealConnection凄吏,設(shè)置foundPooledConnection = true;
             * */
            if (connection != null) {
                foundPooledConnection = true;
                result = connection;
            } else {
                selectedRoute = route;
            }
        }
    }
    /**
     * close當(dāng)前的需要關(guān)閉的socket
     * */
    closeQuietly(toClose);
    /**
     * 如果當(dāng)前的RealConnection需要釋放远舅,調(diào)用eventListener
     * */
    if (releasedConnection != null) {
        eventListener.connectionReleased(call, releasedConnection);
    }
    /**
     * 如果從連接池獲取到RealConnection闰蛔,調(diào)用eventListener
     * */
    if (foundPooledConnection) {
        eventListener.connectionAcquired(call, result);
    }
    /**
     * 當(dāng)前RealConnection可以繼續(xù)使用或者從連接池中找到合適的RealConnection
     * 返回這個RealConnection
     * */
    if (result != null) {
        // If we found an already-allocated or pooled connection, we're done.
        return result;
    }
    // If we need a route selection, make one. This is a blocking operation.
    /**
     * routeSelector在StreamAllocation構(gòu)造方法中被創(chuàng)建
     * 連接池重新尋找
     * 請求第一次執(zhí)行時routeSelector.next()會進(jìn)行域名解析工作
     * */
    boolean newRouteSelection = false;
    if (selectedRoute == null && (routeSelection == null || !routeSelection.hasNext())) {
        newRouteSelection = true;
        routeSelection = routeSelector.next();
    }
    synchronized (connectionPool) {
        if (canceled) throw new IOException("Canceled");
        if (newRouteSelection) {
            // Now that we have a set of IP addresses, make another attempt at getting a connection from
            // the pool. This could match due to connection coalescing.
            List<Route> routes = routeSelection.getAll();
            for (int i = 0, size = routes.size(); i < size; i++) {
                Route route = routes.get(i);
                Internal.instance.get(connectionPool, address, this, route);
                if (connection != null) {
                    foundPooledConnection = true;
                    result = connection;
                    this.route = route;
                    break;
                }
            }
        }
        if (!foundPooledConnection) {
            if (selectedRoute == null) {
                selectedRoute = routeSelection.next();
            }
            // Create a connection and assign it to this allocation immediately. This makes it possible
            // for an asynchronous cancel() to interrupt the handshake we're about to do.
            route = selectedRoute;
            refusedStreamCount = 0;
            result = new RealConnection(connectionPool, selectedRoute);
            acquire(result, false);
        }
    }
    // If we found a pooled connection on the 2nd time around, we're done.
    /**
     * 如果在連接池重新找到合適的RealConnection,返回
     * */
    if (foundPooledConnection) {
        eventListener.connectionAcquired(call, result);
        return result;
    }
    /**
     * 如果還沒有找到,就需要創(chuàng)建新的RealConnect
     * 生成新的socket,建立Tls,并加入ConnectPool
     * */
    // Do TCP + TLS handshakes. This is a blocking operation.
    result.connect(connectTimeout, readTimeout, writeTimeout, pingIntervalMillis,
            connectionRetryEnabled, call, eventListener);
    routeDatabase().connected(result.route());
    Socket socket = null;
    synchronized (connectionPool) {
        reportedAcquired = true;
        // Pool the connection.
        Internal.instance.put(connectionPool, result);
        // If another multiplexed connection to the same address was created concurrently, then
        // release this connection and acquire that one.
        if (result.isMultiplexed()) {
            socket = Internal.instance.deduplicate(connectionPool, address, this);
            result = connection;
        }
    }
    closeQuietly(socket);
    eventListener.connectionAcquired(call, result);
    return result;
}

4.CallServerInterceptor程分析

public Response intercept(Chain chain) throws IOException {
  RealInterceptorChain realChain = (RealInterceptorChain) chain;
 //Http解析器图柏,在ConncetInterceptor中創(chuàng)建
  HttpCodec httpCodec = realChain.httpStream();
  StreamAllocation streamAllocation = realChain.streamAllocation();
  /**
   *請求的使用的連接序六,在ConncetInterceptor中產(chǎn)生
   *連接可能是從ConnectPool中選擇或者重新創(chuàng)建出來
  **/
  RealConnection connection = (RealConnection) realChain.connection();
  Request request = realChain.request();
  long sentRequestMillis = System.currentTimeMillis();
  realChain.eventListener().requestHeadersStart(realChain.call());
  /**
   * 通過httpCodec中用Okio包裹的socket寫請求頭
  **/
  httpCodec.writeRequestHeaders(request);
  realChain.eventListener().requestHeadersEnd(realChain.call(), request);
  Response.Builder responseBuilder = null;
  /**
   * 如果請求有請求body,發(fā)送body
   **/
  if (HttpMethod.permitsRequestBody(request.method()) && request.body() != null) {
 request body.
 /**
   * 請求頭是Expect: 100-continue爆办,先讀響應(yīng)頭
 *    httpCodec.readResponseHeaders方法讀取到狀態(tài)碼100時难咕, 會返回null
**/
    if ("100-continue".equalsIgnoreCase(request.header("Expect"))) {
      httpCodec.flushRequest();
      realChain.eventListener().responseHeadersStart(realChain.call());
      responseBuilder = httpCodec.readResponseHeaders(true);
    }
 /**
   * 如果正常發(fā)送請求body部分
   *  請求頭有Expect: 100-continue,但是服務(wù)器沒有返回狀態(tài)碼100距辆,且不是Http/2協(xié)議余佃,關(guān)閉當(dāng)前連接
**/
    if (responseBuilder == null) {
      realChain.eventListener().requestBodyStart(realChain.call());
      long contentLength = request.body().contentLength();
      CountingSink requestBodyOut =
          new CountingSink(httpCodec.createRequestBody(request, contentLength));
      BufferedSink bufferedRequestBody = Okio.buffer(requestBodyOut);
      request.body().writeTo(bufferedRequestBody);
      bufferedRequestBody.close();
      realChain.eventListener()
          .requestBodyEnd(realChain.call(), requestBodyOut.successfulCount);
    } else if (!connection.isMultiplexed()) {
      streamAllocation.noNewStreams();
    }
  }
  httpCodec.finishRequest();
 /**
   * 正常情況下,讀響應(yīng)headers
**/
  if (responseBuilder == null) {
    realChain.eventListener().responseHeadersStart(realChain.call());
    responseBuilder = httpCodec.readResponseHeaders(false);
  }
  Response response = responseBuilder
      .request(request)
      .handshake(streamAllocation.connection().handshake())
      .sentRequestAtMillis(sentRequestMillis)
      .receivedResponseAtMillis(System.currentTimeMillis())
      .build();
  int code = response.code();
  if (code == 100) {
    responseBuilder = httpCodec.readResponseHeaders(false);
    response = responseBuilder
            .request(request)
            .handshake(streamAllocation.connection().handshake())
            .sentRequestAtMillis(sentRequestMillis)
            .receivedResponseAtMillis(System.currentTimeMillis())
            .build();
    code = response.code();
  }
 /**
   * 讀響應(yīng)headers結(jié)束
**/
  realChain.eventListener()
          .responseHeadersEnd(realChain.call(), response);
/**
   * 如果是forWebSocket跨算,且 code == 101返回空響應(yīng)
*   其他返回 RealResponseBody對象
**/
  if (forWebSocket && code == 101) {
    // Connection is upgrading, but we need to ensure interceptors see a non-null response body.
    response = response.newBuilder()
        .body(Util.EMPTY_RESPONSE)
        .build();
  } else {
   /**
  *將
  *無響應(yīng)內(nèi)容
  * chunk內(nèi)容,
  * 存在contenlength內(nèi)容
  * 不存在contenlength內(nèi)容
  *  的響應(yīng)body包裹成 RealResponseBody對象
 *
**/
    response = response.newBuilder()
        .body(httpCodec.openResponseBody(response))
        .build();
  }
  /**
  * 服務(wù)端關(guān)閉要求連接
**/
  if ("close".equalsIgnoreCase(response.request().header("Connection"))
      || "close".equalsIgnoreCase(response.header("Connection"))) {
    streamAllocation.noNewStreams();
  }
   /**
    * code是204/205爆土,contentLength()還大于0,拋出協(xié)議錯誤
   **/
  if ((code == 204 || code == 205) && response.body().contentLength() > 0) {
    throw new ProtocolException(
        "HTTP " + code + " had non-zero Content-Length: " + response.body().contentLength());
  }
  return response;
}

CallServerInterceptor執(zhí)行完成后返回的是一個只讀取了響應(yīng)headers诸蚕,但是還沒有讀取body的Response步势,OkHttp網(wǎng)絡(luò)請求部分的代碼到此就結(jié)束了,后續(xù)的parseReponse都在更上層的框架中背犯,比如Retrofit就是在OkHttpCall.parseReponse()方法中調(diào)用serviceMethod.toResponse(catchingBody)中調(diào)用GsonConvter或者其他Convertor來進(jìn)行處理坏瘩。

獲取指標(biāo)具體實(shí)現(xiàn)

對于Http 請求耗時,異常漠魏,數(shù)據(jù)大小,狀態(tài)碼 的獲取倔矾,直接使用前面實(shí)現(xiàn)的MAOP,攔截OkHttpClient.Builder的build方法加入統(tǒng)計(jì)Interceptor ,DNSLookUp 耗時柱锹,連接耗時哪自,ssl耗時,通過設(shè)置EventListener.Factory禁熏,可以直接收集壤巷。解析耗時需要攔截上層框架的parseReponse方法進(jìn)行收集。
首包時間需要攔截OkHttp讀請求數(shù)據(jù)的方法來實(shí)現(xiàn)瞧毙,OKHttpClient 最終調(diào)用CallServerInterceptor,關(guān)鍵代碼就是讀取readResponseHeaders的時機(jī)胧华。


image

MAOP 實(shí)現(xiàn)

使用前面提供的MAOP功能,在AOP配置文件中加入宙彪,攔截OkHttpClient的builder方法和Http1Codec的readHeaderLine方法和okhttp3.internal.http2.Http2Stream的takeResponseHeaders方法的配置

image
image

在攔截OkHttpClient的Builder的build()方法中加入統(tǒng)計(jì)Interceptor和EventListenerFactory

image

首包的時間通過:認(rèn)為第一次讀響應(yīng)頭返回時為首包時間撑柔,攔截okhttp3.internal.http1.Http1Code.readHeaderLine的方法和okhttp3.internal.http2.Http2Stream.takeResponseHeaders計(jì)算首包時間


image

Retrofit parse耗時收集

AOP配置文件中加入對retrofit2.OKHttp.parseResponse攔截的配置


20_32_45__09_01_2019.jpg

Method回掉中處理相關(guān)的數(shù)據(jù)


20_35_17__09_01_2019.jpg

綜上,這個方案基本能實(shí)現(xiàn)Http基本指標(biāo)的獲取您访,但是有些細(xì)節(jié)還需完善,可以微信關(guān)注知識星球共同交流


最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末剪决,一起剝皮案震驚了整個濱河市灵汪,隨后出現(xiàn)的幾起案子檀训,更是在濱河造成了極大的恐慌,老刑警劉巖享言,帶你破解...
    沈念sama閱讀 216,651評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件峻凫,死亡現(xiàn)場離奇詭異,居然都是意外死亡览露,警方通過查閱死者的電腦和手機(jī)荧琼,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,468評論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來差牛,“玉大人命锄,你說我怎么就攤上這事∑” “怎么了脐恩?”我有些...
    開封第一講書人閱讀 162,931評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長侦讨。 經(jīng)常有香客問我驶冒,道長,這世上最難降的妖魔是什么韵卤? 我笑而不...
    開封第一講書人閱讀 58,218評論 1 292
  • 正文 為了忘掉前任骗污,我火速辦了婚禮,結(jié)果婚禮上沈条,老公的妹妹穿的比我還像新娘需忿。我一直安慰自己,他們只是感情好拍鲤,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,234評論 6 388
  • 文/花漫 我一把揭開白布贴谎。 她就那樣靜靜地躺著,像睡著了一般季稳。 火紅的嫁衣襯著肌膚如雪擅这。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,198評論 1 299
  • 那天景鼠,我揣著相機(jī)與錄音仲翎,去河邊找鬼。 笑死铛漓,一個胖子當(dāng)著我的面吹牛溯香,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播浓恶,決...
    沈念sama閱讀 40,084評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼玫坛,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了包晰?” 一聲冷哼從身側(cè)響起湿镀,我...
    開封第一講書人閱讀 38,926評論 0 274
  • 序言:老撾萬榮一對情侶失蹤炕吸,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后勉痴,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體赫模,經(jīng)...
    沈念sama閱讀 45,341評論 1 311
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,563評論 2 333
  • 正文 我和宋清朗相戀三年蒸矛,在試婚紗的時候發(fā)現(xiàn)自己被綠了瀑罗。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,731評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡雏掠,死狀恐怖斩祭,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情磁玉,我是刑警寧澤停忿,帶...
    沈念sama閱讀 35,430評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站蚊伞,受9級特大地震影響席赂,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜时迫,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,036評論 3 326
  • 文/蒙蒙 一颅停、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧掠拳,春花似錦癞揉、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,676評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至姐刁,卻和暖如春芥牌,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背聂使。 一陣腳步聲響...
    開封第一講書人閱讀 32,829評論 1 269
  • 我被黑心中介騙來泰國打工壁拉, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人柏靶。 一個月前我還...
    沈念sama閱讀 47,743評論 2 368
  • 正文 我出身青樓弃理,卻偏偏與公主長得像,于是被迫代替她去往敵國和親屎蜓。 傳聞我的和親對象是個殘疾皇子痘昌,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,629評論 2 354