网络库OKHTTP(2)面试题

序、慢慢来才是最快的方法。

背景

OkHttp 是一套处理 HTTP 网络请求的依赖库,由 Square 公司设计研发并开源,目前可以在 Java 和 Kotlin 中使用。对于 Android App 来说,OkHttp 现在几乎已经占据了所有的网络请求操作。

OKHttp源码官网 

问1:OKHttp有哪些拦截器,分别起什么作用

必考题,头两年没考,今年必考。

OKHTTP的拦截器是把所有的拦截器放到一个list里,然后每次依次执行拦截器,并且在每个拦截器分成三部分:

  • 预处理拦截器内容
  • 通过proceed方法把请求交给下一个拦截器
  • 下一个拦截器处理完成并返回,后续处理工作。

这样依次下去就形成了一个链式调用,看看源码,具体有哪些拦截器:

getResponseWithInterceptorChain()

 @Throws(IOException::class)internal fun getResponseWithInterceptorChain(): Response {// Build a full stack of interceptors.val interceptors = mutableListOf<Interceptor>()interceptors += client.interceptorsinterceptors += RetryAndFollowUpInterceptor(client)interceptors += BridgeInterceptor(client.cookieJar)interceptors += CacheInterceptor(client.cache)interceptors += ConnectInterceptorif (!forWebSocket) {interceptors += client.networkInterceptors}interceptors += CallServerInterceptor(forWebSocket)val chain = RealInterceptorChain(call = this,interceptors = interceptors,index = 0,exchange = null,request = originalRequest,connectTimeoutMillis = client.connectTimeoutMillis,readTimeoutMillis = client.readTimeoutMillis,writeTimeoutMillis = client.writeTimeoutMillis)var calledNoMoreExchanges = falsetry {val response = chain.proceed(originalRequest)if (isCanceled()) {response.closeQuietly()throw IOException("Canceled")}return response} catch (e: IOException) {calledNoMoreExchanges = truethrow noMoreExchanges(e) as Throwable} finally {if (!calledNoMoreExchanges) {noMoreExchanges(null)}}}

根据源码可知,一共七个拦截器:

  • addInterceptor(Interceptor),这是由开发者设置的,会按照开发者的要求,在所有的拦截器处理之前进行最早的拦截处理,比如一些公共参数,Header都可以在这里添加。
  • RetryAndFollowUpInterceptor,这里会对连接做一些初始化工作,以及请求失败的充实工作,重定向的后续请求工作。跟他的名字一样,就是做重试工作还有一些连接跟踪工作。
  • BridgeInterceptor,这里会为用户构建一个能够进行网络访问的请求,同时后续工作将网络请求回来的响应Response转化为用户可用的Response,比如添加文件类型,content-length计算添加,gzip解包。
  • CacheInterceptor,这里主要是处理cache相关处理,会根据OkHttpClient对象的配置以及缓存策略对请求值进行缓存,而且如果本地有了可⽤的Cache,就可以在没有网络交互的情况下就返回缓存结果。
  • ConnectInterceptor,这里主要就是负责建立连接了,会建立TCP连接或者TLS连接,以及负责编码解码的HttpCodec
  • networkInterceptors,这里也是开发者自己设置的,所以本质上和第一个拦截器差不多,但是由于位置不同,所以用处也不同。这个位置添加的拦截器可以看到请求和响应的数据了,所以可以做一些网络调试。
  • CallServerInterceptor,这里就是进行网络数据的请求和响应了,也就是实际的网络I/O操作,通过socket读写数据。

问2:OKHttp请求的流程?

我们知道Okhttp中通过okhttpClient对象是通过Builder对象初始化出来的,此处Builder的用法是建造者模式,建造者模式主要是分离出外部类的属性初始化,而初始化属性交给了内部类Buidler类,这么做的好处是外部类不用关心属性的初始化。 而在初始化的时候有interceptorsnetworkInterceptors两种拦截器的初始化,还有dispatcher(分发器)的初始化,以及后面需要讲到的cache(缓存)初始化等。

初始化完了后通过builder的build方法构造出okhttpClient对象,该类被称作客户端类,通过它的newCall方法返回RealCall对象,在newCall过程的过程中需要request的信息,request信息包装了url、method、headers、body等信息。最后通过RealCall的同步或异步方法交给了okhttpClientdispatcher来处理,在处理同步或异步之前都会判断有没有正在executed,所以我们不能对同一个RealCall调用异步或同步方法。

在异步的时候会把RealCall给包装成一个AsyncCall,它是一个runnable对象。接着就来到了分发器异步处理部分,首先会把AsyncCall加入到readyAsyncCalls的集合中,该集合表示准备阶段的请求集合,紧接着从runningAsyncCalls(该集合装的都是要即将请求的集合)readyAsyncCalls集合中找相同host的AsyncCall,如果找到了会把当中记录的相同host的个数给该AsyncCall注意这里保存host个数用的原子性的AtomicInteger来记录的

接着会去判断最大的请求是否大于64以及相同host是否大于5个,这里也是okhttp面试高频知识点,如果都通过的话,会把当前的AsyncCall的相同host记录数加一,接着会加入到runningAsyncCalls集合中,接着循环遍历刚符合条件的AsyncCall,通过线程池去执行AsyncCall,注意此处的线程池的配置是没有核心线程,总的线程个数是没有限制的,也就是说都是非核心线程,并且个数没有限制,非核心线程等待的时间是60秒,并且使用的任务队列是SynchronousQueue,它是一个没有容量的阻塞队列,只会当里面没有任务的时候,才能往里面放任务,当放完之后,只能等它的任务被取走才能放,这不就是jdk里面提供的Executors.newCachedThreadPool线程池吗,可能是okhttp想自己定义线程工厂的参数吧,定义线程的名字。

所以到这里才会进入到子线程,由于AsyncCall是一个runnable,因此最终执行来到了它的run方法吧,run方法最终会走到execute方法,该方法来到了okhttp最有意思的单链表结构的拦截器部分,它会把所有的拦截器组装成一个集合,然后传给RealInterceptorChainprocess方法,在该方法中,会先把下一个RealInterceptorChain初始化出来,然后把下一个RealInterceptorChain传给当前Interceptor的intercept方法,最终一个个的response返回到AsyncCallexecute方法。

处理完当前的AsyncCall后,会交给dispatcher,它会将该AsyncCall的host数减一,并且把它从runningAsyncCalls集合中移除,接着再从readyAsyncCalls集合中拿剩下的AsyncCall继续执行,直到执行完readyAsyncCalls里面的AsyncCall

问3:OkHttp怎么实现连接池?

总结

连接池部分主要是在RealConnectionPool类中,该类用connections(双端队列)存储所有的连接,cleanupRunnable是专门用来清除超时的RealConnection,既然有清除的任务,那肯定有清除的线程池,没错,该线程池(executor)跟okhttp处理异步时候的线程池是一样的,keepAliveDurationNs表示每一个连接keep-alive的时间,默认是5分钟,maxIdleConnections连接池的最大容量,默认是5个。RealConnection中有transmitters字段,用来保存该连接的transmitter个数,通过里面的transmitter个数来标记该RealConnection有没有在使用中。

连接池的意义?

  • 频繁的进行建立Sokcet连接(TCP三次握手)和断开Socket(TCP四次分手)是非常消耗网络资源和浪费时间的,HTTP中的keepalive连接对于 降低延迟和提升速度有非常重要的作用。
  • 复用连接就需要对连接进行管理,这里就引入了连接池的概念。
  • Okhttp支持5个并发KeepAlive,默认链路生命为5分钟(链路空闲后,保持存活的时间),连接池有ConectionPool实现,对连接进行回收和管理。

为什么需要连接池?

频繁的进行建立Sokcet连接和断开Socket是非常消耗网络资源和浪费时间的,所以HTTP中的keepalive连接对于降低延迟和提升速度有非常重要的作用。

keepalive机制是什么呢?

也就是可以在一次TCP连接中可以持续发送多份数据而不会断开连接。所以连接的多次使用,也就是复用就变得格外重要了,而复用连接就需要对连接进行管理,于是就有了连接池的概念。

OkHttp中使用ConectionPool实现连接池,默认支持5个并发KeepAlive,默认链路生命为5分钟。

怎么实现的?

        1,首先,ConectionPool中维护了一个双端队列Deque,也就是两端都可以进出的队列,用来存储连接。

        2.然后在ConnectInterceptor,也就是负责建立连接的拦截器中,首先会找可用连接,也就是从连接池中去获取连接,具体的就是会调用到ConectionPool的get方法。

RealConnection get(Address address, StreamAllocation streamAllocation, Route route) {assert (Thread.holdsLock(this));for (RealConnection connection : connections) {if (connection.isEligible(address, route)) {streamAllocation.acquire(connection, true);return connection;}}return null;}

也就是遍历了双端队列,如果连接有效,就会调用acquire方法计数并返回这个连接。

        3.如果没找到可用连接,就会创建新连接,并会把这个建立的连接加入到双端队列中,同时开始运行线程池中的线程,其实就是调用了ConectionPool的put方法。

public final class ConnectionPool {void put(RealConnection connection) {if (!cleanupRunning) {//没有连接的时候调用cleanupRunning = true;executor.execute(cleanupRunnable);}connections.add(connection);}
}

        4.其实这个线程池中只有一个线程,是用来清理连接的,也就是上述的cleanupRunnable

private final Runnable cleanupRunnable = new Runnable() {@Overridepublic void run() {while (true) {//执行清理,并返回下次需要清理的时间。long waitNanos = cleanup(System.nanoTime());if (waitNanos == -1) return;if (waitNanos > 0) {long waitMillis = waitNanos / 1000000L;waitNanos -= (waitMillis * 1000000L);synchronized (ConnectionPool.this) {//在timeout时间内释放锁try {ConnectionPool.this.wait(waitMillis, (int) waitNanos);} catch (InterruptedException ignored) {}}}}}};

这个runnable会不停的调用cleanup方法清理线程池,并返回下一次清理的时间间隔,然后进入wait等待。

怎么清理的呢?看看源码:

long cleanup(long now) {synchronized (this) {//遍历连接for (Iterator<RealConnection> i = connections.iterator(); i.hasNext(); ) {RealConnection connection = i.next();//检查连接是否是空闲状态,//不是,则inUseConnectionCount + 1//是 ,则idleConnectionCount + 1if (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;}}//如果超过keepAliveDurationNs或maxIdleConnections,//从双端队列connections中移除if (longestIdleDurationNs >= this.keepAliveDurationNs|| idleConnectionCount > this.maxIdleConnections) {      connections.remove(longestIdleConnection);} else if (idleConnectionCount > 0) {      //如果空闲连接次数>0,返回将要到期的时间// A connection will be ready to evict soon.return keepAliveDurationNs - longestIdleDurationNs;} else if (inUseConnectionCount > 0) {// 连接依然在使用中,返回保持连接的周期5分钟return keepAliveDurationNs;} else {// No connections, idle or in use.cleanupRunning = false;return -1;}}closeQuietly(longestIdleConnection.socket());// Cleanup again immediately.return 0;}

也就是当如果空闲连接maxIdleConnections超过5个或者keepalive时间大于5分钟,则将该连接清理掉。

这里有个问题,怎样属于空闲连接?

  public void acquire(RealConnection connection, boolean reportedAcquired) {assert (Thread.holdsLock(connectionPool));if (this.connection != null) throw new IllegalStateException();this.connection = connection;this.reportedAcquired = reportedAcquired;connection.allocations.add(new StreamAllocationReference(this, callStackTrace));}

RealConnection中,有一个StreamAllocation虚引用列表allocations。每创建一个连接,就会把连接对应的StreamAllocationReference添加进该列表中,如果连接关闭以后就将该对象移除。

        5.连接池的工作就这么多,并不复杂,主要就是管理双端队列Deque<RealConnection>,可以用的连接就直接用,然后定期清理连接,同时通过对StreamAllocation的引用计数实现自动回收。

最后

  • 连接池是为了解决频繁的进行建立Sokcet连接(TCP三次握手)和断开Socket(TCP四次分手)。
  • Okhttp的连接池支持最大5个链路的keep-alive连接,并且默认keep-alive的时间是5分钟。
  • 连接池实现的类是RealConnectionPool,它负责存储与清除的工作,存储是通过ArrayDeque的双端队列存储,删除交给了线程池处理cleanupRunnable的任务。
  • 在每次创建RealConnection或从连接池中拿一次RealConnection会给RealConnection的 transmitters集合添加一个若引用的transmitter对象,添加它主要是为了后面判断该连接是否在使用中
  • 在连接池中找连接的时候会对比连接池中相同host的连接。
  • 如果在连接池中找不到连接的话,会创建连接,创建完后会存储到连接池中。
  • 在把连接放入连接池中时,会把清除操作的任务放入到线程池中执行,删除任务中会判断当前连接有没有在使用中,有没有正在使用通过RealConnection的transmitters集合的size是否为0来判断,如果不在使用中,找出空闲时间最长的连接,如果空闲时间最长的连接超过了keep-alive默认的5分钟或者空闲的连接数超过了最大的keep-alive连接数5个的话,会把存活时间最长的连接从连接池中删除。保证keep-alive的最大空闲时间和最大的连接数。


 

问4:OkHttp里面用到了什么设计模式

责任链模式

可以说是okhttp的精髓所在了,主要体现就是拦截器的使用,具体代码可以看看上述的拦截器介绍。

建造者模式

在Okhttp中,建造者模式也是用的挺多的,主要用处是将对象的创建与表示相分离,用Builder组装各项配置。

工厂模式

工厂模式和建造者模式类似,区别就在于工厂模式侧重点在于对象的生成过程,而建造者模式主要是侧重对象的各个参数配置。

例子有CacheInterceptor拦截器中又个CacheStrategy对象:

    CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();public Factory(long nowMillis, Request request, Response cacheResponse) {this.nowMillis = nowMillis;this.request = request;this.cacheResponse = cacheResponse;if (cacheResponse != null) {this.sentRequestMillis = cacheResponse.sentRequestAtMillis();this.receivedResponseMillis = cacheResponse.receivedResponseAtMillis();Headers headers = cacheResponse.headers();for (int i = 0, size = headers.size(); i < size; i++) {String fieldName = headers.name(i);String value = headers.value(i);if ("Date".equalsIgnoreCase(fieldName)) {servedDate = HttpDate.parse(value);servedDateString = value;} else if ("Expires".equalsIgnoreCase(fieldName)) {expires = HttpDate.parse(value);} else if ("Last-Modified".equalsIgnoreCase(fieldName)) {lastModified = HttpDate.parse(value);lastModifiedString = value;} else if ("ETag".equalsIgnoreCase(fieldName)) {etag = value;} else if ("Age".equalsIgnoreCase(fieldName)) {ageSeconds = HttpHeaders.parseSeconds(value, -1);}}}}

观察者模式 

关于Okhttp中websocket的使用,由于webSocket属于长连接,所以需要进行监听,这里是用到了观察者模式:

  final WebSocketListener listener;@Override public void onReadMessage(String text) throws IOException {listener.onMessage(this, text);}

单例模式

每个项目都会有。

问5:论如何优雅地知道OkHttp的请求时间

OkHttp如何进行各个请求环节的耗时统计呢?

OkHttp 版本提供了EventListener接口,可以让调用者接收一系列网络请求过程中的事件,例如DNS解析、TSL/SSL连接、Response接收等。通过继承此接口,调用者可以监视整个应用中网络请求次数、流量大小、耗时(比如dns解析时间,请求时间,响应时间等等)情况。

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的RealConnectionpublic 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) {}
}

请求开始结束监听

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

如何消耗记录时间

在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@Overridepublic EventListener create(@NotNull Call call) {return new NetworkListener();}};return factory;}@Overridepublic 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());}@Overridepublic void dnsStart(@NotNull Call call, @NotNull String domainName) {super.dnsStart(call, domainName);ToolLogUtils.d(TAG, "dnsStart");saveEvent(NetworkTraceBean.DNS_START);}@Overridepublic 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);}@Overridepublic void connectStart(@NotNull Call call, @NotNull InetSocketAddress inetSocketAddress, @NotNull Proxy proxy) {super.connectStart(call, inetSocketAddress, proxy);ToolLogUtils.d(TAG, "connectStart");saveEvent(NetworkTraceBean.CONNECT_START);}@Overridepublic void secureConnectStart(@NotNull Call call) {super.secureConnectStart(call);ToolLogUtils.d(TAG, "secureConnectStart");saveEvent(NetworkTraceBean.SECURE_CONNECT_START);}@Overridepublic void secureConnectEnd(@NotNull Call call, @Nullable Handshake handshake) {super.secureConnectEnd(call, handshake);ToolLogUtils.d(TAG, "secureConnectEnd");saveEvent(NetworkTraceBean.SECURE_CONNECT_END);}@Overridepublic 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);}@Overridepublic 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");}@Overridepublic void requestHeadersStart(@NotNull Call call) {super.requestHeadersStart(call);ToolLogUtils.d(TAG, "requestHeadersStart");saveEvent(NetworkTraceBean.REQUEST_HEADERS_START);}@Overridepublic void requestHeadersEnd(@NotNull Call call, @NotNull Request request) {super.requestHeadersEnd(call, request);ToolLogUtils.d(TAG, "requestHeadersEnd");saveEvent(NetworkTraceBean.REQUEST_HEADERS_END);}@Overridepublic void requestBodyStart(@NotNull Call call) {super.requestBodyStart(call);ToolLogUtils.d(TAG, "requestBodyStart");saveEvent(NetworkTraceBean.REQUEST_BODY_START);}@Overridepublic void requestBodyEnd(@NotNull Call call, long byteCount) {super.requestBodyEnd(call, byteCount);ToolLogUtils.d(TAG, "requestBodyEnd");saveEvent(NetworkTraceBean.REQUEST_BODY_END);}@Overridepublic void responseHeadersStart(@NotNull Call call) {super.responseHeadersStart(call);ToolLogUtils.d(TAG, "responseHeadersStart");saveEvent(NetworkTraceBean.RESPONSE_HEADERS_START);}@Overridepublic void responseHeadersEnd(@NotNull Call call, @NotNull Response response) {super.responseHeadersEnd(call, response);ToolLogUtils.d(TAG, "responseHeadersEnd");saveEvent(NetworkTraceBean.RESPONSE_HEADERS_END);}@Overridepublic void responseBodyStart(@NotNull Call call) {super.responseBodyStart(call);ToolLogUtils.d(TAG, "responseBodyStart");saveEvent(NetworkTraceBean.RESPONSE_BODY_START);}@Overridepublic void responseBodyEnd(@NotNull Call call, long byteCount) {super.responseBodyEnd(call, byteCount);ToolLogUtils.d(TAG, "responseBodyEnd");saveEvent(NetworkTraceBean.RESPONSE_BODY_END);}@Overridepublic void callEnd(@NotNull Call call) {super.callEnd(call);ToolLogUtils.d(TAG, "callEnd");saveEvent(NetworkTraceBean.CALL_END);generateTraceData();NetWorkUtils.timeoutChecker(mRequestId);}@Overridepublic 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);}}

