OkHttpClient框架 使用了大量的设计模式 builder ,好处是可以减少框架的使用者传入很多参数,使用 builder 内部的默认值即可完成网络请求,对默认值进行简单的一一分析:
public Builder() { dispatcher = new Dispatcher(); protocols = OkHttpClient.DEFAULT_PROTOCOLS; connectionSpecs = DEFAULT_CONNECTION_SPECS; eventListenerFactory = EventListener.factory(EventListener.NONE); proxySelector = ProxySelector.getDefault(); cookieJar = CookieJar.NO_COOKIES; socketFactory = SocketFactory.getDefault(); hostnameVerifier = OkHostnameVerifier.INSTANCE; certificatePinner = CertificatePinner.DEFAULT; proxyAuthenticator = Authenticator.NONE; authenticator = Authenticator.NONE; connectionPool = new ConnectionPool(); dns = Dns.SYSTEM; followSslRedirects = true; followRedirects = true; retryOnConnectionFailure = true; connectTimeout = 10_000; readTimeout = 10_000; writeTimeout = 10_000; pingInterval = 0; }复制代码
new Dispatcher()
每次创建 okHttpClient 都会创建一个新的 Dispatcher,这里有线程池,如果需要创建多个 okHttpClient 最好传入这个参数,并复用线程池 这个类的主要作用是处理okHttpClient.newCall发送请求的。有异步的请求 enqueue 和同步的请求 executed 还有处理请求结束的 finished(AsyncCall/RealCall)
synchronized void enqueue(AsyncCall call) { if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) { //总最大请求数和同个 ip 最大请求数的限制 runningAsyncCalls.add(call); executorService().execute(call);//执行请求 } else { readyAsyncCalls.add(call);//放到队列中等待调用 finish } } /** 处理异步请求,这里设计到了1个方法4个全局变量,一个个说: @methord executorService() 创建了一个线程池无最大上限,闲置60秒留存0个核心线程 @filed maxRequests 最大同时发起请求的数量默认64个,实际可以通过ExecutorService的maximumPoolSize和BlockingQueue控制,不知道框架为什么弄一个这个变量 @filed maxRequestsPerHost每个ip最大的请求数,默认5个,设计这个字段可能是为了避免某个服务器同时处理多个请求导致单个请求总时间变长。 @filed runningAsyncCalls如果请求数量未超出maxRequests和maxRequestsPerHost的限制,则加到这个队列里,保存了发送的请求,按照默认的executorService,没有在队列里等待的请求 @filed readyAsyncCalls超出了限制,会加到这个队列里等待。 **/复制代码
synchronized void executed(RealCall call) { runningSyncCalls.add(call); } /**同步请求newCall.execute会调用到这里,android中这个方法会在子线程调用 @filed runningSyncCalls 这个队列里保存了同步的请求,newCall.execute是有返回值的 **/复制代码
public synchronized void setIdleCallback(@Nullable Runnable idleCallback) //当所有请求执行完,会调用 idleCallback.run复制代码
public synchronized void cancelAll() //取消所有的请求,包含异步正在执行的runningAsyncCalls,异步队列中的readyAsyncCalls,同步执行的队列runningSyncCalls。复制代码
public synchronized ListrunningCalls()//获取正在执行的队列 runningAsyncCalls复制代码
public synchronized ListqueuedCalls()//获取等待执行的队列readyAsyncCalls复制代码
privatevoid finished(Deque calls, T call, boolean promoteCalls)//当一个call 执行完调用promoteCalls,所有的 call 都执行完调用idleCallback.run private void promoteCalls()//内部调用runningCallsForHost判断是否把readyAsyncCalls中的请求放到runningAsyncCalls private int runningCallsForHost(AsyncCall call)//获取一个ip 下的请求数,在enqueue和promoteCalls中调用判断是否超出maxRequestsPerHost的限制复制代码
OkHttpClient.DEFAULT_PROTOCOLS
Protocol是一个 enum (HTTP_1_0,HTTP_1_1,HTTP_2,SPDY_3)每次创建一个 OkHttpClient 默认都使用了同一个协议组,默认支持 http/1.1和h2。
static final ListDEFAULT_PROTOCOLS; DEFAULT_PROTOCOLS = Util.immutableList(new Protocol[]{Protocol.HTTP_2, Protocol.HTTP_1_1});复制代码
内部实现只有一个构造方法和一个 get 方法
/**由此可以看出,每个枚举都会有个 string值**/ Protocol(String protocol) { this.protocol = protocol; } /** 通过一个 string 串解析是哪种协议类型 **/ public static Protocol get(String protocol) throws IOException { // Unroll the loop over values() to save an allocation. if (protocol.equals(HTTP_1_0.protocol)) return HTTP_1_0; if (protocol.equals(HTTP_1_1.protocol)) return HTTP_1_1; if (protocol.equals(HTTP_2.protocol)) return HTTP_2; if (protocol.equals(SPDY_3.protocol)) return SPDY_3; throw new IOException("Unexpected protocol: " + protocol); }复制代码
OkHttpClient.DEFAULT_CONNECTION_SPECS
每次创建一个 OkHttpClient 默认都使用了同一个协议组,https 的安全认证,在 RetryAndFollowUpInterceptor 生成请求的必要参数 createAddress() 中调用,在源码分析2的发送请求流程中具体分析 RetryAndFollowUpInterceptor。 ConnectionSpec 中有三个可以直接使用的常亮 MODERN_TLS 中含有了 tls 1.0, 1.1, 1.2, 1.3和 ssl3; COMPATIBLE_TLS 比 MODERN_TLS多了; CLEARTEXT http 使用,数据不经过加密
static final ListDEFAULT_CONNECTION_SPECS;DEFAULT_CONNECTION_SPECS = Util.immutableList(new ConnectionSpec[]{ConnectionSpec.MODERN_TLS, ConnectionSpec.CLEARTEXT});复制代码
/**ConnectionSpec.Builder中 保存了变量@params tls 是否支持数据加密,@params cipherSuites 加密算法类,使用到了类 CipherSuite,定义了一些算法名称字符串常亮。@params tlsVersions 支持的TLS协议版本@params supportsTlsExtensions 是否支持扩展协议**/ public static final class Builder { boolean tls; @Nullable String[] cipherSuites; @Nullable String[] tlsVersions; boolean supportsTlsExtensions; ...... }复制代码
public boolean isCompatible(SSLSocket socket) { if (!tls) { return false; } if (tlsVersions != null && !nonEmptyIntersection(Util.NATURAL_ORDER, tlsVersions, socket.getEnabledProtocols())) { return false; } if (cipherSuites != null && !nonEmptyIntersection(CipherSuite.ORDER_BY_NAME, cipherSuites, socket.getEnabledCipherSuites())) { return false; } return true; }/**判断 tlsVersions,cipherSuites是否支持服务器的加密版本和算法类型,如果支持 true这个方法在类 ConnectionSpecSelector 中调用,由于每个 OKHttpClient 都有几个 ConnectionSpec,发送请求的时候用 ConnectionSpecSelector 选择一个支持的 ConnectionSpec,在 RealConnection.connectTls() 中调用方法apply复制代码
/**如果在isCompatible返回了 true,会进到这里,通过supportedSpec()和服务器支持的版本和算法取交集,在设置给 sslSocket *@params sslSocket 发送请求的 socket。在SSLSocketFactory中设置 https://developer.android.com/reference/javax/net/ssl/SSLSocket.html *@params isFallback 是否因为 SSLHandshakeException 或 SSLProtocolException 失败过,RealConnection.connect()中 connectionSpecSelector.connectionFailed(e) 判断异常,如果是上述两个异常会重新连接并isFallback=true **/ void apply(SSLSocket sslSocket, boolean isFallback) { ConnectionSpec specToApply = supportedSpec(sslSocket, isFallback); if (specToApply.tlsVersions != null) { sslSocket.setEnabledProtocols(specToApply.tlsVersions); } if (specToApply.cipherSuites != null) { sslSocket.setEnabledCipherSuites(specToApply.cipherSuites); } }复制代码
/**使用intersect方法获取sslSocket和这个 ConnectionSpec 共同支持的tlsVersions,cipherSuites当 isFallback=true 时,使用 "TLS_FALLBACK_SCSV"解决重试连接失败的问题(SSLv3有漏洞)@return 并 new 一个新的 ConnectionSpec,保证原有数据不被破坏,但是浪费内存 **/ private ConnectionSpec supportedSpec(SSLSocket sslSocket, boolean isFallback) 复制代码
EventListener.factory(EventListener.NONE)
创建 EventListener 的工厂,在 EventListener 中监听各种回调,比如说connect、request,response。每一个 call 可以创建一个对应的 EventListener。
public interface Factory { //创建 listener 的工厂方法 EventListener create(Call call); } /**在类 EventListener 的静态方法factory中 new 了一个内部类 *默认实现的参数 EventListener.NONE 是一个 new EventListener{ } 的空实现 **/ static EventListener.Factory factory(final EventListener listener) { return new EventListener.Factory() { public EventListener create(Call call) { return listener; } }; }复制代码
/**调用 new OkHttpClient().newCall(Request) 中会进入这个方法 *@params eventListener 在每个 RealCall 中的回调。 **/static RealCall newRealCall(OkHttpClient client, Request originalRequest, boolean forWebSocket) { RealCall call = new RealCall(client, originalRequest, forWebSocket); call.eventListener = client.eventListenerFactory().create(call); return call; }复制代码
ProxySelector.getDefault()
java 中对代理的默认处理,使用了手机厂商的默认实现,整个应用只有一个实例,如果不对手机的代理做特殊处理,这里不要做任何改动。在 RouteSelector 中调用。
private static ProxySelector defaultSelector = new ProxySelectorImpl();//JDK 中的默认实现 public static ProxySelector getDefault() { return defaultSelector; }//如果想修改代理的处理在这里设置一个ProxySelector会影响应用中所有的代理,比如想禁止应用使用代理在 select 方法返回 Proxy.NO_PROXY public static void setDefault(ProxySelector selector) { defaultSelector = selector; }复制代码
CookieJar.NO_COOKIES
对 cookie 的处理,如果应用有设置 cookie 的要求需要调用 new OkHttpClient.Builder().cookieJar(CookieJar cookieJar) 设置保存和读取 cookie 的实现。框架默认是不保存 cookie 的 NO_COOKIES只对接口做了空实现。
void saveFromResponse(HttpUrl url, Listcookies); List loadForRequest(HttpUrl url);复制代码
/** *HttpHeaders的静态方法中调用了 saveFromResponse . *@params HttpUrl保存了请求的地址 *@params Headers里面的数组[key1,value1,key2,value2, ...]保存了请求头的所有键值对. *@methord Cookie.parseAll中调用Cookie.parse解析所有的 Set-Cookie 字段。然后调用CookieJar的saveFromResponse public static void receiveHeaders(CookieJar cookieJar, HttpUrl url, Headers headers) { if (cookieJar == CookieJar.NO_COOKIES) return;//如果不设置 cookie 就不进行解析了,响应速度快 Listcookies = Cookie.parseAll(url, headers); if (cookies.isEmpty()) return; cookieJar.saveFromResponse(url, cookies); }复制代码
//BridgeInterceptor 的 intercept 方法中调用 loadForRequest ,并设置到请求头上。BridgeInterceptor在源码分析1中有详细的讲解public Response intercept(Chain chain) throws IOException { ...... Listcookies = cookieJar.loadForRequest(userRequest.url()); if (!cookies.isEmpty()) { requestBuilder.header("Cookie", cookieHeader(cookies)); } ......}复制代码
SocketFactory.getDefault()
java 中对SocketFactory的默认处理,使用了手机厂商的默认实现,整个应用只有一个实例,如果不对手机的Socket做特殊处理,这里不要做任何改动。
private static SocketFactory defaultFactory; /**和 proxySelector 不同的是只能 get,不能 set *RetryAndFollowUpInterceptor中把 Socket 设置给了Address *在 RealConnection 使用了address.socketFactory().createSocket() **/ public static synchronized SocketFactory getDefault() { if (defaultFactory == null) { defaultFactory = new DefaultSocketFactory(); } return defaultFactory; }复制代码
OkHostnameVerifier.INSTANCE
如果是 https 的请求要验证证书,okhttp 里实现了一套使用 x509 证书格式 的认证。首先 verifyAsIpAddress 判断是 ipAddress 还是 hostName, 如果是 ip 证书中的 type 是7,如果是 host,证书中的 type 是2,然后分别判断证书中是否包含对应的 ip 或者 host。
/**如果是 ip 通过方法 getSubjectAltNames 读取证书中包含的所有ip。在判断请求的 ip 是其中一个。 *@params X509Certificate 使用getSubjectAlternativeNames方法获得一个数组,数组中每个条目包含了 type=7 和 ip 或者 type=2和 host。 private boolean verifyIpAddress(String ipAddress, X509Certificate certificate) { ListaltNames = getSubjectAltNames(certificate, ALT_IPA_NAME); for (int i = 0, size = altNames.size(); i < size; i++) { if (ipAddress.equalsIgnoreCase(altNames.get(i))) { return true; } } return false; }复制代码
/** *和判断 ip 的类似,这个方法是判断 hostname 的 *@methord verifyHostname 由于证书中存的可能是带*号的二级域名,但是 hostname 是一个三级或者四级域名,匹配规则又不可以用正则。就有了这个方法 *下面有一段特殊的判断证书含有 cn 的情况,由于对ssl 不是特别了解,看不懂。 **/ private boolean verifyHostname(String hostname, X509Certificate certificate) { hostname = hostname.toLowerCase(Locale.US); boolean hasDns = false; ListaltNames = getSubjectAltNames(certificate, ALT_DNS_NAME); for (int i = 0, size = altNames.size(); i < size; i++) { hasDns = true; if (verifyHostname(hostname, altNames.get(i))) { return true; } } if (!hasDns) { X500Principal principal = certificate.getSubjectX500Principal(); // RFC 2818 advises using the most specific name for matching. String cn = new DistinguishedNameParser(principal).findMostSpecific("cn"); if (cn != null) { return verifyHostname(hostname, cn); } } return false; }复制代码
CertificatePinner.DEFAULT
安全性处理,如果客户端发送给服务器的数据有很高的保密要求,不希望被任何伪装服务器接受或者代理层拦截,可以使用 new OkHttpClient.Builder().certificatePinner(CertificatePinner certificatePinner) 客户端里的对正式的合法性校验,这个方法在RealConnection的connectTls中调用,当完成 ssl 的握手,拿到服务器的正式。对证书 check,如果证书 check 失败抛出异常。
//默认不添加任何限制public static final CertificatePinner DEFAULT = new Builder().build();复制代码
/**所有的验证限制在全局变量pins中,这里是一个数组,只要有一个命中则验证通过。 *pin中保存了加密的数据和hostname *@methord findMatchingPins 找到含有 hostname 的 pins。 *@methord sha256、sha1 至支持这两种算法,对 x509中 publickey 加密,防止 publickey 直接被代码编写者看到 **/ public void check(String hostname, ListpeerCertificates) throws SSLPeerUnverifiedException { List pins = findMatchingPins(hostname); if (pins.isEmpty()) return;//如果是空的则正常进行请求 if (certificateChainCleaner != null) { //选定某几个x509证书,因为有可能返回的正式里包含了多个域名的认证 peerCertificates = certificateChainCleaner.clean(peerCertificates, hostname); } for (int c = 0, certsSize = peerCertificates.size(); c < certsSize; c++) { X509Certificate x509Certificate = (X509Certificate) peerCertificates.get(c); // Lazily compute the hashes for each certificate. ByteString sha1 = null; ByteString sha256 = null; /**主要的验证逻辑,第一层 for 是证书,第二层 for 是 pin, *如果有任何一个证书和 pin 拼配成功,则可以进行后面的请求, *否则抛出异常AssertionError **/ for (int p = 0, pinsSize = pins.size(); p < pinsSize; p++) { Pin pin = pins.get(p); if (pin.hashAlgorithm.equals("sha256/")) { if (sha256 == null) sha256 = sha256(x509Certificate); if (pin.hash.equals(sha256)) return; // Success! } else if (pin.hashAlgorithm.equals("sha1/")) { if (sha1 == null) sha1 = sha1(x509Certificate); if (pin.hash.equals(sha1)) return; // Success! } else { throw new AssertionError("unsupported hashAlgorithm: " + pin.hashAlgorithm); } } } //这里官方的注释很到位,只是在抛出异常的时候,打个很详细的 log // If we couldn't find a matching pin, format a nice exception. StringBuilder message = new StringBuilder() .append("Certificate pinning failure!") .append("\n Peer certificate chain:"); for (int c = 0, certsSize = peerCertificates.size(); c < certsSize; c++) { X509Certificate x509Certificate = (X509Certificate) peerCertificates.get(c); message.append("\n ").append(pin(x509Certificate)) .append(": ").append(x509Certificate.getSubjectDN().getName()); } message.append("\n Pinned certificates for ").append(hostname).append(":"); for (int p = 0, pinsSize = pins.size(); p < pinsSize; p++) { Pin pin = pins.get(p); message.append("\n ").append(pin); } throw new SSLPeerUnverifiedException(message.toString()); }复制代码
Authenticator.NONE
proxyAuthenticator,authenticator 都是默认值 NONE,接口中只有一个方法。 authenticator 当服务器返回的响应码是401的时候,需要对服务器进行登录授权。如果需要继续执行登录的操作,就复写authenticate,返回一个调用登录的 request。 proxyAuthenticator当服务器返回的响应码是407的时候,需要对代理服务器进行登录授权。如果需要继续执行登录代理服务器的操作,就复写authenticate,返回一个登录代理的 request
public interface Authenticator { //返回一个去授权的请求,替换原有请求 Request authenticate(Route route, Response response) throws IOException;} Authenticator NONE = new Authenticator() { @Override public Request authenticate(Route route, Response response) { return null; } };复制代码
new ConnectionPool()
所有方法都在 Internal 中调用,不管有多少个 OkHttpClient,Internal只有一个实例 public static Internal instance;在 OkHttpClient 有一个静态方法块 Internal.instance = new Internal() {...各个方法的实现...},不知道为什么这样设计,Internal和工具类没差别。
/**在StreamAllocation.findConnection 中创建了 RealConnection 连接成功后调用put *@field cleanupRunning 如果之前没有请求在执行 cleanupRunning 是 false,调用 put 后赋值 true,并且开始在一个单独的线程执行清理的操作 **/ void put(RealConnection connection) { assert (Thread.holdsLock(this)); if (!cleanupRunning) { cleanupRunning = true; executor.execute(cleanupRunnable); } connections.add(connection); }复制代码
/**这里使用了cleanupRunning、cleanupRunnable共同控制了清理线程的执行 *当 put 的时候调用execute并且putcleanupRunning=true,所有的任务执行完cleanup返回了-1,停止清理线程,并cleanupRunning=false,等下次 put并执行execute,如果put的时候有任务在执行不调用execute **/ private final Runnable cleanupRunnable = new Runnable() { @Override public void run() { while (true) { /** *cleanup返回-1终止线程,大于0的值就等待waitNanos的时间再次执行 **/ long waitNanos = cleanup(System.nanoTime()); if (waitNanos == -1) return; if (waitNanos > 0) { long waitMillis = waitNanos / 1000000L; waitNanos -= (waitMillis * 1000000L); synchronized (ConnectionPool.this) { try { ConnectionPool.this.wait(waitMillis, (int) waitNanos); } catch (InterruptedException ignored) { } } } } } }; long cleanup(long now) { int inUseConnectionCount = 0; int idleConnectionCount = 0; RealConnection longestIdleConnection = null; long longestIdleDurationNs = Long.MIN_VALUE; // Find either a connection to evict, or the time that the next eviction is due. synchronized (this) { for (Iteratori = connections.iterator(); i.hasNext(); ) { RealConnection connection = i.next(); // pruneAndGetAllocationCount方法中清理已经被回收的 StreamAllocation 并返回当前存活的数量 if (pruneAndGetAllocationCount(connection, now) > 0) { inUseConnectionCount++; continue; } idleConnectionCount++; // 计算空置时间最长的一个 RealConnection 的时长. long idleDurationNs = now - connection.idleAtNanos; if (idleDurationNs > longestIdleDurationNs) { longestIdleDurationNs = idleDurationNs; longestIdleConnection = connection; } } //上面的代码给4个变量赋值,下面再拿这几个变量计算出改怎样执行清理 if (longestIdleDurationNs >= this.keepAliveDurationNs || idleConnectionCount > this.maxIdleConnections) { // 如果超出最大缓存数或者超出最大保活时长,清理掉这个RealConnection connections.remove(longestIdleConnection); } else if (idleConnectionCount > 0) { // 再过 return 的时间就要清理掉这个 connection 了 return keepAliveDurationNs - longestIdleDurationNs; } else if (inUseConnectionCount > 0) { // 如果在执行的马上能执行完,再过 return 的时间就要清理掉这个 connection 了 return keepAliveDurationNs; } else { // 没有 connect 了,不在执行清理 return -1; } } closeQuietly(longestIdleConnection.socket());//走到了第一个 if 中,清理了一个。还需要再次清理一遍 return 0; }复制代码
Dns.SYSTEM
hostname 转 ip 的接口,默认实现Arrays.asList(InetAddress.getAllByName(hostname)),没有什么好说的。
Listlookup(String hostname) throws UnknownHostException;复制代码
followSslRedirects = true; followRedirects = true;
这两个变量控制如果有301,302,303重定向,或者 http 转 https 的接口是否要继续请求重定向后的Location
retryOnConnectionFailure = true;
当使用连接池并没有和服务器连通,是否要进行重试。
interceptors,networkInterceptors
每个 okHttpClient 中都有两个list 分别保存了这两个拦截器,在 RealCall 的Response getResponseWithInterceptorChain()
方法中调用 interceptors 在请求发起先加到队列里,再添加错误处理、拼接http协议、缓存处理、建立连接的框架中已有的拦截器这时候已经准备好了,又添加了client.networkInterceptors() 最终写入数据到socket中并获取服务器返回的流,形成了一条数据调用链。
Response getResponseWithInterceptorChain() throws IOException { Listinterceptors = new ArrayList<>(); interceptors.addAll(client.interceptors());//OkHttpClient.Builder.addInterceptor(Interceptor interceptor) interceptors.add(retryAndFollowUpInterceptor);//错误处理,和其他的拦截器相比 这个拦截器先初始化了,这样设计我觉得是处理 cancel 用的 interceptors.add(new BridgeInterceptor(client.cookieJar()));//拼接http协议 拼接header和body interceptors.add(new CacheInterceptor(client.internalCache()));//缓存处理 interceptors.add(new ConnectInterceptor(client));//使用连接池建立连接 if (!forWebSocket) { interceptors.addAll(client.networkInterceptors());//OkHttpClient.Builder.addNetworkInterceptor(Interceptor interceptor) } interceptors.add(new CallServerInterceptor(forWebSocket));//想服务器发起请求 ...... }复制代码
Cache cache; InternalCache internalCache;
这两个都是处理缓存的逻辑的,一个是接口,一个是实现类的包装。设置任何一个都可以使用缓存,框架默认是不使用缓存的。在 new CacheInterceptor(client.internalCache()
中使用了InternalCache,优先使用cache,如果没有设置cache才去调用找 InternalCache 个接口。
InternalCache internalCache() { return cache != null ? cache.internalCache : internalCache; }复制代码
Cache中 InternalCache internalCache = new InternalCache(){}
这个变量保存了一个实现类,接口的每个方法都直接调用了cache的方法,接口中主要方法有put、get、remove、update、和两个计数方法。保存文件的逻辑在DiskLruCache中,这两个类通过Source和Sink传递数据,由于okio把流的读写转换为了Source和Sink这两个接口。所以这样设计有很好的扩展性,涉及到的类有:
CacheDiskLruCache
用journalFile文件保存了所有缓存的文件key和key对应文件状态READ、DIRTY、CLEAN、REMOVE,有点像管理缓存文件的数据库,这样做的好处是,管理缓存文件是否超过maxSize,文件读写占用时间 判断状态防止读写混乱。
/**把journalFile文件转换为HashMap,文件作为持久化下次打开初始化数据用。计算数据使用HashMap速度快。 *JournalFile有当前使用文件、一个临时文件和一个备份文件 三个 *本次初始化后的请求写到临时文件和hashMap中**/ public synchronized void initialize() throws IOException { if (initialized) { //防止重复初始化 return; // Already initialized. } if (fileSystem.exists(journalFileBackup)) { // If journal file also exists just delete backup file. if (fileSystem.exists(journalFile)) { fileSystem.delete(journalFileBackup); } else { fileSystem.rename(journalFileBackup, journalFile);//正式文件丢失,把备份文件恢复 } } if (fileSystem.exists(journalFile)) { //正式文件存在 try { readJournal();//判断文件的有效性,把文件中的每一条读出来添加到HashMap中 processJournal();//才判断是不是超出maxSize,如果超出了要删除多余的文件 initialized = true; return; } catch (IOException journalIsCorrupt) { Platform.get().log(WARN, "DiskLruCache " + directory + " is corrupt: " + journalIsCorrupt.getMessage() + ", removing", journalIsCorrupt); } try { delete(); } finally { closed = false; } } rebuildJournal();//把这次写入的文件命名为临时文件,上次保留的正式文件改为备份文件,上次写入的改为正式文件, initialized = true; }复制代码
completeEdit是保存文件和修改文件名的核心方法,里面没有什么复杂的逻辑,DiskLruCache中比较难理解的就是文件状态处理,只要抓住文件 生成临时-写入数据-重命名或删除 这个顺序的线就容易理解了。
/**当本次请求结束的时候,把dirty文件改为clean状态,标识缓存数据可以使用了 *如果请求失败把dirty文件删掉 *并且同时保存到journalFile和hashMap *判断有没有超出最大体积限制并清理**/completeEdit(Editor editor, boolean success);复制代码
DiskLruCache.Entry 一个请求要保存为响应体文件,其他数据经过Cache.Entry转换的文件,两个文件。但是读写文件的时间不是瞬间完成的,为了防止产生阻塞,每个文件都会有clean和dirty两个状态,clean文件可以直接转换为Snapshot.source,所以里面有个方法 snapshot()。
private final class Entry { final String key;//请求的唯一标识key final long[] lengths;//cleanFiles文件的长度 final File[] cleanFiles;//数据写入完了的dirtyFiles会被重命名为cleanFiles final File[] dirtyFiles;//在newSink方法中把这个文件转换为sink,让cache写入数据 boolean readable;//可读状态 journal文件中保存的key是CLEAN ,值为true Editor currentEditor;//当前编辑这个类的Editor }复制代码
DiskLruCache.Snapshot 和Entry一一对应,方便和CacheResponseBody的类传递Source。每个请求都会有一个对应的 Snapshot,保存了要实例化的Source,和每个source的对应的文件长度length(计算缓存体积是否超过maxSize,如果每次都读取文件的大小太耗性能了)。
public final class Snapshot implements Closeable { private final String key;//和entity对应的key private final Source[] sources;//和entity对应的cleanFiles private final long[] lengths;//和entity的length一致 }复制代码
DiskLruCache.Editor 和Entry一一对应,是Entry的操作类,和SharedPreferences.edit是一个作用,把entity转换为流和Cache传递数据,主要方法有两个:
public Source newSource(int index) { ...... return fileSystem.source(entry.cleanFiles[index]);//把cleanFiles文件提供为source读取 ...... } public Sink newSink(int index) { ...... File dirtyFile = entry.dirtyFiles[index];//把dirtyFiles文件提供为sink写入,不是cleanFiles,因为数据存完了会把dirty重命名为clean sink = fileSystem.sink(dirtyFile); ...... }复制代码
Cache.Entry 除了响应体的部分实现类和string的相互转换,写入Sink或者读取Source的持久化为文件,主要方法Entry(Source in)根据流初始化,writeTo(DiskLruCache.Editor editor)把里面的数据写入到文件中。 和android.os.Parcelable是很类似的,这样设计是OkHttpClient为所有的java项目设计的不单独为android。大部分代码是按照顺序写入字符串的,就不粘代码了,没什么逻辑
Cache
remove只是删除文件的操作,track的两个方法都是打点计数用,并没有实质的意义,这里不做分析,只分析put,update,get三个主要方法
CacheRequest put(Response response) { String requestMethod = response.request().method(); if (HttpMethod.invalidatesCache(response.request().method())) { //这里post方法是不支持缓存的注意了 try { remove(response.request()); } catch (IOException ignored) { // The cache cannot be written. } return null; } if (!requestMethod.equals("GET")) { //这里post方法是不支持缓存的注意了 return null; } if (HttpHeaders.hasVaryAll(response)) { //查找Vary变量,如果里面含有 * 不支持缓存 return null; } Entry entry = new Entry(response); DiskLruCache.Editor editor = null; try { editor = cache.edit(key(response.request().url()));//根据response的url生成一个md5获取一个DiskLruCache.Editor,准备写入数据用 if (editor == null) { return null; } entry.writeTo(editor);//把response写入到editor中,这里只写入了header+line return new CacheRequestImpl(editor);//把response写入到editor中,这里只写入了body,并commit } catch (IOException e) { abortQuietly(editor); return null; } }复制代码
/**这个方法是返回响应码是304的时候调用,只更新头信息不用更新body void update(Response cached, Response network) { Entry entry = new Entry(network); DiskLruCache.Snapshot snapshot = ((CacheResponseBody) cached.body()).snapshot; DiskLruCache.Editor editor = null; try { editor = snapshot.edit(); // Returns null if snapshot is not current. if (editor != null) { entry.writeTo(editor);//没用调用new CacheRequestImpl(editor),只把头信息更新了 editor.commit(); } } catch (IOException e) { abortQuietly(editor); } }复制代码
Response get(Request request) { String key = key(request.url());//计算md5 DiskLruCache.Snapshot snapshot; Entry entry; try { snapshot = cache.get(key);//获取保存缓存的流 if (snapshot == null) { return null; } } catch (IOException e) { //当前可能正在写入,不可读,返回空 // Give up because the cache cannot be read. return null; } try { entry = new Entry(snapshot.getSource(ENTRY_METADATA));//获取header+line } catch (IOException e) { //可以正常拿到响应信息 Util.closeQuietly(snapshot); return null; } Response response = entry.response(snapshot);//实例化响应体,形成一个完整的response if (!entry.matches(request, response)) { //判断缓存的请求和法国来的请求是否相同 Util.closeQuietly(response.body()); return null; } return response; }复制代码
create by dingshaoran