目錄介紹
- 01.先提問一個問題
- 02.EventListener回調原理
- 03.請求開始結束監聽
- 04.dns解析開始結束監聽
- 05.連接開始結束監聽
- 06.TLS連接開始結束監聽
- 07.連接綁定和釋放監聽
- 08.request請求監聽
- 09.response響應監聽
- 10.如何監聽統計耗時
- 11.應用實踐之案例
01.先提問一個問題
- OkHttp如何進行各個請求環節的耗時統計呢?
- OkHttp 版本提供了EventListener接口,可以讓調用者接收一系列網絡請求過程中的事件,例如DNS解析、TSL/SSL連接、Response接收等。
- 通過繼承此接口,調用者可以監視整個應用中網絡請求次數、流量大小、耗時(比如dns解析時間,請求時間,響應時間等等)情況。
02.EventListener回調原理
- 先來看一下
public abstract class EventListener { // 按照請求順序回調 public void callStart(Call call) {} // 域名解析 public void dnsStart(Call call, String domainName) {} public void dnsEnd(Call call, String domainName, List<InetAddress> inetAddressList) {} // 釋放當前Transmitter的RealConnection public void connectionReleased(Call call, Connection connection) {} public void connectionAcquired(call, result){}; // 開始連接 public void connectStart(call, route.socketAddress(), proxy){} // 請求 public void requestHeadersStart(@NotNull Call call){} public void requestHeadersEnd(@NotNull Call call, @NotNull Request request) {} // 響應 public void requestBodyStart(@NotNull Call call) {} public void requestBodyEnd(@NotNull Call call, long byteCount) {} // 結束 public void callEnd(Call call) {} // 失敗 public void callFailed(Call call, IOException ioe) {} }
03.請求開始結束監聽
- callStart(Call call) 請求開始
- 當一個Call(代表一個請求)被同步執行或被添加異步隊列中時,即會調用這個回調方法。
- 需要說明這個方法是在dispatcher.executed/enqueue前執行的。
- 由於線程或事件流的限制,這里的請求開始並不是真正的去執行的這個請求。如果發生重定向和多域名重試時,這個方法也僅被調用一次。
final class RealCall implements Call { @Override public Response execute() throws IOException { eventListener.callStart(this); client.dispatcher().executed(this); Response result = getResponseWithInterceptorChain(); if (result == null) throw new IOException("Canceled"); return result; } @Override public void enqueue(Callback responseCallback) { eventListener.callStart(this); client.dispatcher().enqueue(new AsyncCall(responseCallback)); } }
- callFailed/callEnd 請求異常和請求結束
- 每一個callStart都對應着一個callFailed或callEnd。
- callFailed在兩種情況下被調用,第一種是在請求執行的過程中發生異常時。第二種是在請求結束后,關閉輸入流時產生異常時。
final class RealCall implements Call { @Override public Response execute() throws IOException { try { client.dispatcher().executed(this); Response result = getResponseWithInterceptorChain(); if (result == null) throw new IOException("Canceled"); return result; } catch (IOException e) { eventListener.callFailed(this, e); throw e; } } final class AsyncCall extends NamedRunnable { @Override protected void execute() { try { Response response = getResponseWithInterceptorChain(); } catch (IOException e) { eventListener.callFailed(RealCall.this, e); } } } } //第二種 public final class StreamAllocation { public void streamFinished(boolean noNewStreams, HttpCodec codec, long bytesRead, IOException e) { ... if (e != null) { eventListener.callFailed(call, e); } else if (callEnd) { eventListener.callEnd(call); } ... } }
- callEnd也有兩種調用場景。第一種也是在關閉流時。第二種是在釋放連接時。
public final class StreamAllocation { public void streamFinished(boolean noNewStreams, HttpCodec codec, long bytesRead, IOException e) { ... if (e != null) { eventListener.callFailed(call, e); } else if (callEnd) { eventListener.callEnd(call); } ... } public void release() { ... if (releasedConnection != null) { eventListener.connectionReleased(call, releasedConnection); eventListener.callEnd(call); } } }
- 為什么會將關閉流和關閉連接區分開?
- 在http2版本中,一個連接上允許打開多個流,OkHttp使用StreamAllocation來作為流和連接的橋梁。當一個流被關閉時,要檢查這條連接上還有沒有其他流,如果沒有其他流了,則可以將連接關閉了。
- streamFinished和release作用是一樣的,都是關閉當前流,並檢查是否需要關閉連接。不同的是,當調用者手動取消請求時,調用的是release方法,並由調用者負責關閉請求輸出流和響應輸入流。
04.dns解析開始結束監聽
- dnsStart開始
- 其中的lookup(String hostname)方法代表了域名解析的過程,dnsStart/dnsEnd就是在lookup前后被調用的
- DNS解析是請求DNS(Domain Name System)服務器,將域名解析成ip的過程。域名解析工作是由JDK中的InetAddress類完成的。
/** Prepares the socket addresses to attempt for the current proxy or host. */ private void resetNextInetSocketAddress(Proxy proxy) throws IOException { if (proxy.type() == Proxy.Type.SOCKS) { inetSocketAddresses.add(InetSocketAddress.createUnresolved(socketHost, socketPort)); } else { eventListener.dnsStart(call, socketHost); // Try each address for best behavior in mixed IPv4/IPv6 environments. List<InetAddress> addresses = address.dns().lookup(socketHost); if (addresses.isEmpty()) { throw new UnknownHostException(address.dns() + " returned no addresses for " + socketHost); } eventListener.dnsEnd(call, socketHost, addresses); } }
- 那么RouteSelector這個類是在哪里調用
public final class StreamAllocation { public StreamAllocation(ConnectionPool connectionPool, Address address, Call call, EventListener eventListener, Object callStackTrace) { this.routeSelector = new RouteSelector(address, routeDatabase(), call, eventListener); } }
05.連接開始結束監聽
- connectStart連接開始
- OkHttp是使用Socket接口建立Tcp連接的,所以這里的連接就是指Socket建立一個連接的過程。
- 當連接被重用時,connectStart/connectEnd不會被調用。當請求被重定向到新的域名后,connectStart/connectEnd會被調用多次。
private void connectSocket(int connectTimeout, int readTimeout, Call call, EventListener eventListener) 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); eventListener.connectStart(call, route.socketAddress(), proxy); }
- connectEnd連接結束
- 因為創建的連接有兩種類型(服務端直連和隧道代理),所以callEnd有兩處調用位置。為了在基於代理的連接上使用SSL,需要單獨發送CONECT請求。
- 在連接過程中,無論是Socket連接失敗,還是TSL/SSL握手失敗,都會回調connectEnd。
public void connect(int connectTimeout, int readTimeout, int writeTimeout, while (true) { try { establishProtocol(connectionSpecSelector, pingIntervalMillis, call, eventListener); eventListener.connectEnd(call, route.socketAddress(), route.proxy(), protocol); break; } catch (IOException e) { eventListener.connectFailed(call, route.socketAddress(), route.proxy(), null, e); } } private void connectTunnel(int connectTimeout, int readTimeout, int writeTimeout, Call call, EventListener eventListener) throws IOException { Request tunnelRequest = createTunnelRequest(); HttpUrl url = tunnelRequest.url(); for (int i = 0; i < MAX_TUNNEL_ATTEMPTS; i++) { connectSocket(connectTimeout, readTimeout, call, eventListener); eventListener.connectEnd(call, route.socketAddress(), route.proxy(), null); } }
06.TLS連接開始結束監聽
- 開始連接,代碼如下所示
- 在上面看到,在Socket建立連接后,會執行一個establishProtocol方法,這個方法的作用就是TSL/SSL握手。
- 當存在重定向或連接重試的情況下,secureConnectStart/secureConnectEnd會被調用多次。
private void establishProtocol(ConnectionSpecSelector connectionSpecSelector, int pingIntervalMillis, Call call, EventListener eventListener) throws IOException { if (route.address().sslSocketFactory() == null) { protocol = Protocol.HTTP_1_1; socket = rawSocket; return; } eventListener.secureConnectStart(call); connectTls(connectionSpecSelector); eventListener.secureConnectEnd(call, handshake); }
- 結合連接監聽可知
- 如果我們使用了HTTPS安全連接,在TCP連接成功后需要進行TLS安全協議通信,等TLS通訊結束后才能算是整個連接過程的結束,也就是說connectEnd在secureConnectEnd之后調用。
- 所以順序是這樣的
- connectStart ---> secureConnectStart ---> secureConnectEnd ---> ConnectEnd
07.連接綁定和釋放監聽
- 因為OkHttp是基於連接復用的,當一次請求結束后並不會馬上關閉當前連接,而是放到連接池中。
- 當有相同域名的請求時,會從連接池中取出對應的連接使用,減少了連接的頻繁創建和銷毀。
- 當根據一個請求從連接池取連接時,並打開輸入輸出流就是acquired,用完釋放流就是released。
- 如果直接復用StreamAllocation中的連接,則不會調用connectionAcquired/connectReleased。
private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout, int pingIntervalMillis, boolean connectionRetryEnabled) throws IOException { synchronized (connectionPool) { if (result == null) { // 第一次查緩存 Attempt to get a connection from the pool. // Attempt to get a connection from the pool. Internal.instance.get(connectionPool, address, this, null); } } if (releasedConnection != null) { eventListener.connectionReleased(call, releasedConnection); } if (foundPooledConnection) { eventListener.connectionAcquired(call, result); } synchronized (connectionPool) { if (canceled) throw new IOException("Canceled"); if (newRouteSelection) { //第二次查緩存 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) { //如果緩存沒有,則新建連接 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. if (foundPooledConnection) { eventListener.connectionAcquired(call, result); return result; } // Do TCP + TLS handshakes. This is a blocking operation. result.connect(connectTimeout, readTimeout, writeTimeout, pingIntervalMillis, connectionRetryEnabled, call, eventListener); routeDatabase().connected(result.route()); eventListener.connectionAcquired(call, result); return result; }
- connectionAcquired是在連接成功后被調用的。
- 但是在連接復用的情況下沒有連接步驟,connectAcquired會在獲取緩存連接后被調用。由於StreamAllocation是連接“Stream”和“Connection”的橋梁,所以在StreamAllocation中會持有一個RealConnection引用。StreamAllocation在查找可用連接的順序為:StreamAllocation.RealConnection -> ConnectionPool -> ConnectionPool -> new RealConnection
08.request請求監聽
- 在OkHttp中,HttpCodec負責對請求和響應按照Http協議進行編解碼,包含發送請求頭、發送請求體、讀取響應頭、讀取響應體。
- requestHeaders開始和結束,這個直接看CallServerInterceptor攔截器代碼即可。
public final class CallServerInterceptor implements Interceptor { @Override public Response intercept(Chain chain) throws IOException { RealInterceptorChain realChain = (RealInterceptorChain) chain; HttpCodec httpCodec = realChain.httpStream(); StreamAllocation streamAllocation = realChain.streamAllocation(); RealConnection connection = (RealConnection) realChain.connection(); Request request = realChain.request(); long sentRequestMillis = System.currentTimeMillis(); realChain.eventListener().requestHeadersStart(realChain.call()); httpCodec.writeRequestHeaders(request); realChain.eventListener().requestHeadersEnd(realChain.call(), request); if (HttpMethod.permitsRequestBody(request.method()) && request.body() != null) { if (responseBuilder == null) { // Write the request body if the "Expect: 100-continue" expectation was met. 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); } } return response; } }
09.response響應監聽
- responseHeadersStart和responseHeadersEnd代碼如下所示
public final class CallServerInterceptor implements Interceptor { @Override public Response intercept(Chain chain) throws IOException { Response.Builder responseBuilder = null; if (HttpMethod.permitsRequestBody(request.method()) && request.body() != null) { if ("100-continue".equalsIgnoreCase(request.header("Expect"))) { httpCodec.flushRequest(); realChain.eventListener().responseHeadersStart(realChain.call()); responseBuilder = httpCodec.readResponseHeaders(true); } } httpCodec.finishRequest(); if (responseBuilder == null) { realChain.eventListener().responseHeadersStart(realChain.call()); responseBuilder = httpCodec.readResponseHeaders(false); } int code = response.code(); if (code == 100) { // server sent a 100-continue even though we did not request one. // try again to read the actual response responseBuilder = httpCodec.readResponseHeaders(false); response = responseBuilder .request(request) .handshake(streamAllocation.connection().handshake()) .sentRequestAtMillis(sentRequestMillis) .receivedResponseAtMillis(System.currentTimeMillis()) .build(); code = response.code(); } realChain.eventListener() .responseHeadersEnd(realChain.call(), response); return response; } }
- responseBodyStart監聽
- 響應體的讀取有些復雜,要根據不同類型的Content-Type決定如何讀取響應體,例如固定長度的、基於塊(chunk)數據的、未知長度的。具體看openResponseBody方法里面的代碼。
- 同時Http1與Http2也有不同的解析方式。下面以Http1為例。
public final class Http1Codec implements HttpCodec { @Override public ResponseBody openResponseBody(Response response) throws IOException { streamAllocation.eventListener.responseBodyStart(streamAllocation.call); String contentType = response.header("Content-Type"); if (!HttpHeaders.hasBody(response)) { Source source = newFixedLengthSource(0); return new RealResponseBody(contentType, 0, Okio.buffer(source)); } if ("chunked".equalsIgnoreCase(response.header("Transfer-Encoding"))) { Source source = newChunkedSource(response.request().url()); return new RealResponseBody(contentType, -1L, Okio.buffer(source)); } long contentLength = HttpHeaders.contentLength(response); if (contentLength != -1) { Source source = newFixedLengthSource(contentLength); return new RealResponseBody(contentType, contentLength, Okio.buffer(source)); } return new RealResponseBody(contentType, -1L, Okio.buffer(newUnknownLengthSource())); } }
- responseBodyEnd監聽
- 由下面代碼可知,當響應結束后,會調用連接callEnd回調(如果異常則會調用callFailed回調)
public final class StreamAllocation { public void streamFinished(boolean noNewStreams, HttpCodec codec, long bytesRead, IOException e) { eventListener.responseBodyEnd(call, bytesRead); if (releasedConnection != null) { eventListener.connectionReleased(call, releasedConnection); } if (e != null) { eventListener.callFailed(call, e); } else if (callEnd) { eventListener.callEnd(call); } } }
10.如何監聽統計耗時
- 如何消耗記錄時間
- 在OkHttp庫中有一個EventListener類。該類是網絡事件的偵聽器。擴展這個類以監視應用程序的HTTP調用的數量、大小和持續時間。
- 所有啟動/連接/獲取事件最終將接收到匹配的結束/釋放事件,要么成功(非空參數),要么失敗(非空可拋出)。
- 比如,可以在開始鏈接記錄時間;dns開始,結束等方法解析記錄時間,可以計算dns的解析時間。
- 比如,可以在開始請求記錄時間,記錄connectStart,connectEnd等方法時間,則可以計算出connect連接時間。
- 代碼如下所示
- Eventlistener只適用於沒有並發的情況,如果有多個請求並發執行我們需要使用Eventlistener. Factory來給每個請求創建一個Eventlistener。
- 這個mRequestId是唯一值,可以選擇使用AtomicInteger自增+1的方式設置id,這個使用了cas保證多線程條件下的原子性特性。
/** * <pre> * @author yangchong * email : yangchong211@163.com * time : 2019/07/22 * desc : EventListener子類 * revise: * </pre> */ public class NetworkListener extends EventListener { private static final String TAG = "NetworkEventListener"; private static AtomicInteger mNextRequestId = new AtomicInteger(0); private String mRequestId ; public static Factory get(){ Factory factory = new Factory() { @NotNull @Override public EventListener create(@NotNull Call call) { return new NetworkListener(); } }; return factory; } @Override public void callStart(@NotNull Call call) { super.callStart(call); //mRequestId = mNextRequestId.getAndIncrement() + ""; //getAndAdd,在多線程下使用cas保證原子性 mRequestId = String.valueOf(mNextRequestId.getAndIncrement()); ToolLogUtils.i(TAG+"-------callStart---requestId-----"+mRequestId); saveEvent(NetworkTraceBean.CALL_START); saveUrl(call.request().url().toString()); } @Override public void dnsStart(@NotNull Call call, @NotNull String domainName) { super.dnsStart(call, domainName); ToolLogUtils.d(TAG, "dnsStart"); saveEvent(NetworkTraceBean.DNS_START); } @Override public void dnsEnd(@NotNull Call call, @NotNull String domainName, @NotNull List<InetAddress> inetAddressList) { super.dnsEnd(call, domainName, inetAddressList); ToolLogUtils.d(TAG, "dnsEnd"); saveEvent(NetworkTraceBean.DNS_END); } @Override public void connectStart(@NotNull Call call, @NotNull InetSocketAddress inetSocketAddress, @NotNull Proxy proxy) { super.connectStart(call, inetSocketAddress, proxy); ToolLogUtils.d(TAG, "connectStart"); saveEvent(NetworkTraceBean.CONNECT_START); } @Override public void secureConnectStart(@NotNull Call call) { super.secureConnectStart(call); ToolLogUtils.d(TAG, "secureConnectStart"); saveEvent(NetworkTraceBean.SECURE_CONNECT_START); } @Override public void secureConnectEnd(@NotNull Call call, @Nullable Handshake handshake) { super.secureConnectEnd(call, handshake); ToolLogUtils.d(TAG, "secureConnectEnd"); saveEvent(NetworkTraceBean.SECURE_CONNECT_END); } @Override public void connectEnd(@NotNull Call call, @NotNull InetSocketAddress inetSocketAddress, @NotNull Proxy proxy, @Nullable Protocol protocol) { super.connectEnd(call, inetSocketAddress, proxy, protocol); ToolLogUtils.d(TAG, "connectEnd"); saveEvent(NetworkTraceBean.CONNECT_END); } @Override public void connectFailed(@NotNull Call call, @NotNull InetSocketAddress inetSocketAddress, @NotNull Proxy proxy, @Nullable Protocol protocol, @NotNull IOException ioe) { super.connectFailed(call, inetSocketAddress, proxy, protocol, ioe); ToolLogUtils.d(TAG, "connectFailed"); } @Override public void requestHeadersStart(@NotNull Call call) { super.requestHeadersStart(call); ToolLogUtils.d(TAG, "requestHeadersStart"); saveEvent(NetworkTraceBean.REQUEST_HEADERS_START); } @Override public void requestHeadersEnd(@NotNull Call call, @NotNull Request request) { super.requestHeadersEnd(call, request); ToolLogUtils.d(TAG, "requestHeadersEnd"); saveEvent(NetworkTraceBean.REQUEST_HEADERS_END); } @Override public void requestBodyStart(@NotNull Call call) { super.requestBodyStart(call); ToolLogUtils.d(TAG, "requestBodyStart"); saveEvent(NetworkTraceBean.REQUEST_BODY_START); } @Override public void requestBodyEnd(@NotNull Call call, long byteCount) { super.requestBodyEnd(call, byteCount); ToolLogUtils.d(TAG, "requestBodyEnd"); saveEvent(NetworkTraceBean.REQUEST_BODY_END); } @Override public void responseHeadersStart(@NotNull Call call) { super.responseHeadersStart(call); ToolLogUtils.d(TAG, "responseHeadersStart"); saveEvent(NetworkTraceBean.RESPONSE_HEADERS_START); } @Override public void responseHeadersEnd(@NotNull Call call, @NotNull Response response) { super.responseHeadersEnd(call, response); ToolLogUtils.d(TAG, "responseHeadersEnd"); saveEvent(NetworkTraceBean.RESPONSE_HEADERS_END); } @Override public void responseBodyStart(@NotNull Call call) { super.responseBodyStart(call); ToolLogUtils.d(TAG, "responseBodyStart"); saveEvent(NetworkTraceBean.RESPONSE_BODY_START); } @Override public void responseBodyEnd(@NotNull Call call, long byteCount) { super.responseBodyEnd(call, byteCount); ToolLogUtils.d(TAG, "responseBodyEnd"); saveEvent(NetworkTraceBean.RESPONSE_BODY_END); } @Override public void callEnd(@NotNull Call call) { super.callEnd(call); ToolLogUtils.d(TAG, "callEnd"); saveEvent(NetworkTraceBean.CALL_END); generateTraceData(); NetWorkUtils.timeoutChecker(mRequestId); } @Override public void callFailed(@NotNull Call call, @NotNull IOException ioe) { super.callFailed(call, ioe); ToolLogUtils.d(TAG, "callFailed"); } private void generateTraceData(){ NetworkTraceBean traceModel = IDataPoolHandleImpl.getInstance().getNetworkTraceModel(mRequestId); Map<String, Long> eventsTimeMap = traceModel.getNetworkEventsMap(); Map<String, Long> traceList = traceModel.getTraceItemList(); traceList.put(NetworkTraceBean.TRACE_NAME_TOTAL,NetWorkUtils.getEventCostTime(eventsTimeMap,NetworkTraceBean.CALL_START, NetworkTraceBean.CALL_END)); traceList.put(NetworkTraceBean.TRACE_NAME_DNS,NetWorkUtils.getEventCostTime(eventsTimeMap,NetworkTraceBean.DNS_START, NetworkTraceBean.DNS_END)); traceList.put(NetworkTraceBean.TRACE_NAME_SECURE_CONNECT,NetWorkUtils.getEventCostTime(eventsTimeMap,NetworkTraceBean.SECURE_CONNECT_START, NetworkTraceBean.SECURE_CONNECT_END)); traceList.put(NetworkTraceBean.TRACE_NAME_CONNECT,NetWorkUtils.getEventCostTime(eventsTimeMap,NetworkTraceBean.CONNECT_START, NetworkTraceBean.CONNECT_END)); traceList.put(NetworkTraceBean.TRACE_NAME_REQUEST_HEADERS,NetWorkUtils.getEventCostTime(eventsTimeMap,NetworkTraceBean.REQUEST_HEADERS_START, NetworkTraceBean.REQUEST_HEADERS_END)); traceList.put(NetworkTraceBean.TRACE_NAME_REQUEST_BODY,NetWorkUtils.getEventCostTime(eventsTimeMap,NetworkTraceBean.REQUEST_BODY_START, NetworkTraceBean.REQUEST_BODY_END)); traceList.put(NetworkTraceBean.TRACE_NAME_RESPONSE_HEADERS,NetWorkUtils.getEventCostTime(eventsTimeMap,NetworkTraceBean.RESPONSE_HEADERS_START, NetworkTraceBean.RESPONSE_HEADERS_END)); traceList.put(NetworkTraceBean.TRACE_NAME_RESPONSE_BODY,NetWorkUtils.getEventCostTime(eventsTimeMap,NetworkTraceBean.RESPONSE_BODY_START, NetworkTraceBean.RESPONSE_BODY_END)); } private void saveEvent(String eventName){ NetworkTraceBean networkTraceModel = IDataPoolHandleImpl.getInstance().getNetworkTraceModel(mRequestId); Map<String, Long> networkEventsMap = networkTraceModel.getNetworkEventsMap(); networkEventsMap.put(eventName, SystemClock.elapsedRealtime()); } private void saveUrl(String url){ NetworkTraceBean networkTraceModel = IDataPoolHandleImpl.getInstance().getNetworkTraceModel(mRequestId); networkTraceModel.setUrl(url); } }
- 關於執行順序,打印結果如下所示
2020-09-22 20:50:15.351 28144-28277/cn.com.zwwl.bayuwen D/NetworkEventListener: dnsStart 2020-09-22 20:50:15.373 28144-28277/cn.com.zwwl.bayuwen D/NetworkEventListener: dnsEnd 2020-09-22 20:50:15.374 28144-28277/cn.com.zwwl.bayuwen D/NetworkEventListener: connectStart 2020-09-22 20:50:15.404 28144-28277/cn.com.zwwl.bayuwen D/NetworkEventListener: secureConnectStart 2020-09-22 20:50:15.490 28144-28277/cn.com.zwwl.bayuwen D/NetworkEventListener: secureConnectEnd 2020-09-22 20:50:15.490 28144-28277/cn.com.zwwl.bayuwen D/NetworkEventListener: connectEnd 2020-09-22 20:50:15.492 28144-28277/cn.com.zwwl.bayuwen D/NetworkEventListener: requestHeadersStart 2020-09-22 20:50:15.492 28144-28277/cn.com.zwwl.bayuwen D/NetworkEventListener: requestHeadersEnd 2020-09-22 20:50:15.528 28144-28277/cn.com.zwwl.bayuwen D/NetworkEventListener: responseHeadersStart 2020-09-22 20:50:15.528 28144-28277/cn.com.zwwl.bayuwen D/NetworkEventListener: responseHeadersEnd 2020-09-22 20:50:15.532 28144-28277/cn.com.zwwl.bayuwen D/NetworkEventListener: responseBodyStart 2020-09-22 20:50:15.534 28144-28277/cn.com.zwwl.bayuwen D/NetworkEventListener: responseBodyEnd 2020-09-22 20:50:15.547 28144-28277/cn.com.zwwl.bayuwen D/NetworkEventListener: callEnd
11.應用實踐之案例
- 網絡攔截分析,主要是分析網絡流量損耗,以及request,respond過程時間。打造網絡分析工具……
- 項目代碼地址:https://github.com/yangchong211/YCAndroidTool
- 如果你覺得這個攔截網絡助手方便了測試,以及開發中查看網絡數據,可以star一下……