参考

网络库OKHttp(1)流程+拦截器-CSDN博客

谈谈OKHttp的几道面试题 - 简书

面试官:Okhttp连接池是咋实现的? - 掘金

论如何优雅地知道OkHttp的请求时间

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/110391.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Python Connect SQLServer 2008

Macos&#xff08;经过了两天&#xff0c;无数次的方法验证&#xff0c;寻找各种资料&#xff0c;总结如下&#xff09; brew install freetds0.91 如果出现错误就进行手工安装&#xff0c;也可以直接使用 brew install freetds安装最新版本&#xff08;测试通过&#xff09; …

kubernetes 多集群管理和联邦集群将是下一波运维浪潮

问题 调研一下国内外K8s平台软件&#xff0c;哪个具有创建标准的K8s集群的功能&#xff1f; 背景 随着云原生技术在越来越多的企业和组织中的大规模落地&#xff0c;如何高效、可靠地管理大规模资源池以应对不断增长的业务挑战成为了当下云原生技术的关键挑战。在过去的很长…

【后端】韩顺平Java学习笔记(基础篇01)

因为之前有c基础&#xff0c;所以差不多一样的就简写了owo 来源&#xff1a;韩顺平 零基础30天学会Java 目录 I. 控制结构&#xff08;简&#xff09; 一、介绍 1. 顺序 → 从上到下执行&#xff0c;无跳转 2. 分支 → 单、双、多、嵌套 1&#xff09;单&#xff0c;即…

