Some people say that the current client interviews are getting more and more volume, and more and more content needs to be mastered by developers, from basic Java foundation, Android foundation, Android system principle, Android third-party library, hybrid development, data structure, Algorithms are all questions. If you want to get a good job opportunity, this is indeed the case. Below are the Android interview questions I summarized for everyone.
1, Android interview must ask Java basics
2, Android interview must ask Android basics
3, Android interview must ask advanced knowledge points
4. Android interview must ask performance optimization
1. HTTP and caching theory
1.1 HTTP caching strategy
The HTTP caching mechanism also relies on the parameter classes in the request and response headers. Whether the final response result is pulled from the cache or the server is a complete mechanism. The HTTP caching mechanism process is as follows.
HTTP caching can be divided into two types: mandatory caching and contrast caching
1.2 Mandatory caching
The server is required to participate in determining whether to continue to use the cache. When the client requests data for the first time, the server returns the expiration time of the cache (Expires and Cache-Control), and the cache can continue to be used without expiration. Otherwise, it is not applicable. Ask the server again.
Two flags used for mandatory caching:
- Expires : The value of Expires is the expiration time returned by the server, that is, the next request, the request time is less than the expiration time returned by the server, and the cached data is used directly. The expiration time is generated by the server, and there may be an error between the time of the client and the server.
- Cache-Control : Expires has a time verification problem, so HTTP1.1 uses Cache-Control instead of Expires.
Among them, the values of Cache-Control are as follows:
- private:: The client can be cached.
- public:: Both the client and the proxy server can be cached.
- max-age=xxx: The cached content will become invalid after xxx seconds
- no-cache: Need to use contrast cache to verify cached data.
- no-store: All content will not be cached; forced cache, contrast cache will not be triggered.
1.3 Compare cache
The server is required to participate in determining whether to continue to use the cache. When the client requests data for the first time, the server will return the cache identifier (Last-Modified/If-Modified-Since and Etag/If-None-Match) together with the data. The client and the client back up both to the cache. When requesting data again, the client sends the cache identifier of the last backup to the server, and the server judges based on the cache identifier. If 304 is returned, it means that the client is notified You can continue to use the cache.
Compare the two identifiers of the cache:
- Last-Modified: Indicates the time when the resource was last modified. When the client sends the first request, the server returns the time when the resource was last modified. An example of the return format is as follows:
Last-Modified: Tue, 12 Jan 2016 09:31:27 GMT
. - If-Modified-Since: The server receives the resource modification time sent by the client and compares it with its current resource modification time. If the resource modification time is greater than the resource modification time sent by the client, the resource has been modified If it is modified, 200 is returned to indicate that the resource needs to be requested again, otherwise 304 is returned to indicate that the resource has not been modified and the cache can be used continuously. Unlike If-Unmodified-Since, If-Modified-Since can only be used with GET or HEAD. When used in combination with If-None-Match, it will be ignored unless the server does not support If-None-Match.
2,OKHttp
The mainstream Android network request frameworks now include OKHttp and Retrofit, but the top layer of Retrofit also uses OKHttp. Before officially introducing OKHttp, let's take a look at some common status codes of Http:
- 100~199: Indication information, indicating that the request has been received and continue processing.
- 200~299: The request is successful, indicating that the request has been successfully received and understood.
- 300~399: Redirect, further operations must be performed to complete the request.
- 400~499: Client error, the request has a syntax error or the request cannot be fulfilled.
- 500~599: Server-side error, the server failed to fulfill a legal request.
2.1 OKHttp request process
The following is a general flow chart of OKHttp internally initiated requests, as shown in the figure below.
The following is the code for using OKHttp to make a Get request.
//1.新建OKHttpClient客户端
OkHttpClient client = new OkHttpClient();
//新建一个Request对象
Request request = new Request.Builder()
.url(url)
.build();
//2.Response为OKHttp中的响应
Response response = client.newCall(request).execute();
As you can see, when using OKHttp to make a request, you only need to create an OKHttpClient object and a Request object, and then call the execute() method and the enqueue() method. Among them, the execute() method is a synchronous method, and the enqueue() method is an asynchronous method.
2.2 OKHttpClient
Before using OKHttpClient, you need to create an OKHttpClient client. The construction method of OKHttpClient is as follows.
OkHttpClient client = new OkHttpClient();
public OkHttpClient() {
this(new Builder());
}
OkHttpClient(Builder builder) {
....
}
As you can see, OkHttpClient uses the builder mode, and the configurable parameters in the Builder are as follows.
public static final class Builder {
Dispatcher dispatcher;// 分发器
@Nullable Proxy proxy;
List<Protocol> protocols;
List<ConnectionSpec> connectionSpecs;// 传输层版本和连接协议
final List<Interceptor> interceptors = new ArrayList<>();// 拦截器
final List<Interceptor> networkInterceptors = new ArrayList<>();
EventListener.Factory eventListenerFactory;
ProxySelector proxySelector;
CookieJar cookieJar;
@Nullable Cache cache;
@Nullable InternalCache internalCache;// 内部缓存
SocketFactory socketFactory;
@Nullable SSLSocketFactory sslSocketFactory;// 安全套接层socket 工厂,用于HTTPS
@Nullable CertificateChainCleaner certificateChainCleaner;// 验证确认响应证书 适用 HTTPS 请求连接的主机名。
HostnameVerifier hostnameVerifier;// 验证确认响应证书 适用 HTTPS 请求连接的主机名。
CertificatePinner certificatePinner;// 证书锁定,使用CertificatePinner来约束哪些认证机构被信任。
Authenticator proxyAuthenticator;// 代理身份验证
Authenticator authenticator;// 身份验证
ConnectionPool connectionPool;// 连接池
Dns dns;
boolean followSslRedirects; // 安全套接层重定向
boolean followRedirects;// 本地重定向
boolean retryOnConnectionFailure;// 重试连接失败
int callTimeout;
int connectTimeout;
int readTimeout;
int writeTimeout;
int pingInterval;
// 这里是默认配置的构建参数
public Builder() {
dispatcher = new Dispatcher();
protocols = DEFAULT_PROTOCOLS;
connectionSpecs = DEFAULT_CONNECTION_SPECS;
...
}
// 这里传入自己配置的构建参数
Builder(OkHttpClient okHttpClient) {
this.dispatcher = okHttpClient.dispatcher;
this.proxy = okHttpClient.proxy;
this.protocols = okHttpClient.protocols;
this.connectionSpecs = okHttpClient.connectionSpecs;
this.interceptors.addAll(okHttpClient.interceptors);
this.networkInterceptors.addAll(okHttpClient.networkInterceptors);
...
}
2.3 Synchronization request
The synchronous request uses the execute() method, which is used as follows.
Response response = client.newCall(request).execute();
The following are some of the source code involved.
/**
* Prepares the {@code request} to be executed at some point in the future.
*/
@Override public Call newCall(Request request) {
return RealCall.newRealCall(this, request, false /* for web socket */);
}
// RealCall为真正的请求执行者
static RealCall newRealCall(OkHttpClient client, Request originalRequest, boolean forWebSocket) {
// Safely publish the Call instance to the EventListener.
RealCall call = new RealCall(client, originalRequest, forWebSocket);
call.eventListener = client.eventListenerFactory().create(call);
return call;
}
@Override public Response execute() throws IOException {
synchronized (this) {
// 每个Call只能执行一次
if (executed) throw new IllegalStateException("Already Executed");
executed = true;
}
captureCallStackTrace();
timeout.enter();
eventListener.callStart(this);
try {
// 通知dispatcher已经进入执行状态
client.dispatcher().executed(this);
// 通过一系列的拦截器请求处理和响应处理得到最终的返回结果
Response result = getResponseWithInterceptorChain();
if (result == null) throw new IOException("Canceled");
return result;
} catch (IOException e) {
e = timeoutExit(e);
eventListener.callFailed(this, e);
throw e;
} finally {
// 通知 dispatcher 自己已经执行完毕
client.dispatcher().finished(this);
}
}
Response getResponseWithInterceptorChain() throws IOException {
// Build a full stack of interceptors.
List<Interceptor> interceptors = new ArrayList<>();
// 在配置 OkHttpClient 时设置的 interceptors;
interceptors.addAll(client.interceptors());
// 负责失败重试以及重定向
interceptors.add(retryAndFollowUpInterceptor);
// 请求时,对必要的Header进行一些添加,接收响应时,移除必要的Header
interceptors.add(new BridgeInterceptor(client.cookieJar()));
// 负责读取缓存直接返回、更新缓存
interceptors.add(new CacheInterceptor(client.internalCache()));
// 负责和服务器建立连接
interceptors.add(new ConnectInterceptor(client));
if (!forWebSocket) {
// 配置 OkHttpClient 时设置的 networkInterceptors
interceptors.addAll(client.networkInterceptors());
}
// 负责向服务器发送请求数据、从服务器读取响应数据
interceptors.add(new CallServerInterceptor(forWebSocket));
Interceptor.Chain chain = new RealInterceptorChain(interceptors, null, null, null, 0,
originalRequest, this, eventListener, client.connectTimeoutMillis(),
client.readTimeoutMillis(), client.writeTimeoutMillis());
// 使用责任链模式开启链式调用
return chain.proceed(originalRequest);
}
// StreamAllocation 对象,它相当于一个管理类,维护了服务器连接、并发流
// 和请求之间的关系,该类还会初始化一个 Socket 连接对象,获取输入/输出流对象。
public Response proceed(Request request, StreamAllocation streamAllocation, HttpCodec httpCodec,
RealConnection connection) throws IOException {
...
// Call the next interceptor in the chain.
// 实例化下一个拦截器对应的RealIterceptorChain对象
RealInterceptorChain next = new RealInterceptorChain(interceptors, streamAllocation, httpCodec,
connection, index + 1, request, call, eventListener, connectTimeout, readTimeout,
writeTimeout);
// 得到当前的拦截器
Interceptor interceptor = interceptors.get(index);
// 调用当前拦截器的intercept()方法,并将下一个拦截器的RealIterceptorChain对象传递下去,最后得到响应
Response response = interceptor.intercept(next);
...
return response;
}
2.4 Asynchronous request
The asynchronous request uses enqueue(). According to the asynchronous writing method, we can follow a lot of parameters after the Builder, as shown below.
Request request = new Request.Builder()
.url("http://publicobject.com/helloworld.txt")
.build();
client.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
e.printStackTrace();
}
@Override
public void onResponse(Call call, Response response) throws IOException {
...
}
void enqueue(AsyncCall call) {
synchronized (this) {
readyAsyncCalls.add(call);
}
promoteAndExecute();
}
// 正在准备中的异步请求队列
private final Deque<AsyncCall> readyAsyncCalls = new ArrayDeque<>();
// 运行中的异步请求
private final Deque<AsyncCall> runningAsyncCalls = new ArrayDeque<>();
// 同步请求
private final Deque<RealCall> runningSyncCalls = new ArrayDeque<>();
// Promotes eligible calls from {@link #readyAsyncCalls} to {@link #runningAsyncCalls} and runs
// them on the executor service. Must not be called with synchronization because executing calls
// can call into user code.
private boolean promoteAndExecute() {
assert (!Thread.holdsLock(this));
List<AsyncCall> executableCalls = new ArrayList<>();
boolean isRunning;
synchronized (this) {
for (Iterator<AsyncCall> i = readyAsyncCalls.iterator(); i.hasNext(); ) {
AsyncCall asyncCall = i.next();
// 如果其中的runningAsynCalls不满,且call占用的host小于最大数量,则将call加入到runningAsyncCalls中执行,
// 同时利用线程池执行call;否者将call加入到readyAsyncCalls中。
if (runningAsyncCalls.size() >= maxRequests) break; // Max capacity.
if (runningCallsForHost(asyncCall) >= maxRequestsPerHost) continue; // Host max capacity.
i.remove();
executableCalls.add(asyncCall);
runningAsyncCalls.add(asyncCall);
}
isRunning = runningCallsCount() > 0;
}
for (int i = 0, size = executableCalls.size(); i < size; i++) {
AsyncCall asyncCall = executableCalls.get(i);
asyncCall.executeOn(executorService());
}
return isRunning;
}
2.5 CacheInterceptor network request cache processing
The caching principle of okHttp is that the cache interceptor will determine whether there is a cache available based on the requested information and the cached response information. If there is a cache that can be used, then the cache will be returned to the user, otherwise the chain of responsibility mode will continue to be used. Get the response from the server. When the response is obtained, the response is cached to disk. The codes involved are:
@Override
public Response intercept(Chain chain) throws IOException {
// 根据request得到cache中缓存的response
Response cacheCandidate = cache != null
? cache.get(chain.request())
: null;
long now = System.currentTimeMillis();
// request判断缓存的策略,是否要使用了网络,缓存或两者都使用
CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
Request networkRequest = strategy.networkRequest;
Response cacheResponse = strategy.cacheResponse;
if (cache != null) {
cache.trackResponse(strategy);
}
if (cacheCandidate != null && cacheResponse == null) {
closeQuietly(cacheCandidate.body()); // The cache candidate wasn't applicable. Close it.
}
// If we're forbidden from using the network and the cache is insufficient, fail.
if (networkRequest == null && cacheResponse == null) {
return new Response.Builder()
.request(chain.request())
.protocol(Protocol.HTTP_1_1)
.code(504)
.message("Unsatisfiable Request (only-if-cached)")
.body(Util.EMPTY_RESPONSE)
.sentRequestAtMillis(-1L)
.receivedResponseAtMillis(System.currentTimeMillis())
.build();
}
// If we don't need the network, we're done.
if (networkRequest == null) {
return cacheResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.build();
}
Response networkResponse = null;
try {
// 调用下一个拦截器,决定从网络上来得到response
networkResponse = chain.proceed(networkRequest);
} finally {
// If we're crashing on I/O or otherwise, don't leak the cache body.
if (networkResponse == null && cacheCandidate != null) {
closeQuietly(cacheCandidate.body());
}
}
// If we have a cache response too, then we're doing a conditional get.
// 如果本地已经存在cacheResponse,那么让它和网络得到的networkResponse做比较,决定是否来更新缓存的cacheResponse
if (cacheResponse != null) {
if (networkResponse.code() == HTTP_NOT_MODIFIED) {
Response response = cacheResponse.newBuilder()
.headers(combine(cacheResponse.headers(), networkResponse.headers()))
.sentRequestAtMillis(networkResponse.sentRequestAtMillis())
.receivedResponseAtMillis(networkResponse.receivedResponseAtMillis())
.cacheResponse(stripBody(cacheResponse))
.networkResponse(stripBody(networkResponse))
.build();
networkResponse.body().close();
// Update the cache after combining headers but before stripping the
// Content-Encoding header (as performed by initContentStream()).
cache.trackConditionalCacheHit();
cache.update(cacheResponse, response);
return response;
} else {
closeQuietly(cacheResponse.body());
}
}
Response response = networkResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.networkResponse(stripBody(networkResponse))
.build();
if (cache != null) {
if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {
// Offer this request to the cache.
// 缓存未经缓存过的response
CacheRequest cacheRequest = cache.put(response);
return cacheWritingResponse(cacheRequest, response);
}
if (HttpMethod.invalidatesCache(networkRequest.method())) {
try {
cache.remove(networkRequest);
} catch (IOException ignored) {
// The cache cannot be written.
}
}
}
return response;
}
2.6 Connection pool of ConnectInterceptor
ConnectInterceptor connection pool interceptor contains two aspects, one is the network interceptor that plays a role from the perspective of network connection, and the other is the interceptor that plays a role from the perspective of the operation of the connection pool.
@Override public Response intercept(Chain chain) throws IOException {
RealInterceptorChain realChain = (RealInterceptorChain) chain;
Request request = realChain.request();
StreamAllocation streamAllocation = realChain.streamAllocation();
// We need the network to satisfy this request. Possibly for validating a conditional GET.
boolean doExtensiveHealthChecks = !request.method().equals("GET");
// HttpCodec是对 HTTP 协议操作的抽象,有两个实现:Http1Codec和Http2Codec,顾名思义,它们分别对应 HTTP/1.1 和 HTTP/2 版本的实现。在这个方法的内部实现连接池的复用处理
HttpCodec httpCodec = streamAllocation.newStream(client, chain, doExtensiveHealthChecks);
RealConnection connection = streamAllocation.connection();
return realChain.proceed(request, streamAllocation, httpCodec, connection);
}
// Returns a connection to host a new stream. This // prefers the existing connection if it exists,
// then the pool, finally building a new connection.
// 调用 streamAllocation 的 newStream() 方法的时候,最终会经过一系列
// 的判断到达 StreamAllocation 中的 findConnection() 方法
private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,
int pingIntervalMillis, boolean connectionRetryEnabled) throws IOException {
...
// Attempt to use an already-allocated connection. We need to be careful here because our
// already-allocated connection may have been restricted from creating new streams.
// 尝试使用已分配的连接,已经分配的连接可能已经被限制创建新的流
releasedConnection = this.connection;
// 释放当前连接的资源,如果该连接已经被限制创建新的流,就返回一个Socket以关闭连接
toClose = releaseIfNoNewStreams();
if (this.connection != null) {
// We had an already-allocated connection and it's good.
result = this.connection;
releasedConnection = null;
}
if (!reportedAcquired) {
// If the connection was never reported acquired, don't report it as released!
// 如果该连接从未被标记为获得,不要标记为发布状态,reportedAcquired 通过 acquire() 方法修改
releasedConnection = null;
}
if (result == null) {
// Attempt to get a connection from the pool.
// 尝试供连接池中获取一个连接
Internal.instance.get(connectionPool, address, this, null);
if (connection != null) {
foundPooledConnection = true;
result = connection;
} else {
selectedRoute = route;
}
}
}
// 关闭连接
closeQuietly(toClose);
if (releasedConnection != null) {
eventListener.connectionReleased(call, releasedConnection);
}
if (foundPooledConnection) {
eventListener.connectionAcquired(call, result);
}
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.
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.
// 根据一系列的 IP地址从连接池中获取一个链接
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.
if (foundPooledConnection) {
// 如果我们在第二次的时候发现了一个池连接,那么我们就将其返回
eventListener.connectionAcquired(call, result);
return result;
}
// Do TCP + TLS handshakes. This is a blocking operation.
// 进行 TCP 和 TLS 握手
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;
}
From the above source code analysis, we can see:
- Determine whether the current connection can be used: whether the stream has been closed and has been restricted to create new streams;
- If the current connection cannot be used, a connection is obtained from the connection pool;
- No available connection is found in the connection pool, a new connection is created, handshake is performed, and then it is placed in the connection pool.
When obtaining a connection from the connection pool, the internal get() method is used. Internal has a static instance, which will be initialized in the static code block of OkHttpClient. We will call the get() method of the connection pool in the internal get() to get a connection. And, from this, we understand that one of the benefits of connection reuse is that it eliminates the need for a TCP and TLS handshake process. Because establishing a connection itself also takes some time, after the connection is reused, the efficiency of our network access can be improved.
Next, we will analyze in detail how ConnectionPool implements connection management.
OkHttp's cache management is divided into two steps. On the one hand, when we create a new connection, we have to put it in the cache; on the other hand, we have to clean the cache. In ConnectionPool, when we cache a connection in the connection pool, we only need to call the add() method of the deque to add it to the deque, and the operation of clearing the connection cache is handed over to the thread pool for timing carried out.
private final Deque<RealConnection> connections = new ArrayDeque<>();
void put(RealConnection connection) {
assert (Thread.holdsLock(this));
if (!cleanupRunning) {
cleanupRunning = true;
// 使用线程池执行清理任务
executor.execute(cleanupRunnable);
}
// 将新建的连接插入到双端队列中
connections.add(connection);
}
private final Runnable cleanupRunnable = new Runnable() {
@Override public void run() {
while (true) {
// 内部调用 cleanup() 方法来清理无效的连接
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 (Iterator<RealConnection> i = connections.iterator(); i.hasNext(); ) {
RealConnection connection = i.next();
// If the connection is in use, keep searching.
// 遍历所有的连接
if (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;
}
}
// maxIdleConnections 表示最大允许的闲置的连接的数量,keepAliveDurationNs表示连接允许存活的最长的时间。
// 默认空闲连接最大数目为5个,keepalive 时间最长为5分钟。
if (longestIdleDurationNs >= this.keepAliveDurationNs
|| idleConnectionCount > this.maxIdleConnections) {
// We've found a connection to evict. Remove it from the list, then close it below (outside
// of the synchronized block).
// 该连接的时长超出了最大的活跃时长或者闲置的连接数量超出了最大允许的范围,直接移除
connections.remove(longestIdleConnection);
} else if (idleConnectionCount > 0) {
// A connection will be ready to evict soon.
// 闲置的连接的数量大于0,停顿指定的时间(等会儿会将其清理掉,现在还不是时候)
return keepAliveDurationNs - longestIdleDurationNs;
} else if (inUseConnectionCount > 0) {
// All connections are in use. It'll be at least the keep alive duration 'til we run again.
// 所有的连接都在使用中,5分钟后再清理
return keepAliveDurationNs;
} else {
// No connections, idle or in use.
// 没有连接
cleanupRunning = false;
return -1;
}
}
From the above source code analysis, it can be seen that the connection in the cache is first traversed to find the connection with the longest idle time, and then it is determined whether the connection should be cleaned up based on the idle time of the connection and the maximum allowable number of connections. At the same time, note that the return value of the above method is a time. If the connection with the longest idle time still needs a period of time to be cleaned up, the time difference of this period of time will be returned, and then the connection pool will be cleaned up again after this period of time. .
3,Retrofit
Retrofit is a encapsulation of a RESTful HTTP network request framework. Essentially, the network request is completed by OkHttp, while Retrofit is only responsible for the encapsulation of the network request interface. The client uses Retrofit, which actually uses the Retrofit interface layer to encapsulate the request parameters, Header, Url and other information, and then OkHttp completes the subsequent request operations. When the server returns the data, OkHttp then passes the original results to Retrofit, and Retrofit then Analyze the results according to the needs of users.
3.1 Basic usage
First, define an HTTP API to describe the request. For example, the following is a Get request.
public interface GitHubService {
@GET("users/{user}/repos")
Call<List<Repo>> listRepos(@Path("user") String user);
}
Then, create a Retrofit and generate the implementation of the API. The return type is the return value type of the request, and the parameters of the method are the requested parameters.
// 1.Retrofit构建过程
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://api.github.com/")
.build();
// 2.创建网络请求接口类实例过程
GitHubService service = retrofit.create(GitHubService.class);
Finally, call the API method to generate a Call completion request.
// 3.生成并执行请求过程
Call<List<Repo>> repos = service.listRepos("octocat");
repos.execute() or repos.enqueue()
The above is an example of a simple Get request. POST request only needs to change the API definition to POST. The basic use process of Retrofit is very concise, but conciseness does not mean simplicity. In order to achieve this concise use process, Retrofit uses excellent architecture design and a large number of design patterns internally. If you read the latest version of Retrofit's source code carefully, you will find a lot of use Design pattern. For example, the Retrofit construction process will use the builder model and the factory method model, and the network request interface instance process will use the appearance model, the proxy model, the singleton model, the strategy model, and the decoration model (builder model) to generate and execute the request. process
Adapter mode (agent mode, decoration mode).
3.2 Source code analysis
3.2.1 Retrofit build process
1, Retrofit core object analysis
First of all, there is a global variable in Retrofit that is very important. In versions before V2.5, LinkedHashMap() is used, which is a network request configuration object, which is obtained after analyzing the method annotations in the network request interface.
public final class Retrofit {
private final Map<Method, ServiceMethod<?>> serviceMethodCache = new ConcurrentHashMap<>();
...
}
Retrofit uses the builder mode to create a Retrofit instance through the internal Builder class, as shown below.
public static final class Builder {
// 平台类型对象(Platform -> Android)
private final Platform platform;
// 网络请求工厂,默认使用OkHttpCall(工厂方法模式)
private @Nullable okhttp3.Call.Factory callFactory;
// 网络请求的url地址
private @Nullable HttpUrl baseUrl;
// 数据转换器工厂的集合
private final List<Converter.Factory> converterFactories = new ArrayList<>();
// 网络请求适配器工厂的集合,默认是ExecutorCallAdapterFactory
private final List<CallAdapter.Factory> callAdapterFactories = new ArrayList<>();
// 回调方法执行器,在 Android 上默认是封装了 handler 的 MainThreadExecutor, 默认作用是:切换线程(子线程 -> 主线程)
private @Nullable Executor callbackExecutor;
private boolean validateEagerly;
2. Builder's internal structure
public static final class Builder {
...
Builder(Platform platform) {
this.platform = platform;
}
public Builder() {
this(Platform.get());
}
...
}
class Platform {
private static final Platform PLATFORM = findPlatform();
static Platform get() {
return PLATFORM;
}
private static Platform findPlatform() {
try {
// 使用JVM加载类的方式判断是否是Android平台
Class.forName("android.os.Build");
if (Build.VERSION.SDK_INT != 0) {
return new Android();
}
} catch (ClassNotFoundException ignored) {
}
try {
// 同时支持Java平台
Class.forName("java.util.Optional");
return new Java8();
} catch (ClassNotFoundException ignored) {
}
return new Platform();
}
static class Android extends Platform {
...
@Override public Executor defaultCallbackExecutor() {
//切换线程(子线程 -> 主线程)
return new MainThreadExecutor();
}
// 创建默认的网络请求适配器工厂,如果是Android7.0或Java8上,则使
// 用了并发包中的CompletableFuture保证了回调的同步
// 在Retrofit中提供了四种CallAdapterFactory(策略模式):
// ExecutorCallAdapterFactory(默认)、GuavaCallAdapterFactory、
// va8CallAdapterFactory、RxJavaCallAdapterFactory
@Override List<? extends CallAdapter.Factory> defaultCallAdapterFactories(
@Nullable Executor callbackExecutor) {
if (callbackExecutor == null) throw new AssertionError();
ExecutorCallAdapterFactory executorFactory = new ExecutorCallAdapterFactory(callbackExecutor);
return Build.VERSION.SDK_INT >= 24
? asList(CompletableFutureCallAdapterFactory.INSTANCE, executorFactory)
: singletonList(executorFactory);
}
...
@Override List<? extends Converter.Factory> defaultConverterFactories() {
return Build.VERSION.SDK_INT >= 24
? singletonList(OptionalConverterFactory.INSTANCE)
: Collections.<Converter.Factory>emptyList();
}
...
static class MainThreadExecutor implements Executor {
// 获取Android 主线程的Handler
private final Handler handler = new Handler(Looper.getMainLooper());
@Override public void execute(Runnable r) {
// 在UI线程对网络请求返回数据处理
handler.post(r);
}
}
}
3. Add baseUrl
The most basic function of baseUrl is the process of converting String type url to OkHttp HttpUrl, the code involved is as follows.
public Builder baseUrl(String baseUrl) {
checkNotNull(baseUrl, "baseUrl == null");
return baseUrl(HttpUrl.get(baseUrl));
}
public Builder baseUrl(HttpUrl baseUrl) {
checkNotNull(baseUrl, "baseUrl == null");
List<String> pathSegments = baseUrl.pathSegments();
if (!"".equals(pathSegments.get(pathSegments.size() - 1))) {
throw new IllegalArgumentException("baseUrl must end in /: " + baseUrl);
}
this.baseUrl = baseUrl;
return this;
}
4. The build process
The main task of build() is to perform the creation of Retrofit objects, and the involved code is as follows.
public Retrofit build() {
if (baseUrl == null) {
throw new IllegalStateException("Base URL required.");
}
okhttp3.Call.Factory callFactory = this.callFactory;
if (callFactory == null) {
// 默认使用okhttp
callFactory = new OkHttpClient();
}
Executor callbackExecutor = this.callbackExecutor;
if (callbackExecutor == null) {
// Android默认的callbackExecutor
callbackExecutor = platform.defaultCallbackExecutor();
}
// Make a defensive copy of the adapters and add the defaultCall adapter.
List<CallAdapter.Factory> callAdapterFactories = new ArrayList<>(this.callAdapterFactories);
// 添加默认适配器工厂在集合尾部
callAdapterFactories.addAll(platform.defaultCallAdapterFactorisca llbackExecutor));
// Make a defensive copy of the converters.
List<Converter.Factory> converterFactories = new ArrayList<>(
1 + this.converterFactories.size() + platform.defaultConverterFactoriesSize());
// Add the built-in converter factory first. This prevents overriding its behavior but also
// ensures correct behavior when using converters thatconsumeall types.
converterFactories.add(new BuiltInConverters());
converterFactories.addAll(this.converterFactories);
converterFactories.addAll(platform.defaultConverterFactories();
return new Retrofit(callFactory, baseUrl, unmodifiableList(converterFactories),
unmodifiableList(callAdapterFactories), callbackExecutor, validateEagerly);
}
3.2.2 Create an instance of the network request interface
Retrofit.create() uses appearance mode and proxy mode to create an interface instance for network requests. The method of creating create() is as follows.
public <T> T create(final Class<T> service) {
Utils.validateServiceInterface(service);
if (validateEagerly) {
// 判断是否需要提前缓存ServiceMethod对象
eagerlyValidateMethods(service);
}
// 使用动态代理拿到请求接口所有注解配置后,创建网络请求接口实例
return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class<?>[] { service },
new InvocationHandler() {
private final Platform platform = Platform.get();
private final Object[] emptyArgs = new Object[0];
@Override public Object invoke(Object proxy, Method method, @Nullable Object[] args)
throws Throwable {
// If the method is a method from Object then defer to normal invocation.
if (method.getDeclaringClass() == Object.class) {
return method.invoke(this, args);
}
if (platform.isDefaultMethod(method)) {
return platform.invokeDefaultMethod(method, service, proxy, args);
}
return loadServiceMethod(method).invoke(args != null ? args : emptyArgs);
}
});
}
private void eagerlyValidateMethods(Class<?> service) {
Platform platform = Platform.get();
for (Method method : service.getDeclaredMethods()) {
if (!platform.isDefaultMethod(method)) {
loadServiceMethod(method);
}
}
}
Then, let's look at the loadServiceMethod() method.
ServiceMethod<?> loadServiceMethod(Method method) {
ServiceMethod<?> result = serviceMethodCache.get(method);
if (result != null) return result;
synchronized (serviceMethodCache) {
result = serviceMethodCache.get(method);
if (result == null) {
// 解析注解配置得到了ServiceMethod
result = ServiceMethod.parseAnnotations(this, method);
// 可以看到,最终加入到ConcurrentHashMap缓存中
serviceMethodCache.put(method, result);
}
}
return result;
}
abstract class ServiceMethod<T> {
static <T> ServiceMethod<T> parseAnnotations(Retrofit retrofit, Method method) {
// 通过RequestFactory解析注解配置(工厂模式、内部使用了建造者模式)
RequestFactory requestFactory = RequestFactory.parseAnnotations(retrofit, method);
Type returnType = method.getGenericReturnType();
if (Utils.hasUnresolvableType(returnType)) {
throw methodError(method,
"Method return type must not include a type variable or wildcard: %s", returnType);
}
if (returnType == void.class) {
throw methodError(method, "Service methods cannot return void.");
}
// 最终是通过HttpServiceMethod构建的请求方法
return HttpServiceMethod.parseAnnotations(retrofit, method, requestFactory);
}
abstract T invoke(Object[] args);
}
3.3 Retrofit flow chart
The latest version of Retrofit is 2.9.0, which has not been updated for more than half a year. Although Retrofit is only a RESTful HTTP network request framework package library. However, it encapsulates OkHttp through a large number of design patterns inside, which makes users feel that it is very concise and easy to understand. It mainly uses a dynamic proxy method to dynamically parse the annotations of the network request interface into an HTTP request, and finally execute the request process. The complete process of Retrofit is shown in the figure below.
4,Glide
4.1 Basic usage
As an Android image loading framework, Glide has the advantages of full-featured, high-performance, and simple to use. Use the following line of code to complete the loading and display of the picture.
Glide.with(context).load(url).into(iv);
In addition, we can also specify a placeholder image during the image loading process.
Glide.with(this)
.load(url)
.placeholder(R.drawable.noimage)
.into(iv);
4.2 Source code analysis
The following is a complete architecture diagram of the Glide framework.
4.2.1 with(context)
We use Glide, all started from the Glide.with() method, the source code is as follows.
public static RequestManager with(Context context) {
RequestManagerRetriever retriever = RequestManagerRetriever.get();
return retriever.get(context);
}
public static RequestManager with(Activity activity) {
RequestManagerRetriever retriever = RequestManagerRetriever.get();
return retriever.get(activity);
}
public static RequestManager with(FragmentActivity activity) {
RequestManagerRetriever retriever = RequestManagerRetriever.get();
return retriever.get(activity);
}
public static RequestManager with(android.app.Fragment fragment) {
RequestManagerRetriever retriever = RequestManagerRetriever.get();
return retriever.get(fragment);
}
public static RequestManager with(Fragment fragment) {
RequestManagerRetriever retriever = RequestManagerRetriever.get();
return retriever.get(fragment);
}
As you can see, there are many with() methods, but the content is basically the same. They are all obtained through RequestManagerRetriever.get() to obtain the RequestManagerRetriever object retriever, and then obtain a RequestManager object through retriever.get(context) and return. The key difference between these with() methods is that the parameters passed in are different, which can be Context, Activity, Fragment, and so on.
The reason why Glide is bound to the life cycle of the context passed in the with(context) method when loading the image, if the activity is passed in, then Glide will stop the loading of the image when the activity is destroyed. The advantage of this is that it avoids the consumption of redundant resources, and also avoids the problem of null pointers caused by loading pictures after the Activity is destroyed.
Next, let's take a look at the source code of the RequestManagerRetriever class.
public class RequestManagerRetriever implements Handler.Callback {
//饿汉式创建单例
private static final RequestManagerRetriever INSTANCE = new RequestManagerRetriever();
//返回单例对象
public static RequestManagerRetriever get() {
return INSTANCE;
}
//根据传入的参数,获取不同的RequestManager
public RequestManager get(Context context) {
if (context == null) {
throw new IllegalArgumentException("You cannot start a load on a null Context");
} else if (Util.isOnMainThread() && !(context instanceof Application)) {
if (context instanceof FragmentActivity) {
return get((FragmentActivity) context);
} else if (context instanceof Activity) {
return get((Activity) context);
} else if (context instanceof ContextWrapper) {
return get(((ContextWrapper) context).getBaseContext());
}
}
return getApplicationManager(context);
}
//省略无关代码......
}
The above code is a hungry Chinese singleton mode, the core is to obtain different RequestManager according to the incoming parameters. Next, we look at the getApplicationManager method.
private RequestManager getApplicationManager(Context context) {
// 返回一个单例
if (applicationManager == null) {
synchronized (this) {
if (applicationManager == null) {
applicationManager = new RequestManager(context.getApplicationContext(),
new ApplicationLifecycle(), new EmptyRequestManagerTreeNode());
}
}
}
return applicationManager;
}
getApplicationManager(Context context) creates and returns applicationManager through double check singleton mode. If the incoming context is Activity, the operation is as follows.
public RequestManager get(Activity activity) {
//如果不在主线程或者Android SDK的版本低于HONEYCOMB,传入的还是Application类型的context
if (Util.isOnBackgroundThread() || Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
return get(activity.getApplicationContext());
} else {
//判断当前activity是否被销毁
assertNotDestroyed(activity);
android.app.FragmentManager fm = activity.getFragmentManager();
//通过fragmentGet(activity, fm)获取RequestManager
return fragmentGet(activity, fm);
}
}
If the incoming context is Activity, the operation is as follows.
public RequestManager get(Activity activity) {
//如果不在主线程或者Android SDK的版本低于HONEYCOMB,传入的还是Application类型的context
if (Util.isOnBackgroundThread() || Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
return get(activity.getApplicationContext());
} else {
//判断当前activity是否被销毁
assertNotDestroyed(activity);
android.app.FragmentManager fm = activity.getFragmentManager();
//通过fragmentGet(activity, fm)获取RequestManager
return fragmentGet(activity, fm);
}
}
The following is a brief summary of the with() method.
- Get the RequestManagerRetriever singleton object through RequestManagerRetriever.get().
- Get the RequestManager through retriever.get(context), and do different processing by judging the context type in the get(context) method.
- When the context is Application, create and return a RequestManager object through getApplicationManager(Context context).
- When the context is Activity, create and add a fragment without an interface to the current activity through fragmentGet(activity, fm), so that the image loading is bound to the life cycle of the activity, and then a RequestManager object is created and returned.
4.2.2 load(url)
load(url) is mainly used to load pictures on the network, as shown below.
Glide.with(context)
.load(url)
.placeholder(R.drawable.place_image)
.error(R.drawable.error_image)
.into(imageView);
The source code of the load(url) method is as follows.
public DrawableTypeRequest<String> load(String string) {
return (DrawableTypeRequest<String>) fromString().load(string);
}
The load() method involves two methods, fromString() and load(string), let’s look at these two methods next.
fromString()
public DrawableTypeRequest<String> fromString() {
return loadGeneric(String.class);
}
private <T> DrawableTypeRequest<T> loadGeneric(Class<T> modelClass) {
ModelLoader<T, InputStream> streamModelLoader =
Glide.buildStreamModelLoader(modelClass, context);
ModelLoader<T, ParcelFileDescriptor> fileDescriptorModelLoader =
Glide.buildFileDescriptorModelLoader(modelClass, context);
if (modelClass != null && streamModelLoader == null && fileDescriptorModelLoader == null) {
throw new IllegalArgumentException("Unknown type " + modelClass + ". You must provide a Model of a type for"
+ " which there is a registered ModelLoader, if you are using a custom model, you must first call"
+ " Glide#register with a ModelLoaderFactory for your custom model class");
}
//这句是核心,本质是创建并返回了一个DrawableTypeRequest
return optionsApplier.apply(
new DrawableTypeRequest<T>(modelClass, streamModelLoader,
fileDescriptorModelLoader, context,
glide, requestTracker, lifecycle, optionsApplier));
}
The essence of the loadGeneric() method is to create and return a DrawableTypeRequest, a Drawable type request.
load(string)
The code of the load(string) method is as follows.
@Override
public DrawableRequestBuilder<ModelType> load(ModelType model) {
super.load(model);
return this;
}
This method first calls the load() method of GenericRequestBuilder, the parent class of DrawableRequestBuilder, and then returns to itself. Next, take a look at the load() method in the parent class of DrawableRequestBuilder, as shown below.
public GenericRequestBuilder<ModelType, DataType, ResourceType, TranscodeType> load(ModelType model) {
this.model = model;
isModelSet = true;
return this;
}
The parent class of DrawableRequestBuilder is GenericRequestBuilder. From the name, we can also see that the former is the builder of Drawable requests, and the latter is the general request builder. They have a child-parent relationship.
After the above analysis, we can know that a DrawableTypeRequest object will be returned after Glide.with(context).load(url). Its parent class is DrawableRequestBuilder, and the parent class of DrawableRequestBuilder is GenericRequestBuilder. We write placeHolder(), error () and so on. The methods for requesting configuration of related pictures are defined in GenericRequestBuilder.
4.2.3 into(imageView)
The first two parts in Glide create a Request. This Request can be understood as a configuration request for image loading. It should be noted that only a request is created, but not executed. Only when the into() method is called, the request will actually be executed. The source code of into(imageView) is as follows.
public Target<GlideDrawable> into(ImageView view) {
return super.into(view);
}
It is found that it calls the into() method of the parent class GenericRequestBuilder, then we continue to look at the into() method in GenericRequestBuilder.
public Target<TranscodeType> into(ImageView view) {
//确保在主线程
Util.assertMainThread();
//确保view不为空
if (view == null) {
throw new IllegalArgumentException("You must pass in a non null View");
}
//对ScaleType进行配置
if (!isTransformationSet && view.getScaleType() != null) {
switch (view.getScaleType()) {
case CENTER_CROP:
applyCenterCrop();
break;
case FIT_CENTER:
case FIT_START:
case FIT_END:
applyFitCenter();
break;
//$CASES-OMITTED$
default:
// Do nothing.
}
}
//核心
return into(glide.buildImageViewTarget(view, transcodeClass));
}
As you can see, the above method is the core code of into(), which is defined in the generic request builder, GenericRequestBuilder. The core of the method is the last line: into(glide.buildImageViewTarget(view, transcodeClass)), first create a Target type object through glide.buildImageViewTarget(view, transcodeClass), and then pass this target into the into() method in GenericRequestBuilder in.
Regarding the buildImageViewTarget(view, transcodeClass) method, we will not repeat it.
6,EventBus
6.1 Basic usage
6.1.1 Basic concepts
EventBus is a publish-subscribe event bus for Android. It simplifies the complexity of communication between various components in the application, especially the problem of communication between fragments, and can avoid many inconveniences caused by the use of broadcast communication.
EventBus consists of three roles: Publisher, Event and Subscriber.
- Event : Event, it can be of any type, EventBus will notify globally according to the event type.
- Subscriber : Event subscribers, before EventBus 3.0 we must define the methods that start with onEvent, namely onEvent, onEventMainThread, onEventBackgroundThread, and onEventAsync. After 3.0, the name of the event processing method can be taken at will, but it needs to be added Annotate @subscribe above, and specify the thread model, the default is POSTING.
- Publisher : The publisher of the event, which can publish the event in any thread. Generally, you can get an EventBus object by using EventBus.getDefault(), and then call the post(Object) method.
EventBus is a typical event publish-subscribe model. Events are delivered by publishers to subscribers through EvenentBus. The overall framework is as follows.
EventBus provides four threading models, namely:
- POSTING: By default, it means that the thread of the event processing function is in the same thread as the thread that publishes the event.
- MAIN: Indicates that the thread of the event processing function is in the main thread (UI) thread, so time-consuming operations cannot be performed here.
- BACKGROUND: Indicates that the thread of the event processing function is in the background thread, so UI operations cannot be performed. If the thread that publishes the event is the main thread (UI thread), then the event processing function will start a background thread. If the thread that publishes the event is a background thread, the event processing function uses this thread.
- ASYNC: It means that no matter which thread the event is published, the event processing function will always create a new child thread to run, and UI operations cannot be performed either.
6.1.2 Basic usage
EventBus use process is divided into 3 steps. First, define an event class.
public class SayHelloEvent {
private String message;
public void sayHellow(String message) {
this.message = message;
}
}
Then, prepare an event subscriber, in order to prevent performance consumption problems caused by the event, you also need to cancel the event subscription in the onStop life cycle.
@Subscribe(threadMode = ThreadMode.MAIN)
public void onMessageEvent(SayHelloEvent event) {
String message = event.getMessage();
...
}
@Override
public void onStart() {
super.onStart();
EventBus.getDefault().register(this);
}
@Override
public void onStop() {
EventBus.getDefault().unregister(this);
super.onStop();
}
Finally, call the EventBus.getDefault().post method to send the event where the event needs to be sent.
EventBus.getDefault().post(new SayHelloEvent("Hello,EventBus!!!"));
6.2 Source code analysis
6.2.1 EventBus.getDefault().register(this)
First, let's look at EventBus from the development of getDefault() method for obtaining EventBus instance.
public static EventBus getDefault() {
if (defaultInstance == null) {
synchronized (EventBus.class) {
if (defaultInstance == null) {
defaultInstance = new EventBus();
}
}
}
return defaultInstance;
}
As you can see, double check and lock singleton mode is used in getDefault() to create EventBus instance, and then we look at the construction method.
private static final EventBusBuilder DEFAULT_BUILDER = new EventBusBuilder();
public EventBus() {
this(DEFAULT_BUILDER);
}
In the default constructor of EventBus, another parameterized constructor is called, and a DEFAULT_BUILDER object of type EventBusBuilder is passed in. Let's look at the construction method of EventBusBuilder.
public class EventBusBuilder {
...
EventBusBuilder() {
}
...
}
Basically nothing is done inside, continue to check this parameterized construction method of EventBus.
private final Map<Class<?>, CopyOnWriteArrayList<Subscription>> subscriptionsByEventType;
private final Map<Object, List<Class<?>>> typesBySubscriber;
private final Map<Class<?>, Object> stickyEvents;
EventBus(EventBusBuilder builder) {
...
// 1
subscriptionsByEventType = new HashMap<>();
// 2
typesBySubscriber = new HashMap<>();
// 3
stickyEvents = new ConcurrentHashMap<>();
// 4
mainThreadSupport = builder.getMainThreadSupport();
mainThreadPoster = mainThreadSupport != null ? mainThreadSupport.createPoster(this) : null;
backgroundPoster = new BackgroundPoster(this);
asyncPoster = new AsyncPoster(this);
...
// 5
subscriberMethodFinder = new SubscriberMethodFinder(builder.subscriberInfoIndexes,
builder.strictMethodVerification, builder.ignoreGeneratedIndex);
// 从builder取中一些列订阅相关信息进行赋值
...
// 6
executorService = builder.executorService;
}
In note 1, a subscriptionsByEventType object is created, and you can see that it is a subscriptionsByEventType object of type HashMap, and its key is of type Event, and value is a linked list of Subscriptions. The Subscription here is a subscription information object, which stores two important fields, one is a subscriber of type Object, which is the registered object (usually an Activity object in Android); the other is of type SubscriberMethod of SubscriberMethod, it is the subscription method annotated by @Subscribe, which stores an important field eventType, which is of type Class<?> and represents the type of Event. In Note 2, a new typesBySubscriber object of type Map is created. Its key is the subscriber object, and the value is all the Event type linked lists in the subscriber object. In daily use, it is only used to determine whether an object has been registered. In Note 3, a stickyEvents object of type ConcurrentHashMap is created. It is a field dedicated to sticky event processing. The key is the Class object of the event, and the value is the current event.
Some students may not know about sticky events. The so-called sticky events are relative to ordinary events. Ordinary events are registered before the event is sent before they can be received; while sticky events can be received by subscribing to the event after the event is sent. In addition, sticky events will be stored in the memory. Every time you enter, you will find the latest sticky events in the memory, unless you manually unregister them.
Then, we look at
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。