前一篇:Android面试知识点总结(三)—— 通信/服务/网络篇
OkHttp原理
为什么设置两个行列runningAsyncCalls & readyAsyncCalls
?,为什么运用SynchronousQueue?
- 运用2个Deque在外围保护整个恳求链路,便利办理恳求与操作
- SynchronousQueue为无容量行列,在线程池中能直接去运转,不需求进行等候,由于外围现已针对恳求行列进行了相应的保护
为什么拜访同一个目标机器恳求数量小于5 ,运转行列最大上限为64?
- 由于Http协议关于同一协议最大恳求并发数约束导致,
Android = 4,web 6,9
- 由于Deque内存占用默以为16,运转行列相当于4次扩容
行列为什么是ArrayDeque ?
- 用作栈时,功能优于
Stack
,当用于行列时,功能优于LinkedList
- 两头都可以操作,便利进行办理,支撑双向迭代器
ThreadPoolExecutor线程池首要的创立参数有哪些
参阅:ThreadPoolExecutor 参数解析
//OkHttp的装备
public synchronized ExecutorService executorService() {
if (executorService == null) {
executorService = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(), Util.threadFactory("OkHttp Dispatcher", false));
}
return executorService;
}
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory) {
//...
}
-
corePoolSize :中心线程数量
- 即便没有使命履行,中心线程也会一向存活
- 线程数小于中心线程时,即便有闲暇线程,线程沲也会创立新线程履行使命
- 设置allowCoreThreadTimeout=true时,中心线程会超时封闭
-
maximumPoolSize: 最大线程数
- 当一切中心线程都在履行使命,且使命行列已满时,线程沲会创立新线程履行使命
- 当线程数 = maxPoolSize,且使命行列已满,此刻增加使命时会触发RejectedExecutionHandler进行处理
-
keepAliveTime 、TimeUnit:线程闲暇时刻
- 假如线程数 > corePoolSize,且有线程闲暇时刻抵达keepAliveTime时,线程会毁掉,直到线程数量 = corePoolSize
- 假如设置allowCoreThreadTimeout = true时,中心线程履行完使命也会毁掉直到数量 = 0
-
workQueue: 使命行列
- ArrayBlockingQueue 有界行列,需求指定行列巨细
- LinkedBlockingQueue 若指定巨细则和ArrayBlockingQueue类似,若不指定巨细则默许能存储Integer.MAX_VALUE个使命,相当于无界行列,此刻maximumPoolSize值其实是无意义的
- SynchronousQueue 同步堵塞行列,当有使命增加进来后,必须有线程从行列中取出,当时线程才会被开释,newCachedThreadPool就运用这种行列
-
threadFactory: 创立线程的工厂
- 经过他可以创立线程时做一些想做的事,比方自界说线程称号
OkHttp缓存机制
Http协议下的缓存机制
强制缓存:经过http协议所传送的数据,会被保存到缓存数据库中
比照缓存:经过回来值(304运用比照缓存
,200运用服务器最新数据
)确认是否运用
Http本身缓存机制
- 依据文件进行磁盘缓存
- 内部保护依据LRU算法的缓存整理线程
OkHttp在CacheInterceptor
阻拦器中进行缓存的断定机制:
-
以Request为key从Cache中读取候选缓存
-
依据「当时时刻,Request,候选缓存」构建一个缓存战略,用于判别当时恳求是否需求运用网络,是否存在缓存
-
依据缓存战略,假如当时恳求不运用网络且没有缓存,直接报错并回来状况码504
-
依据缓存战略,假如当时恳求不运用网络且存在缓存,直接回来缓存数据
-
进行网络操作,将恳求交给下面的阻拦器处理,一起取得回来的Response
-
若经过网络回来的Response的状况码为304,混合缓存Response和网络回来的Response的恳求头,更新缓存并回来缓存Response
-
读取网络回来的Response,判别是否需求缓存,假如需求则对Response进行缓存
缓存战略首要是依据CacheStrategy中的networkRequest和cacheResponse来决定的:
networkRequest | cacheResponse | 对应处理 |
---|---|---|
null | null | 直接报错,状况码回来504 |
null | non-null | 直接回来缓存Response |
non-null | null | 恳求最新数据,并满意缓存条件则缓存Response |
non-null | non-null | 网络Response状况码为304,则混合恳求头后更新缓存,并回来缓存;若为200,直接回来网络Response,满意缓存条件则缓存Response |
五大阻拦器是那几个?对应的效果是什么?
参阅:OKHhttp恳求流程-五大阻拦器
-
RetryAndFollowUpInterceptor
重试重定向阻拦器
担任判别用户是否撤销了恳求;恳求失利依据条件判别是否重试,在取得了成果之后会依据相应码判别是否需求重定向
-
BridgeInterceptor
桥接阻拦器
担任将http协议标准的恳求头补全,并增加一些默许的行为
-
CacheInterceptor
缓存阻拦器
担任读取并判别是否运用缓存,同一个host的恳求,假如存在缓存就先读出缓存,不然就去恳求服务器,拿到成果后讲缓存存到磁盘,供下次运用
-
ConnectInterceptor
链接阻拦器
担任判别衔接池里边是否存在创立好的socket流,判别当时的衔接是否可以运用,流是否现已被封闭,是否现已被约束创立新的流,假如当时的衔接无法运用,就从衔接池中获取一个衔接,假如衔接池中也没有发现可用的衔接,创立一个新的衔接,并进行握手,然后将其放到衔接池中。省去了重复性的TCP/TL握手挥手进程,进步网络拜访功率
-
CallServerInterceptor
恳求服务器阻拦器
担任调用上层阻拦器传过来的RealConnection 和 HttpCodec,发送Request,并接纳 Response 然后回来给上层阻拦器。
总结:
retryAndFollowUpInterceptor 担任恳求的重试和重定向
BridgeInterceptor 担任补全恳求头字段
CacheInterceptor 担任Response的缓存
ConnectInterceptor 担任树立Http衔接和衔接的复用
CallServerInterceptor 发送恳求数据和读取呼应数据
OKHttp有哪些阻拦器,分别起什么效果
OKHTTP
的阻拦器是把一切的阻拦器放到一个list里,然后每次顺次履行阻拦器,而且在每个阻拦器分红三部分:
- 预处理阻拦器内容
- 经过
proceed
办法把恳求交给下一个阻拦器 - 下一个阻拦器处理完结并回来,后续处理作业。
这样顺次下去就形成了一个链式调用,看看源码,详细有哪些阻拦器:
Response getResponseWithInterceptorChain() throws IOException {
// Build a full stack of interceptors.
List<Interceptor> interceptors = new ArrayList<>();
interceptors.addAll(client.interceptors());
interceptors.add(retryAndFollowUpInterceptor);
interceptors.add(new BridgeInterceptor(client.cookieJar()));
interceptors.add(new CacheInterceptor(client.internalCache()));
interceptors.add(new ConnectInterceptor(client));
if (!forWebSocket) {
interceptors.addAll(client.networkInterceptors());
}
interceptors.add(new CallServerInterceptor(forWebSocket));
Interceptor.Chain chain = new RealInterceptorChain(
interceptors, null, null, null, 0, originalRequest);
return chain.proceed(originalRequest);
}
依据源码可知,一共七个阻拦器:
-
addInterceptor(Interceptor)
,这是由开发者设置的,会依照开发者的要求,在一切的阻拦器处理之前进行最早的阻拦处理,比方一些公共参数,Header都可以在这儿增加。 -
RetryAndFollowUpInterceptor
,这儿会对衔接做一些初始化作业,以及恳求失利的充实作业,重定向的后续恳求作业。跟他的名字相同,便是做重试作业还有一些衔接跟踪作业。 -
BridgeInterceptor
,这儿会为用户构建一个可以进行网络拜访的恳求,一起后续作业将网络恳求回来的呼应Response转化为用户可用的Response,比方增加文件类型,content-length核算增加,gzip解包。 -
CacheInterceptor
,这儿首要是处理cache相关处理,会依据OkHttpClient目标的装备以及缓存战略对恳求值进行缓存,而且假如本地有了可⽤的Cache,就可以在没有网络交互的状况下就回来缓存成果。 -
ConnectInterceptor
,这儿首要便是担任树立衔接了,会树立TCP衔接或许TLS衔接,以及担任编码解码的HttpCodec -
networkInterceptors
,这儿也是开发者自己设置的,所以本质上和第一个阻拦器差不多,可是由于方位不同,所以用处也不同。这个方位增加的阻拦器可以看到恳求和呼应的数据了,所以可以做一些网络调试。 -
CallServerInterceptor
,这儿便是进行网络数据的恳求和呼应了,也便是实际的网络I/O操作,经过socket读写数据。
运用阻拦器和网络阻拦器有什么差异?
从整个职责链路来看,运用阻拦器是最先履行的阻拦器,也便是用户自己设置request属性后的原始恳求,而网络阻拦器位于ConnectInterceptor和CallServerInterceptor之间,此刻网络链路现已准备好,只等候发送恳求数据。它们首要有以下差异。
- 首要,运用阻拦器在RetryAndFollowUpInterceptor和CacheInterceptor之前,所以一旦产生过错重试或许网络重定向,网络阻拦器或许履行屡次,由于相当于进行了二次恳求,可是运用阻拦器永久只会触发一次。别的假如在CacheInterceptor中命中了缓存就不需求走网络恳求了,因而会存在短路网络阻拦器的状况。
- 其次,除了CallServerInterceptor之外,每个阻拦器都应该至少调用一次realChain.proceed办法。实际上在运用阻拦器这层可以屡次调用proceed办法(本地反常重试)或许不调用proceed办法(中止),可是网络阻拦器这层衔接现已准备好,可且仅可调用一次proceed办法。
- 终究,从运用场景看,运用阻拦器由于只会调用一次,通常用于计算客户端的网络恳求建议状况;而网络阻拦器一次调用代表了必定会建议一次网络通信,因而通常可用于计算网络链路上传输的数据。
OkHttp怎样完结衔接池
为什么需求衔接池?
频繁的进行树立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办法计数并回来这个衔接。
假如没找到可用衔接,就会创立新衔接,并会把这个树立的衔接参加到双端行列中,一起开始运转线程池中的线程,其实便是调用了ConectionPool
的put办法。
public final class ConnectionPool {
void put(RealConnection connection) {
if (!cleanupRunning) {
//没有衔接的时分调用
cleanupRunning = true;
executor.execute(cleanupRunnable);
}
connections.add(connection);
}
}
3)其实这个线程池中只要一个线程,是用来整理衔接的,也便是上述的cleanupRunnable
private final Runnable cleanupRunnable = new Runnable() {
@Override
public 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 + 1
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;
}
}
//假如超越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分钟,则将该衔接整理掉。
怎样归于闲暇衔接?
其实便是有关方才提到的一个办法acquire
计数办法:
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
增加进该列表中,假如衔接封闭以后就将该目标移除。
总结:
衔接池的作业就这么多,首要便是办理双端行列Deque<RealConnection>
,可以用的衔接就直接用,然后定期整理衔接,一起经过对StreamAllocation
的引证计数完结主动收回。
OkHttp里边用到了什么规划形式
- 职责链形式
这个不要太显着,可以说是okhttp的精髓地点了,首要表现便是阻拦器的运用,详细代码可以看看上述的阻拦器介绍。
- 建造者形式
在Okhttp中,建造者形式也是用的挺多的,首要用处是将目标的创立与表示相别离,用Builder拼装各项装备。 比方Request:
public class Request {
public static class Builder {
@Nullable HttpUrl url;
String method;
Headers.Builder headers;
@Nullable RequestBody body;
public Request build() {
return new Request(this);
}
}
}
- 工厂形式
工厂形式和建造者形式类似,差异就在于工厂形式侧重点在于目标的生成进程,而建造者形式首要是侧重目标的各个参数装备。 例子有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;
//...
}
- 观察者形式
之前我写过一篇文章,是关于Okhttp中websocket的运用,由于webSocket归于长衔接,所以需求进行监听,这儿是用到了观察者形式:
final WebSocketListener listener;
@Override public void onReadMessage(String text) throws IOException {
listener.onMessage(this, text);
}
-
单例形式:
每个OkHttpClient 目标都办理自己独有的线程池和衔接池,运用单例同享OkHttpClient目标
-
享元形式:
在Dispatcher的线程池中,所用到了享元形式,一个不限容量的线程池 , 线程闲暇时存活时刻为 60 秒。线程池完结了目标复用,下降线程创立开支,从规划形式上来讲,运用了享元形式。(享元形式:尝试重用现有的同类目标,假如未找到匹配的目标,则创立新目标,首要用于削减创立目标的数量,以削减内存占用和进步功能)
运用OkHttp发送网络恳求并依据恳求成果改写UI有哪几种办法
- 运用AsyncTask + OkHttp的同步恳求
- 运用OkHttp的异步恳求+runOnUiThread办法(或许经过Handler发送到UI线程)
可否介绍一下OkHttp的整个异步恳求流程
经过OkHttpClient目标和Request目标创立Call 目标,运用Call调用enqueue
办法,经过该办法调用分发器Dispather的enqueue办法,将AsyncCall入队并提交给线程池履行,线程池中的线程会调用Call的execute()办法,Call的execute()办法会调用getResponseWithInterceptorChain()办法经过一系列阻拦器对恳求进行处理之后宣布该恳求并读取呼应
留意:异步调用恳求时,存在2个行列(运转,等候),需求满意运转行列小于64而且同一个host小于5,才会参加运转行列进行恳求,不然会参加等候行列进行等候
OkHttp关于网络恳求都有哪些优化,怎样完结的
-
分发器线程池的引入
异步2个行列加上线程池处理无容量行列
-
缓存
okhttp 的缓存战略是,key 为 Request的 url 的 MD5 值,value 为 response
阻拦器完结缓存效果,无网络状况,判别有无缓存,有则回来,有网络状况,无缓存,则缓存本次恳求,有则在满意回来值304时,混合恳求头后更新缓存并回来
-
多路复用
衔接池,socket复用机制,5分钟内保存5个衔接,有就直接复用,没有创立后放入
-
支撑gzip紧缩
当 response 经过 bridgeInterceptor 处理的时分会进行 gzip 紧缩,这样可以大大减小咱们的 response ,他不是什么状况下都紧缩的,只要在Encoding == null 而且Range == null进行紧缩
if (userRequest.header("Accept-Encoding") == null && userRequest.header("Range") == null) { transparentGzip = true; requestBuilder.header("Accept-Encoding", "gzip"); }
OkHttp源码中用到的中心类有哪些,简略讲一下
OkhttpClient :
对外的API,OkHttp的许多功能模块,全部包装进这个类;创立分为两种:一种是new OkHttpClient()的办法;另一种是运用建造者(Builder)形式 – new OkHttpClient.Builder()…Build()。 那么这两种办法有什么差异呢? 第一种:new OkHttpClient(),OkHttp做了许多作业,许多咱们需求的参数在这儿都取得默许值,也便是默许值设定。 第二种:默许的设置和第一种办法相同,可是咱们可以运用建造者形式单独的设置每一个属性; 留意事项:OkHttpClient强烈建议大局单例运用,由于每一个OkHttpClient都有自己单独的衔接池和线程池,复用衔接池和线程池可以削减推迟、节约内存。
RealCall:
集成Call类,从源代码中,可看到运用Call类,发送出(同步/异步)恳求.RealCall的首要效果:发送恳求,傍边还有阻拦器的树立进程,异步回调。
Dispatcher(分发器,调度器,多线程):
保存同步和异步Call的地方,并担任履行异步AsyncCall
Interceptor:
有用户自界说的Interceptor、RetryAndFollowUpInterceptor、BridgeInterceptor、CacheInterceptor、ConnectInterceptor、 CallServerInterceptor。阻拦器之所以可以顺次调用,并终究再从后向前回来Response,都依赖于RealInterceptorChain的proceed办法.
RealInterceptorChain(阻拦器链):
getResponseWithInterceptorChain()办法中,先创立了一个阻拦器列表interceptors,当阻拦器列表拼装完结,就会实例化阻拦器链目标RealInterceptorChain:
Response getResponseWithInterceptorChain() throws IOException {
//...
Interceptor.Chain chain = new RealInterceptorChain(interceptors, null, null, null, 0,
originalRequest, this, eventListener, client.connectTimeoutMillis(),
client.readTimeoutMillis(), client.writeTimeoutMillis());
return chain.proceed(originalRequest);
}
然后调用该目标的proceed办法进行发送恳求并接纳呼应。
为什么OkHttp好用呢?OkHttp有什么特点呢?
- 支撑http2,对一台主机的一切恳求同享同一个socket 衔接
- 内置衔接池,支撑衔接复用,削减推迟
- 支撑透明的gzip紧缩呼应体
- 经过缓存避免不必要的网络恳求
- 恳求失利时主动重试主机的其他ip,主动重定向
- 好用的API,比方供给装备dns的api,可以装备httpdns
OkHttp怎样完结断点续传流程,用什么保存
获取本地已下载文件的长度(无文件为0),经过获取简要下载文件的长度(HTTP相应头部content-Length),比照长度后,在下载恳求的头部文件中装备Range范围
okhttp完结带进展上传下载
OkHttp把恳求和呼应分别封装成了RequestBody和ResponseBody,下载进展自界说ResponseBody,重写source()办法,上传进展自界说RequestBody,重写writeTo()办法
为什么response.body().string() 只能调用一次
第一次拿到字节约后就默许封闭了这个流
Handler原理
面试题
-
用处
- 安排 Message 和 runnables 在将来的某个时刻履行
- 在不同的线程上履行操作并排入行列。(在多个线程并发更新UI的一起确保线程安全)
-
Message:承受和处理音讯目标
-
MessageQueue:办理Message的行列,先进先出,每一个线程最多存在一个
MessageQueue首要包括两个操作:
刺进
和读取
,MessageQueue 内部经过一个单链表数据结构保护音讯列表next:一个无限循环的办法,假如音讯行列中没有音讯,那么 next 办法会一向堵塞。当有新音讯到来时,next 办法会回来这条音讯并将其从单链表中移除
-
Looper:音讯泵,是 MessageQueue 的办理者,会不断从 MessageQueue 中取出音讯,并将音讯分给对应的 Handler 处理,每个线程只要一个 Looper
Looper 会不断地从 MessageQueue 中 查看是否有新音讯,假如有新音讯就会立刻处理,不然会一向堵塞
拓展:可经过 Looper.prepare() 为当时线程创立一个 Looper,除了 prepare 办法外,Looper 还供给了
prepareMainLooper
办法,Looper 供给了quit
和quitSafely
来退出一个 Looper1.prepareMainLooper
:给 ActivityThread(主线程) 创立 Looper 运用,本质也是经过 prepare 办法完结的2.quit与quitSafely差异
:quit
会直接退出 Looper,而quitSafly
仅仅设定一个退出符号,然后把音讯行列中的已有音讯处理完毕后才安全地退出留意:loop 办法是一个死循环,仅有跳出循环的办法是 MessageQueue 的 next 办法回来了null,只要运用quit办法告诉音讯退出行列,使行列next符号为null来抵达堵塞意图,当没有音讯时,next会一向堵塞,导致loop办法一向堵塞
handler的恳求发送到哪里去了?运用post()跟sendMessage()有没有什么差异
-
handler终究的恳求都发送到了MessageQueue中enqueueMessage()办法中了,该办法中会履行一个死循环不断的处理Message音讯
-
不论是post(),仍是sendXX的其他办法,终究都会进入到enqueueMessage()办法中,本质上没有差异,便是post办法会传入一个Runnable,承受音讯会在run()办法中,send假如不主动给Message中的callback**
Runnable
目标赋值的话,承受音讯会在handlerMessage中进行回来(handlerMessage可以经过完结Handler.CallBack完结,也可以直接完结重写handler的handlerMessage办法完结
**)留意:当Message中Runnable不为空时,只会履行闭包(
无参无回来
)函数run不会回传,为null时,会将包体数据经过handlerMessage回传回来
Looper什么时分被创立的?,怎样消费的?
在主线程ActivityThread
的main()中调用prepareMainLooper()创立,随后调用loop办法,进行死循环,不断的调用MessageQueue中的next办法死循环获取Message目标,然后经过Message目标依据target拿到handler目标调用dispatchMessage()进行音讯的分发
一个线程有几个Handler ?一个线程有几个Looper ?怎样确保?
- Handler个数与地点线程无关,可以在线程中实例化恣意个数的Handler。
- Looper的结构办法被声明为了private,咱们无法经过new关键字来实例化Looper,仅有敞开的可以实例化Looper的办法是prepare()
- 实例化Looper并将实例化后的Looper保存到ThreadLocal中,而假如ThreadLocal中现已保存了Looper,则会抛出一个RuntimeException的反常。那么意味着在一个线程中最多只能调用一次prepare()办法,这样就确保了Looper的仅有性。
Looper、Handler、MessageQueue、Message之间的联系
主线程创立Looper时,一起会创立一个MessageQueue目标,然后经过Looper的loop办法去对MessageQueue中的Message进行分发,Message得到处理后会调用Handler中的dispatchMessage调用run发送数据
Looper是音讯泵,是 MessageQueue 的办理者,MessageQueue是办理Message的行列,Message是承受和处理音讯的载体,Handler是音讯通讯的桥梁用来发送和承受处理
Handler、Thread和HandlerThread、IntentService的差异
参阅:IntentService 和 HandlerThread 的原理
- Handler是音讯通讯的桥梁,首要用来发送音讯和处理音讯
- Thread是一个普通的线程
- HandlerThread是一个带有Looper的线程,在其run()办法中调用Looper.prepare()创立了Looper实例,而且开启了loop()
- IntentService是 Service 的一个子类,它的内部有一个 Handler 和 HandlerThread。所以 IntentService 与 Service 最大的不同便是 IntentService 在后台开启了一个子线程,而 Service 并没有,它仍是在 UI 线程里。IntentService 经过 Handler 和 HandlerThread 来开启一个线程
Handler线程是怎样完结切换的?
当在A线程中创立handler的时分,一起创立了MessageQueue与Looper,Looper在A线程中调用loop进入一个无限的for循环从MessageQueue中取音讯,当B线程调用handler发送一个message的时分,会经过音讯发送时存入的一个Handler目标去调用dispatchMessage()办法,将message刺进到handler对应的MessageQueue中,由于Looper.loop()是在A线程中发动的,所以则回到了A线程,抵达了从B线程切换到A线程的意图。
Handler内存走漏的原因是什么?怎样解决?
经过匿名内部类的办法来实例化Handler,而非静态的匿名内部类默许持有外部类的引证,即匿名内部类Handler持有了外部类。由于Handler的生命周期与宿主的生命周期不共同然后触发了内存走漏
比方说在Activity中实例化了一个非静态的匿名内部类Handler,然后经过Handler发送了一个推迟音讯,可是在音讯还未履行时完毕了Activity,此刻由于Handler持有Activity,就会导致Activity无法被GC收回,也便是出现了内存走漏的问题。
解决办法:将 Handler
界说成静态的内部类,在内部持有 Activity
的弱引证,并在 Acitivity
的 onDestroy()
中调用 handler.removeCallbacksAndMessages(null)
及时移除一切音讯。
Message数据结构是啥?为什么这样规划?,每次创立时都是创立新的目标吗?收回机制是什么?
- Message是一种单链表结构,是一种非线性、非连续性物理结构,由n个独立节点衔接组成
- 由于loop跟MessageQueue中存在死循环,为了便利合理的去运用内存空间
- Message内部存在一个目标池,每次创立时会优先从目标池中进行拿去,假如不存在才会创立新的目标。
- Message在每次收回会调用recycle办法,recycle会将该目标进行初始化并存入目标池中,供后续运用
MessageQueue数据结构?怎样存储数据?
满意先进先出,在队尾增加数据,队首读取数据或许删去,MessageQueue
是一个用于存储音讯、用链表完结的特别行列结构
经过死循环,运用快慢指针p和prev,每次向后移动一格,直到找到某个节点p的when大于咱们要刺进音讯的when字段,则刺进到p和prev之间。 或许遍历到链表完毕,刺进到链表结束。
推迟是怎样完结的?
handler.postDelay
并不是先等候必定的时刻再放入到MessageQueue中,而是直接进入MessageQueue,以Message的时刻次序排列和唤醒的办法结合完结的。
Looper 的 loop() 死循环为什么不卡死?
当音讯不行用或许没有音讯的时分就会堵塞在next办法,而堵塞的办法是经过pipe/epoll机制,会在 MessageQueue.next()中调用nativePollOnce()办法,此刻线程会开释CPU资源进入休眠状况,直到下个音讯抵达或许有事务产生,经过往pipe管道写端写入数据来唤醒主线程作业
总结:在没有音讯时会处于休眠状况,有音讯才会被换新,并不会耗费许多CPU资源
**epoll机制:
**是一种I/O多路复用的机制,详细逻辑便是一个进程可以监督多个描绘符,当某个描绘符安排妥当(一般是读安排妥当或许写安排妥当),可以告诉程序进行相应的读写操作,这个读写操作是堵塞的。在Android中,会创立一个Linux管道(Pipe)
来处理堵塞和唤醒。
- 当音讯行列为空,管道的读端等候管道中有新内容可读,就会经过
epoll
机制进入堵塞状况。 - 当有音讯要处理,就会经过管道的写端写入内容,唤醒主线程(
enqueueMessage
办法needWake`字段履行唤醒操作)。
loop() 后的处理为什么不行履行
由于 loop() 是死循环,直到 quit 前后边的处理都无法得到履行,所以避免将处理放在 loop() 的后边。
异步 Message 或同步屏障
异步 Message
:设置了 isAsync 属性的 Message 实例
可以用异步 Handler 发送 也可以调用 Message#setAsynchronous() 直接设置为异步 Message
同步屏障
:在 MessageQueue 的某个方位放一个 target 属性为 null 的 Message,确保此后的非异步 Message 无法履行,只能履行异步 Message
原理:当 MessageQueue 轮循 Message 时分发现树立了同步屏障的时分,会去跳过其他 Message,读取下个 async 的 Message 并履行,屏障移除之前同步 Message 都会被堵塞
总结:异步音讯一般是体系内部运用的,当handler收到异步音讯时,会优先处理异步音讯,比及异步音讯处理完后,才会处理同步音讯
IdleHandler 闲暇 Message
适用于期望闲暇时分履行,但不影响主线程操作的使命
体系运用:
Activity destroy 回调就放在了 IdleHandler 中,ActivityThread 中 GCHandler 运用了 IdleHandler,在闲暇的时分履行 GC 操作
App运用:
发送一个回来 true 的 IdleHandler,在里边让某个 View 不断闪耀,这样当用户发愣时就可以诱导用户点击这个 View ,将某部分初始化放在 IdleHandler 里不影响 Activity 的发动
RecyclerView原理-缓存战略
参阅:真实带你搞懂 RecyclerView 的缓存机制
- ViewHolder是View的容器,一个ViewHolder中包括一个View,一个View也就对应一个ViewHolder
- Recycler是RecyclerView的一个内部类,首要担任ViewHolder的收回和复用
四级缓存机制:
层级 | 缓存变量 | 缓存属性 | 容量 | 数据结构 | 缓存用处 |
---|---|---|---|---|---|
1 | mChangeScrap与mAttachedScrap | 可见缓存 | n | ArrayList | 用于布局进程中屏幕可见表项的收回和复用 |
2 | mCachedViews | 缓存列表 | 2 | ArrayList | 用于移除屏幕表项的收回和复用,不会清空数据 |
3 | mViewCacheExtension | 自界说 | n | ||
4 | RecyclerViewPool | 缓存池 | 5 | SparseArray | 用于移除表项的收回和服用,会将ViewHolder的数据重置 |
mAttachedScrap:寄存别离但未remove的ViewHolder,不会做数据的修正,且不走Adapter的绑定办法
mChangedScrap:寄存别离但未remove且产生了改动的ViewHolder,运用这儿的缓存,需求从头走Adapter绑定办法
mCacheViews:寄存别离且remove的ViewHolder,最大容量为2,不需求从头绑定
RecycleViewPool:本身是一个内部类,保存的ViewHolder不仅仅是removed掉的视图,而且是康复了出厂设置的视图,任何绑定过的痕迹都没有了,需求走Adapter绑定办法
- 依据不同的itemType来对该itemType下的ViewHolder进行缓存,每个不同的itemType默许缓存5个(该值可以经过
setMaxRecycledViews
办法修正) - 依据不同的itemType运用SparseArray缓存对应的Scrap,每个Scrap中又用ArrarList缓存对应的ViewHolder(
默许巨细 = 5,可修正
)
mViewCacheExtension:自界说缓存,官方未完结,本身为空
收回流程:
在滑动的进程中,当一个ViewHolder从可见变成不行见时,走到scrapView
办法,判别是否是修正过的(此刻的ViewHolder仅仅仅仅被别离,没有被review)
,假如未被修正,增加到mAttachedScrap中,假如被修正了则增加到mChangedScrap中;持续持续滑动进程中,上述列表中被review的ViewHolder会走到recycleViewHolderInternal
办法中,(此刻现已被review)
假如契合条件mCacheViews容量为2
则存入mCacheViews中,假如满了不满意,则会将最久的(index = 0)的数据寄存到RecyclerViewPool中,将新的ViewHolder放入index = 1的方位(原index = 1顺位到index = 0方位),假如此刻直接在mCacheViews中取视图是可以直接展示,不需求从头绑定的,一旦进入到了RecyclerViewPool中被初始化了,就需求从头绑定了
复用流程:
首要会经过getChangedScrapViewForPosition
办法从mChangedScrap中寻觅,假如没找到经过getScrapOrHiddenOrCachedHolderForPosition
办法从mAttachedScrap中查找,假如没找到会从ChildHelper类中的mHiddenViews中查找(LayoutManager中会动态增加一组消失的视图),假如未找到则从mCahceViews中寻觅,假如还未找到则从RecycleViewPool中寻觅。终究还未找到的话则调用Adapter.CreateViewHolder创立
Glide源码
加载恳求发送到了哪里?
恳求发送进去的时分,存在2个行列WeakHashMap
,一个正在履行行列,一个等候行列,新接纳的恳求会直接存入运转行列,假如恳求request处于暂定状况的话,会存入等候列表,反之直接调用begin进行运转
恳求是怎样被处理的?
调用bejin()办法后,会优先从内存中读取对应的缓存,假如不存在则去磁盘中进行查找,都没有的话,会履行SingleRequest,进行网络获取(网络工具运用是,HttpUrlConnection,会得到一个InptStream流
),依据获取的到资源进行新的缓存与展示
怎样保护的?
经过生命周期进行恳求的处理与收回(内部会运用Fragment进行生命周期的办理,假如不存在则会从头去创立一个带固定tag的Fragment,而且该Fragment是无UI的
),内部request会在运用的时分优先从内存池中获取,假如不存在则从头创立,在收回时,会将其初始化后放入内存池中供下次运用
Glide与Picasso的差异,Glide的优势
- 多种图片格式的缓存,适用于更多的内容表现形式(如Gif、WebP、缩略图、Video)
- Glide加载图片的格式占用内存小(
Glide:RGB_565,Picasso:ARGB_8888
) - 生命周期集成(依据Activity或许Fragment的生命周期办理图片加载恳求)
- 高效处理Bitmap(bitmap的复用和主动收回,削减体系收回压力)
- 高效的缓存战略(
活动内存、内存、磁盘
),灵敏(Picasso只会缓存原始尺度的图片,Glide缓存的是多种标准),加载速度快且内存开支小
Glide内存办理表现在哪?
LRU算法:依据时刻戳进行排序,最新的在最上层,有新的会去收回掉最旧的那个,新的放在最上面
-
资源缓存在活动内存
ActiveResourceCache
中(缓存当时正在运用的资源(留意是弱引证)
),可以直接拿来运用 -
资源缓存在内存
LruResourceCache
中(缓存最近运用过可是当时未运用的资源,依据LRU算法
) -
缓存在磁盘中
BitmapPool
中(缓存一切被开释的图片,内存复用,依据LRU算法
)留意:
- LruResourceCache和ActiveResourceCache规划是为了尽或许的资源复用
- BitmapPool的规划意图是为了尽或许的内存复用
资源加载流程?(或
三级缓存是怎样完结的?)
参阅:跟着源码学规划:Glide框架及源码解析
- 当咱们需求显现某个资源时,Glide会先去查找
ActiveResourceCache
,假如找不到资源则查找LruResourceCache
,假如在LruResourceCache
也找不到适宜的资源,则会依据加载战略从硬盘或许网络加载资源 - 获取数据后Glide会从
BitmapPool
中找寻适宜的可供内存复用的抛弃recycled bitmap(找不到则会从头创立bitmap目标),然后改写bitmap的数据。 - bitmap被转换封装为Resource缓存入
ActiveResourceCache
和Request
目标中,Request依据target获取resource中引证的bitmap并展示。 - 当target的资源需求开释时,resource会依据缓存战略被缓存到
LruResourceCache
,一起ActiveResourceCache
中的弱引证会被删去。假如,该资源不能缓存到LruResourceCache
,则资源将被收回到磁盘缓存
中。 - 当需求收回内存时(比方体系内存不足或许生命周期完毕),
LruResourceCache
将依据LRU算法收回一些resource到磁盘缓存
。 -
磁盘缓存
会依据LRU算法和缓存池的尺度来开释一些老旧资源。当体系GC时,则会收回可收回的资源开释内存
留意:假如是依据网络进行的恳求,则在网络恳求成功之后,取得对应的图片流,然后会显现在对应的视图上,一起此刻也会在磁盘中进行相应的缓存,由于现已显现在对应视图上了,ActiveResourceCache中也会存储一份
Glide为什么不忧虑内存走漏?
在用户需求时进行缓存,不需求时进行合理的收回
-
当体系内存不足时,LruResourceCache会依据LRU算法移除一些内存中的缓存资源到BitmapPool
-
到BitmapPool会依据LRU算法移除一些资源为新缓存的供给方位
-
当运用再次需求资源时,会优先复用到BitmapPool中的bitmap目标(复用其内存),只需改写bitmap的像素数据 1)这样能有效地下降内存颤动; 2)由于许多状况下可以复用抛弃bitmap的内存,因而避免了内存分配等形成的功能损耗,体系比较流畅 3)下降了体系GC的频率 4)LruResourceCache和BitmapPool中都是当时不在运用的资源,做整体的资源收回功能会更好。
页面间的生命周期是怎样完结的?
Glide会传递一个上下文context
,Glide会在内部经过context创立一个无UI
的Fragment,经过监听Fragment的生命周期来监听外部Activity的生命周期Activtiy与Fragment生命周期相互绑定
留意:Glide内部经过context创立时,会创立2个行列(分为v4,非v4),经过Tag去RequestManagerFragment目标去查找,假如没有找到RequestManagerRetriever(Glide办理Fragment的中间件,内部含有2个相同的行列,一个办理非v4-Fragment,一个办理v4-Fragment)
中的
requestManagerFragment行列中查找(HashMap),假如都没有找到则创立一个新的无视图的Fragment,并增加到行列中去,为下次供给运用
Glide怎样做大图加载
关于图片加载还有种状况,便是单个图片十分巨大,而且还不答应紧缩。比方显现:世界地图、清明上河图、微博长图等
首要不紧缩,依照原图尺度加载,那么屏幕肯定是不够大的,而且考虑到内存的状况,不或许一次性整图加载到内存中
所以这种状况的优化思路一般是部分加载,经过BitmapRegionDecoder
来完结
这种状况下通常Glide
只担任将图片下载下来,图片的加载由咱们自界说的ImageView
来完结
-
BitmapRegionDecoder介绍
BitmapRegionDecoder
首要用于显现图片的某一块矩形区域,假如你需求显现某个图片的指定区域,那么这个类十分适宜。 不过这种办法虽然也能加载大图,但做的还不够,滑动时内存颤动,卡顿现象比较显着,不能用于线上 -
可用于线上的大图加载计划 :介绍一个开源库:subsampling-scale-image-view
SubsamplingScaleImageView
将大图切片,再判别是否可见,假如可见则参加内存中,不然收回,削减了内存占用与颤动 一起依据不同的缩放比例选择适宜的采样率,进一步削减内存占用 一起在子线程进行decodeRegion操作,解码成功后回调至主线程,削减UI卡顿
Glide异步加载线程池有多少个?
缓存一般有三级,内存缓存、硬盘、网络。
由于网络会堵塞,所以读内存和硬盘可以放在一个线程池,网络需求别的一个线程池,网络也可以采用Okhttp内置的线程池。
读硬盘和读网络需求放在不同的线程池中处理,所以用两个线程池比较适宜。
Glide 必然也需求多个线程池,看下源码是不是这样
public final class GlideBuilder {
//...
private GlideExecutor sourceExecutor; //加载源文件的线程池,包括网络加载
private GlideExecutor diskCacheExecutor; //加载硬盘缓存的线程池
//...
private GlideExecutor animationExecutor; //动画线程池
复制代码
Glide运用了三个线程池,不考虑动画的话便是两个。
参阅:蓝师傅针对Glide问答点
参阅:关于Glide常见面试
LiveData原理
具有生命周期感知能力,支撑黏性事情,采用了观察者形式,某种程度上也可以用作事情总线。
LiveData 源码中首要用到的类:
-
Observer
:观察者接口 -
LiveData
:发送现已增加观察的逻辑都在其间 -
ObserverWrapper
:笼统的观察者包装类,供给了mLastVersion 和判别以及更新观察者是否活泼的办法 -
LifecycleBoundleObserver
:承继 ObserverWrapper,可以感知生命周期,会在页面活泼的时分更新观察者 -
AlwaysActiveObserver
:承继 ObserverWrapper ,无法感知生命周期,可以在恣意时刻接纳到告诉。
LiveData 怎样感知生命周期感知?需求撤销注册吗?
调用 observe 办法时,会调用 owner.getLifecycle().addObserver 来抵达感知生命周期的意图,不需求进行额外的注册,observe内部帮助处理了remove跟add相关的场景
setValue 和 postValue 有什么差异
setValue 只能在主线程运用,而 postValue 不约束线程。本质上postValue终究运用的仍是postValue,只不过在内部运用handler将数据分发到了主线程之后来进行调用
设置相同的值,订阅的观察者们会收到相同的值吗
会,LiveData内部只要判别调用时Version的判别,并没有判别值之间的差异性
粘性事情原理,怎样避免数据倒灌
-
作业机制:每次改动LiveData数据都会对数据版别号加1,并触发版别号小于数据版别号的观察者监听,触发后观察者的版别号与数据版别号共同
-
粘性事情:更新数据后,观察者再订阅,新注册的观察者版别号为-1小于数据版别号,所以注册时会触发一次数据监听。
-
数据粘连:LiveData的激活状况标识,会在对应的LifecyOwner履行onStart后设置为true,履行onDestroy后设置为false,在未激活状况下不管产生多少次改动,只要终究一次数据会发送给观察者
-
数据倒灌:由于LiveData的激活状况标识先变为false,再变为true,导致触发小于数据版别号的一切观察者的监听。
常见场景为:运用ViewModel持有LivaData,并在生命周期内创立监听目标,则在Activity由于屏幕翻转等装备改动引发onDestroy时,ViewModel不会履行clear,因而保留了内部的LiveData,而在生命周期内从头创立监听目标的版别号为-1,所以在onStart之后会触发观察者监听
常见场景为:运用ViewModel持有LivaData,并在生命周期内创立监听目标,则在Activity由于屏幕翻转等装备改动引发onDestroy时,ViewModel不会履行clear,因而保留了内部的LiveData,而在生命周期内从头创立监听目标的版别号为-1,所以在onStart之后会触发观察者监听
-
解决计划(避免数据倒灌):
- 修正version,经过反射办法操控version的值,让version大于lastVersion
- 复写LiveData,操控observe分发机制
observeForever怎样用
LiveData 作为事情总线机制或许装备之类
AlwaysActiveObserver 不依赖生命周期了,所以不会像 LifecycleBoundObserver 在生命周期变为 DESTROYED 时调用 LiveData#removeObserver 从 LiveData#mObservers Map 中移除本身,所以咱们在运用 LiveData#observeForever 时应在不需求的时分调用 LiveData#removeObserver ,不然或许会产生内存走漏呢
总结一下有几种状况 LiveData 会分发值
- 调用 setValue 和 postValue 而且 LifecycleOwner 处于活泼状况时
- LiveData 有值,而且处于活泼状况时,调用 LiveData#observe 订阅观察者
- LiveData 有新值,也便是 ObserverWrapper 的 mLastVersion 小于 LiveData 的 mVersion,LifecycleOwner 从不活泼状况转为活泼状况时
RxJava
参阅:RxJava2.0有用操作符总结及原理简析
参阅:RxJava常见操作符
Rx首要操作符
一切的操作都是操作符,除了 Subscribe
每个操作符都会生成新的 Observable
,每个操作符的左边便是上游(upstream),右边便是下流(downstream)
-
Map:Map 对上游的数据项进行简略的改换(映射),回来新的
ObservableMap
。完结便是把下流的Observer
包装成MapObserver
订阅给上游 -
FlatMap:把上游的数据项 Map 化成
Observable
,然后把这些Observable
flatten 拍平,这个拍平指的便是不确保次序的 merge简略点说便是把上游的数据项都改换成
Observable
,把这些Observable
都订阅给下流,可是关于只发射一个值的Observable
做一下特别处理,关于并发操作也要进行处理,所以要复杂一点假如想要确保按次序 merge 可以运用 concatMap 操作符
-
SubscribeOn 和 ObserveOn:RxJava 不会直接运用线程或线程池,而是运用
Scheduler
调度器和SubscribeOn
,ObserveOn
这两个操作符来完结异步操作-
常用的线程调度器有:
-
合适履行核算密集型使命的
Schedulers.computation()
, -
合适履行 I/O 密集型使命的
Schedulers.io()
, -
合适串行使命的
Schedulers.single()
假如不指定调度器的话,那么在哪个线程订阅
subscribe()
的,就在哪个线程上履行操作符的逻辑,就在哪个线程告诉观察者
-
-
-
Merge:
merge()
可以合并多个Observable
,其实便是把多个Observable
变成调集后调用flatMap()
。所以 merge 是不确保次序的假如想要按次序合并多个
Observable
,可以运用concat()
假如不想由于其间一个Observable
的过错导致流中止,可以运用mergeDelayError()
比及一切数据项都发射完再发射和处理过错 一个Observable
实例可以运用mergeWith()
来 merge 另一个实例 -
Concat:
concat()
可以将多个Observable
按次序拼接起来,一个Observable
发射完了再发射下一个Observable
的一个
Observable
实例可以运用concatWith()
来 concat 其他的实例 -
Zip:
zip()
可以按次序紧缩一切Observable
的第 i 个数据项。最少的那个Observable
发射完了就算完结了,其他Observable
或许会被立刻 dispose 而且接纳不到complete
的回调doOnComplete()
一个
Observable
实例可以运用zipWith()
来 zip 其他的实例 -
Timer、Interval、Delay:
timer()
固定的时刻推迟后发射,Interval
固定的时刻距离发射,Delay
发射每个数据前都推迟固定时刻后再发射 -
SkipUntil 和 SkipWhile:
-
skipUntil()
让源Observable
放弃发射数据,直到给定的Observable
发射了数据它才可以正常发射数据 -
skipWhile()
让源Observable
放弃发射数据,直到给定条件变成false
-
-
TakeUntil 和 TakeWhile:
-
takeUntil()
让源Observable
在给定的Observable
发射了数据后立刻 complete -
takeWhile()
会镜像发射源Observable
的数据,直到给定条件变成false
时立刻 complete
-
-
Catch:
- Catch 操作符可以阻拦
onError
告诉,而且可以把它替换成数据项或数据项序列-
onErrorReturn()
在Observable
遇到 error 时不调用Observer
的onError()
办法,而是发射一个给定的数据项,然后 complete -
onErrorResumeNext()
在Observable
遇到 error 时不调用Observer
的onError()
办法,而是将操控权交给给定的Observable
-
onErrorComplete()
在Observable
遇到 error 时不调用Observer
的onError()
办法,过错告诉被丢弃并直接 complete
-
- Catch 操作符可以阻拦
-
Retry:Retry 操作符可以在
onError
时从头订阅(也便是重试)
EventBus原理
观察者形式又可称为发布 – 订阅形式,它界说了目标间的一种一对多的依赖联系,每当这个目标的状况改动时,其它的目标都会接纳到告诉并被主动更新。
-
观察者形式有以下角色:
-
笼统被观察者:将一切已注册的观察者目标保存在一个调集中。
-
详细被观察者:当内部状况产生改动时,将会告诉一切已注册的观察者。
-
-
笼统观察者:界说了一个更新接口,当被观察者状况改动时更新自己。
- 详细观察者:完结笼统观察者的更新接口。
为什么要运用事情总线机制来代替广播呢
- 广播:耗时、简单被捕获,不安全
- 事情总线:更节约资源、更高效、能将信息传递给原生以外的各种目标
关于事情总线EventBus而言,它的优缺陷?
- 优点:开支小、代码更优雅、简洁、解耦发送者和承受者,可动态设置事情处理线程和优先级
- 缺陷:每个事情必须自界说一个事情类,增加保护本钱
在得知了EventBus的原理之后,咱们留意到,每次咱们在register之后,都必须进行一次unregister,这是为什么呢?
由于register是强引证,它会让目标无法得到内存收回,导致内存走漏。所以必须在unregister办法中开释目标所占的内存。
EventBus2.x的版别,那么它又与EventBus3.x的版别有哪些差异呢?
-
EventBus2.x
运用的是运转时注解,它采用了反射的办法对整个注册的类的一切办法进行扫描来完结注册,因而会对功能有必定影响。 -
EventBus3.x
运用的是编译时注解,Java文件会编译成.class文件,再对class文件进行打包等一系列处理。在编译成.class文件时,EventBus会运用EventBusAnnotationProcessor注解处理器读取@Subscribe()注解并解析、处理其间的信息,然后生成Java类来保存一切订阅者的订阅信息。这样就创立出了对文件或类的索引联系,并将其编入到apk中。 - 从
EventBus3.0
开始运用了目标池缓存削减了创立目标的开支。
-
除了EventBus,其完结在比较盛行的事情总线还有RxBus,它与EventBus相比又怎样呢?
RxBus是依据RxJava开源基础上的
-
RxJava的Observable有onError、onComplete等状况回调
-
Rxjava运用组合而非嵌套的办法,避免了回调阴间
-
Rxjava的线程调度规划的愈加优秀,更简略易用
-
Rxjava可运用多种操作符来进行链式调用来完结复杂的逻辑
-
Rxjava的信息功率高于EventBus2.x,低于EventBus3.x
假如项目中运用了RxJava,则运用RxBus,不然运用EventBus3.x
-
EventBus.getDefault()剖析
内部经过两层判别的办法创立一个EventBus目标
在EventBus的结构办法中,创立一个保存注册目标和订阅信息目标(包括:注解订阅办法集,event类型等
)的行列(HashMap),一起也创立了一个行列用于储存被注册过的目标,还会创立一个保存粘连性注册目标的行列
EventBus.getDefault().register(this)剖析
经过注册的目标,讲该目标超类中以及本身存在的一切订阅过的办法进行存储,而且还会处理相关粘连性时刻的发送
- 先从FindState池中取目标,假如不存在就创立一个而且放入其间,然后依据FindState目标获取对应订阅的音讯调集
- 关于订阅办法的查找,优先运用索引查找,当找不到索引类时,持续运用反射的办法查找
EventBus.getDefault().post(Object())
ThreadMode | 履行线程 | |
---|---|---|
POSTING | 在发送事情的线程中履行 | 直接调用音讯接纳方 |
MAIN | 在主线程中履行 | 假如事情便是在主线程发送的,则直接调用音讯接纳方,不然经过 mainThreadPoster 进行处理 |
MAIN_ORDERED | 在主线程中按次序履行 | 经过 mainThreadPoster 进行处理,以此确保音讯处理的有序性 |
BACKGROUND | 在后台线程中按次序履行 | 假如事情是在主线程发送的,则提交给 backgroundPoster 处理,不然直接调用音讯接纳方 |
ASYNC | 提交给闲暇的后台线程履行 | 将音讯提交到 asyncPoster 进行处理 |
拿到在register办法中的订阅信息行列,经过反射的办法method.invoke
进行数据的分发作业,假如存在不同的线程,会经过handler进行相关线程的切换
EventBus.getDefault().unregister(this)
- 移除该订阅目标
- 移除该订阅目标下对应的订阅办法和时刻
EventBus.getDefault.postSticky(Object())
将注册粘连性事情的注册目标增加到对应的粘连性时刻行列中,终究经过post进行事情发送
发问
1.有这么一种状况:A类完结了B接口,Test1Activity注册了EventBus,而且其间test办法被Subscribe注释
问题一:假如办法的参数为A类,Test2Activity中进行了post操作,事情类型为B,Test1Activity的test办法可以接纳到事情吗
接纳不到事情
问题二:假如办法的参数为B类,Test2Activity中进行了post操作,事情类型为A,Test1Activity的test办法可以接纳到事情吗
可以接纳到事情
发送方的数据类型需求是接纳方的子集或本身,不然无法接纳到
原理:在
post()
事情时,会调用lookupAllEventTypes()
办法,去查找注册过的当时事情类型和一切父类类型、接口类型的观察者并进行触发
2.一个父类Parent,其间test办法被Subscribe注解,Son承继了Parent而且重写了test办法,请问假如Son被注册,假如post事情后,Son可以接纳到事情,那么Parent类可以接纳到事情吗?
Son可以收到,Parent不可以收到,子类中重写了父类的注解办法,就不会再去查找父类中的注解办法,就只触发子类的重写办法不会去触发父类的办法
子类覆盖了父类的办法,父类的注册事情指向了子类,就会出现在子类中重写后,只要子类会进行触发;反之子类不对父类办法进行重写时,会触发父类的注册事情
3.描绘一下EventBus是怎样进行线程切换的?
承继Handler完结了HandlerPoster,本质上经过Handler进行相应的线程切换
LeakCanary原理
参阅:LeakCanary完结原理
- 监测Activity / Fragment的生命周期的 onDestroy() 的调用
- 首要创立WeakReference包装目标(需求传入引证行列),然后将WeakReference缓存至调集(
ReferenceQueue
)中 - 若期间产生GC,WeakReference包装的目标不再被引证即会被收回,一起WeakReference本身参加引证行列。此刻经过获取引证行列中的WeakReference,去移除WeakReference调集中的对应元素。若WeakReference调集还残留元素,则阐明对应WeakReference没有参加引证行列,也意味着WeakReference没有被收回
- 切换子线程再终究断定。主动触发一次GC,等候100ms后,再次查看调集。若仍发现目标被引证未被开释,则断定这些缓存调集中的目标存在内存走漏,将进行dump heap操作
- 产生内存走漏之后,dump heap生成hprof文件并解析文件,生成走漏引证链 (
依赖另一个专门剖析hprof的库来解析文件和生成剖析成果。在1.X是依赖haha库,2.X改成依赖shark库
)
setContentView()原理
Activity 与 AppCompatActivity的差异
AppCompatActivity终究是承继至Activity,AppcompaActivity带ActionBar标题栏,Activity 则不带,AppCompatActivity为兼容办法
AppCompatActivity -> FragmentActivity -> ComponentActivity -> Activity
- Activity 和 AppCompatActivity 加载布局前都会创立一个 DecorView,并将体系布局加载到 DecorView 中,经过 DecorView 找到 id 为 android.id.content 的FrameLayout,终究经过 LayoutInflater 加载咱们的 xml 布局。
- Activity 没有设置Factory ,AppCompatActivity 设置了 Factory。
- Activity 不会阻拦 View,而 AppCompatActivity 会阻拦 View,并将部分 View 转换成对应的 AppCompatView。
ThreadLocal、AsyncTask原理
ThreadLocal
运用一个map保存一切线程的部分数据,map的key是线程的id(此处的id便是对应Thread中包括的ThreadLocalMap目标)
,value便是所要存储的数据
每一个Thread目标内部会存在一个ThreadLocalMap引证(默许是null),然后ThreadLocal经过
create()
的办法将当时的ThreadLoaclMap存储到对应的Thread中
ThreadLoaclMap在初始创立时,以ThreadLoacl为key,value为空创立一个相关目标
AsyncTask
参阅:了解AsycnTask就这么简略
AsyncTask是线程池和Handler的封装
关于线程池,AsyncTask内置有两个线程池:
-
THREAD_POOL_EXECUTOR(ThreadPoolExecutor)
中心线程数
CORE_POOL_SIZE = 1
,最大线程数MAXIMUM_POOL_SIZE = 20
-
SERIAL_EXECUTOR(SerialExecutor)
-
依据第一个线程池THREAD_POOL_EXECUTOR的二次包装,而且加上了同步锁,确保每次咱们new AsyncTask并调用execute()时履行的使命是串行的,以及确保操作一些同享变量时线程安全
由于AsyncTask调用execute办法,默许运用SerialExecutor这个线程池,SerialExecutor经过同步关键词synchronized来确保线程履行使命行列里头的使命是串行的
-
-
关于AsyncTask的运用,还有需求留意的便是简单导致内存走漏的状况:
-
非静态内部类引证了外部变量导致的内存走漏
假如用到了内部类,给内部类增加静态修饰符即可
-
未履行的AsyncTask导致的内存走漏 需求在界面的生命周期完毕的时分,设置使命撤销符号
-
-
内部线程的切换机制
- 经过外部传入Handler、Looper或许本身依据主线程Looper创立一个Handler,经过Handler进行线程间的切换
线程池的基本原理
线程池详细的参数阐明 – okHttp线程池
-
在开发进程中,合理地运用线程池可以带来3个好处。
- 下降资源耗费。
- 进步呼应速度
- 进步线程的可办理性。
-
假如当时运转的线程少于corePoolSize,则创立新线程来履行使命(留意,履行这一步骤需求获取大局锁)。
-
假如运转的线程等于或多于corePoolSize,则将使命参加BlockingQueue。
-
假如无法将使命参加BlockingQueue(行列已满),则创立新的线程来处理使命。
-
假如创立新线程将使当时运转的线程超出maximumPoolSize,使命将被拒绝,并调用 RejectedExecutionHandler.rejectedExecution()办法。
第一篇:Android面试知识点总结(一)—— 基础知识篇