利用爬虫采集音频信息完整代码示例

以下是一个使用WWW::RobotRules和duoip.cn/get_proxy的Perl下载器程序&#xff1a; #!/usr/bin/perluse strict; use warnings; use WWW::RobotRules; use LWP::UserAgent; use HTTP::Request; use HTTP::Response;# 创建一个UserAgent对象 my $ua LWP::UserAgent->new();#…

【网络】计算机网络基础概念入门

&#x1f341; 博主 "开着拖拉机回家"带您 Go to New World.✨&#x1f341; &#x1f984; 个人主页——&#x1f390;个人主页 &#x1f390;✨&#x1f341; &#x1fa81;&#x1f341;&#x1fa81;&#x1f341;&#x1fa81;&#x1f341;&#x1fa81;&#…

59 分割等和子集

分割等和子集 NP 完全问题&#xff08;01背包&#xff09;题解1 二维DP题解2 空间优化DP&#xff08;改为1D&#xff09; 给你一个只包含正整数的非空数组 nums 。请你判断是否可以将这个数组分割成两个子集&#xff0c;使得两个子集的元素和相等。 示例 1&#xff1a; 输入&a…

Cdiscount、亚马逊新品不开单怎么办 ?测评自养号关键之处及搭建技巧揭秘

一、Listing如何优化? 一个产品要想有销量&#xff0c;Listing优化必须得做好&#xff0c;这是形成转化产生订单的基础。 有些卖家误以为“反复修改了N次”就叫做“优化”&#xff0c;这绝对是误解 想要做好Listing优化&#xff0c;需要知道优化的基本标准&#xff0c;同时…

私人服务器可以干嘛

目录 搭建个人网站或博客&#xff1a; 远程桌面&#xff1a; 作为网盘储存&#xff1a; 作为测试和学习环境&#xff1a; 推广产品&#xff1a; 游戏私服(注意,仅限于个人自己单机玩)&#xff1a; 个人服务器可以用于多种用途&#xff0c;以下是一些常见的用途&#xff1a;…

【k8s】1、基础概念和架构及组件

一、kubernetes概述 K8S是一种开源的容器编排平台&#xff0c;用于自动化部署、扩展和管理容器化的应用程序&#xff0c;它提供了一种容器编排和管理的方式&#xff0c;可以帮助开发人员更轻松的管理容器化的应用程序&#xff0c;并且提供了一种跨多个主机的自动化部署和管理机…

CORE: Cooperative Reconstruction for Multi-Agent Perception 论文阅读

论文连接 CORE: Cooperative Reconstruction for Multi-Agent Perception 0. 摘要 本文提出了 CORE&#xff0c;一种概念简单、有效且通信高效的多智能体协作感知模型。 从合作重建的新颖角度解决了该任务&#xff1a; 合作主体共同提供对环境的更全面的观察整体观察可以作为…

【STM32】--PZ6860L,STM32F4,ARM3.0开发板

一、ARM3.0开发板详细介绍 1.开发板整体介绍 &#xff08;1&#xff09;各种外设和主板原理图 &#xff08;2&#xff09;主板供电部分5V和3.3V兼容设计 注意跳线帽 2.STM32核心板介绍 3.核心板原理图 STM32和51的IO对应关系 下载电路 二、ARM3.0开发板ISP下载原理分析 1.I…

Go语言入门心法(六): HTTP面向客户端|服务端编程

Go语言入门心法(一): 基础语法 Go语言入门心法(二): 结构体 Go语言入门心法(三): 接口 Go语言入门心法(四): 异常体系 Go语言入门心法(五): 函数 一:go语言面向web编程认知 Go语言的最大优势在于并发与性能,其性能可以媲美C和C,并发在网络编程中更是至关重要 使用http发送请…

android开源投屏工具scrcpy简介

目录 一&#xff0c;初识scrcpy 1.1 scrcpy介绍 1.2 scrcpy特点 二&#xff0c;scrcpy指令说明 2.1 画面设置 2.1.1 缩小分辨率 2.1.2 修改画面比特率 2.1.3 限制画面帧率 2.1.4 画面裁剪 2.1.5 锁定屏幕朝向 2.2 屏幕录制 2.3 连接方式 2.3.1 无线 2.3.2 多设备…

Ai数字人直播系统SaaS源码大开源,源码独立部署助力中小企业发展!

源码独立部署ai数字人直播系统&#xff0c;如果放在上半年的话没有数百万投资几乎是天方夜谭&#xff0c;连想做个数字人代理商少则投资十万多则数十万才能进得了代理门槛。在此期间&#xff0c;数字人市场一度出现了大批不良企业利用网上下载的视频合成源码二次包装后打着数字…

三级分类部分三级目录无法加载,后端接口能在前端返回所有数据

项目场景&#xff1a; 实现ElementUI中三级分类的功能&#xff0c;发现没有前端三级目录的二级目录可以新建三级目录&#xff0c;数据库中也有数据&#xff0c;但是无法在前端显示&#xff01;后端的接口没有返回数据库的数据。 问题描述 提示&#xff1a;这里描述项目中遇到…

selenium多窗口、多iframe切换、alert、3种等待

1、多标签/多窗口之间的切换 场景&#xff1a; 在页面操作过程中有时候点击某个链接会弹出新的窗口&#xff0c;这时就需要切换到新打开的窗口上进行操作。这种情况下&#xff0c;需要识别多标签或窗口的情况。 操作方法&#xff1a; switch_to.window()方法&#xff1a;切换…

【Eclipse】解决插件下载速度太慢

解决方案&#xff1a;修改镜像 下面列出几个国内的镜像网站&#xff1a; 中国科学技术大学(5.6MB/s) http://mirrors.ustc.edu.cn/eclipse/ 北京理工大学&#xff08;600KB/s&#xff09; http://mirror.bit.edu.cn/eclipse/ 大连东软信息学院(400KB/s) http://mirrors.neuso…

基于YOLOv8的多目标检测与自动标注软件【python源码+PyqtUI界面+exe文件】【深度学习】

基本功能演示 摘要&#xff1a;YOLOv8是YOLO系列最新的版本&#xff0c;支持多种视觉任务。本文基于YOLOv8的基础模型实现了80种类别的目标检测&#xff0c;可以对图片进行批量自动标注&#xff0c;并将检测结果保存为YOLO格式便于后续进行其他任务训练。本文给出完整的Python实…

八股文学习四(kafka)

一. 消息中间件kafka (1)基本概念 (2) 生产者 生产者将消息发送到topic中去&#xff0c;同时负责选择将message发送到topic的哪一个partition中。通过round-robin做简单的负载均衡。也可以根据消息中的某一个关键字来进行区分。通常第二种方式使用的更多。 (3)消费者 消费模…

【初识Linux】:常见指令(2)

朋友们、伙计们&#xff0c;我们又见面了&#xff0c;本期来给大家解读一下有关Linux的基础知识点&#xff0c;如果看完之后对你有一定的启发&#xff0c;那么请留下你的三连&#xff0c;祝大家心想事成&#xff01; C 语 言 专 栏&#xff1a;C语言&#xff1a;从入门到精通 数…