[TOC]
框架篇
EventBus
简述EventBus的理解
EventBus作为通信事件传递的总线,你无需控制值的传递,也无需通过广播等低效实现,通过EventBus在你需要发送的地方post信息,在你需要接收的地方接收信息处理即可,(前提是register过)
EventBus的五种线程模式
- POSTING:默认,发布和订阅在同一个线程,同发布者一个线程(主--->主 , 子--->子),最小的开销,因为不用切换线程,避免了线程的完全切换,使用此模式的事件处理程序必须快速返回,以避免阻塞可能是主线程的发布线程。
- MAIN:事件处理函数的线程在主线程(UI)线程。不能进行
耗时操作,订阅者需快速返回以免阻塞主线程 - MAIN_ORDERED:事件处理函数的线程在主线程(UI)线程。不会阻塞线程
- BACKGROUND:处理函数在后台线程,不能进行UI操作。发布在主线程,订阅会开启一个新的后台线程。发布在后台线程,事件处理函数也在该后台线程
- ASYNC:无论事件发布的线程是哪一个,都会重新开辟一个新的子线程运行,不能进行UI操作
EventBus的事件类型
接收事件必须是public修饰符修饰,不能用static关键字修饰,不能是抽象的(abstract)
- 普通事件:先订阅在发布,发布到订阅者后进行处理
- 粘性事件:支持先发布在订阅,当订阅者订阅后会自动发送到订阅者进行处理,发送粘性事件EventBus.postSticky(),接收粘性事件sticky = true
为什么必须是public?
因为源码定义
优先级
优先级高的订阅者优先接收到任务
简述源码分析事件
- register:通过注解初始化订阅方法后,在register后,在缓存中获取所有该订阅者的方法,循环遍历订阅,新建newSubscription方法,根据priority优先级将newSubscription方法放入subscriptions中,判断如果
是粘性事件,则执行其对应的订阅方法。 - unregister:从typesBySubscriber获取订阅事件类型,根据订阅事件类型从subscriptionsByEventType获取订阅者信息,将subscription的active置为false,并移除该subscription
索引如何理解?
EventBus 3.0以后,采用Subscribe注解配置事件订阅方法,采用反射的方式来查找订阅事件的方法,我们都知道反射对性能是有影响的,所以提出了索引的概念。
在项目编译时,通过配置,生成一个辅助类用来存储订阅信息,原理是HashMap,存储注册类的class类型和事件订阅方法的信息,提高速度3-4倍
MAIN和MAIN_ORDERED的区别?
-
在
MAIN模式下,如果事件发布者post事件也是在主线程的话,会阻塞post事件所在的线程,意思是连续post多个事件,如果接收事件方法执行完,才能post下一个事件**post(1) ——> onReceiveMsg(1) ——>post(2)——>onReceiveMsg(2)——>post(3)——>onReceiveMsg(3)** -
如果事件发布者post事件不在主线程,连续post多个事件,同时在主线程是接收事件是耗时操作的话,执行的流程是非阻塞的
**post(1)——>post(2)——>psot(3)——>onReceiveMsg(3)** 或 **post(1)——>post(2)——>psot(3)——>onReceiveMsg(2)——>onReceiveMsg(3)** -
MAIN_ORDERED模式下,无论什么场景都是非阻塞的
EventBus可否跨进程问题?
不能,单进程间通信
HermesEventBus------>饿了吗开发框架,可应用于单进程和多进程。
使用IPC机制,首先选择一个主进程,其他则为子进程,每一个event会经过4步:
- 使用Hermes库将event传递给主进程。
- 主进程使用EventBus在主进程内部发送event。
- 主进程使用Hermes库将event传递给所有的子进程。
- 每个子进程使用EventBus在子进程内部发送event。
BackgroundThread和Async区别
BackgroundThread:发布在主线程,新开辟子线程中执行。发布在子线程,则在子线程中执行,这个子线程是阻塞式的,按顺序交付所有事件,所以也不适合做耗时任务,因为多个事件共用这一个后台线程
Async:无论发布在哪一个线程,都会在重新开辟一个子线程执行
EventBus的优缺点:
优点:
EventBus是greenrobot公司出的另一款开源框架,这个框架是针对Android优化的发布/订阅事件总线,使用EventBus可以极大的减少我们程序的耦合度。
调度灵活。不依赖于 Context,使用时无需像广播一样关注 Context 的注入与传递。
使用简单。
快速且轻量。
完全解耦了请求链之间的关系,避免了请求者被长持有,
比广播更轻量
可以定义在调用线程、主线程、后台线程、异步。
粘性事件
优先级概念
为了避免频繁的向主线程 sendMessage()(Handler机制),EventBus 的做法是在一个消息里尽可能多的处理更多的消息事件,所以使用了 while 循环,持续从消息队列 queue 中获取消息。
同时为了避免长期占有主线程,间隔 10ms (maxMillisInsideHandleMessage = 10ms)会重新发送 sendMessage(),用于让出主线程的执行权,避免造成 UI 卡顿和 ANR。
缺点:
各种Event的定义工作量大。每次传的内容不一样,就需要重新定义一个JavaBean
单向传播
需要显性注册
EventBus如何做到线程切换
private void postToSubscription(Subscription subscription, Object event, boolean isMainThread) {
switch (subscription.subscriberMethod.threadMode) {
case POSTING:
invokeSubscriber(subscription, event);
break;
case MAIN:
if (isMainThread) {
invokeSubscriber(subscription, event);
} else {
mainThreadPoster.enqueue(subscription, event);
}
break;
case MAIN_ORDERED:
if (mainThreadPoster != null) {
mainThreadPoster.enqueue(subscription, event);
} else {
// temporary: technically not correct as poster not decoupled from subscriber
invokeSubscriber(subscription, event);
}
break;
case BACKGROUND:
if (isMainThread) {
backgroundPoster.enqueue(subscription, event);
} else {
invokeSubscriber(subscription, event);
}
break;
case ASYNC:
asyncPoster.enqueue(subscription, event);
break;
default:
throw new IllegalStateException("Unknown thread mode: " + subscription.subscriberMethod.threadMode);
}
}
主要分为主线程执行和子线程执行,当为主线程时:
public class HandlerPoster extends Handler implements Poster {
private final PendingPostQueue queue;
private final int maxMillisInsideHandleMessage;
private final EventBus eventBus;
private boolean handlerActive;
protected HandlerPoster(EventBus eventBus, Looper looper, int maxMillisInsideHandleMessage) {
super(looper);
this.eventBus = eventBus;
this.maxMillisInsideHandleMessage = maxMillisInsideHandleMessage;
queue = new PendingPostQueue();
}
public void enqueue(Subscription subscription, Object event) {
PendingPost pendingPost = PendingPost.obtainPendingPost(subscription, event);
synchronized (this) {
queue.enqueue(pendingPost);
if (!handlerActive) {
handlerActive = true;
if (!sendMessage(obtainMessage())) {
throw new EventBusException("Could not send handler message");
}
}
}
}
@Override
public void handleMessage(Message msg) {
boolean rescheduled = false;
try {
long started = SystemClock.uptimeMillis();
//循环处理消息事件,避免重复sendMessage()
while (true) {
PendingPost pendingPost = queue.poll();
if (pendingPost == null) {
synchronized (this) {
// Check again, this time in synchronized
pendingPost = queue.poll();
if (pendingPost == null) {
handlerActive = false;
return;
}
}
}
eventBus.invokeSubscriber(pendingPost);
long timeInMethod = SystemClock.uptimeMillis() - started;
//避免长期占用主线程,间隔10ms重新sendMassage()
if (timeInMethod >= maxMillisInsideHandleMessage) {
if (!sendMessage(obtainMessage())) {
throw new EventBusException("Could not send handler message");
}
rescheduled = true;
return;
}
}
} finally {
handlerActive = rescheduled;
}
}
}
使用HandlerPoster将任务通过sendMessage方法发送到主线程执行,通过消息队列存储该handler的任务,通过10ms发送一次任务,防止主线程卡顿,MAIN情况下,如果在主线程,直接执行。MAIN_ORDER的情况下,全部交给handler异步执行,所以区别于MAIN不是同步的
final class BackgroundPoster implements Runnable, Poster {
private final PendingPostQueue queue;
private final EventBus eventBus;
private volatile boolean executorRunning;
BackgroundPoster(EventBus eventBus) {
this.eventBus = eventBus;
queue = new PendingPostQueue();
}
public void enqueue(Subscription subscription, Object event) {
PendingPost pendingPost = PendingPost.obtainPendingPost(subscription, event);
synchronized (this) {
queue.enqueue(pendingPost);
if (!executorRunning) {
executorRunning = true;
eventBus.getExecutorService().execute(this);
}
}
}
@Override
public void run() {
try {
try {
while (true) {
PendingPost pendingPost = queue.poll(1000);
if (pendingPost == null) {
synchronized (this) {
// Check again, this time in synchronized
pendingPost = queue.poll();
if (pendingPost == null) {
executorRunning = false;
return;
}
}
}
eventBus.invokeSubscriber(pendingPost);
}
} catch (InterruptedException e) {
eventBus.getLogger().log(Level.WARNING, Thread.currentThread().getName() + " was interruppted", e);
}
} finally {
executorRunning = false;
}
}
}
class AsyncPoster implements Runnable, Poster {
private final PendingPostQueue queue;
private final EventBus eventBus;
AsyncPoster(EventBus eventBus) {
this.eventBus = eventBus;
queue = new PendingPostQueue();
}
public void enqueue(Subscription subscription, Object event) {
PendingPost pendingPost = PendingPost.obtainPendingPost(subscription, event);
queue.enqueue(pendingPost);
eventBus.getExecutorService().execute(this);
}
@Override
public void run() {
PendingPost pendingPost = queue.poll();
if(pendingPost == null) {
throw new IllegalStateException("No pending post available");
}
eventBus.invokeSubscriber(pendingPost);
}
}
子线程执行是通过线程池进行管理,内部也存在一个消息队列,按顺序执行任务,对于BACKGROUND情况下,同时只会使用线程池中的一个线程,而Async直接放入线程池,让线程池去规划线程。当线程池中等待任务过多时,会触发oom(线程池是newCachedThreadPool(),则线程为非核心线程MAX)
粘性事件的原理
普通事件是先注册后发布,而粘性事件可以先发布后注册,实现方式上是这样的:
发送时会将粘性事件的事件类型和对应事件保存起来,在执行post方法,在注册后,如果是粘性事件,会多走一步类似于post的方法,触发进行分发
如何判断当前线程是否为主线程?
在发布事件的地方判断发送线程和主线程的Looper对象是否相等
return Looper.getMainLooper() == Looper.myLooper();
如何优化EventBus
- 尽量使用索引功能,避免不必要的反射,提升性能
- 增加EventBus的进程间通信
为什么使用ConcurrentHashMap保存数据
因为EventBus是单进程、多线程间通信,可能涉及到线程安全问题,使用ConcurrentHashMap可以有效解决线程安全和效率。
Okhttp3
简述OkHttp
是基于Socket的封装,主要有三个类:Response、Request、Call
同步使用client.excute(); 异步使用client.enqueue();
OkHttp的高效在于内部有一个Dispatcher,是okhttp维护的一个线程池,对最大连接数(并发),host最大访问量做了定义,维护了3个队列(同步正在执行,准备执行,异步正在执行)和一个线程池(0~max)
内部还维护了连接池,实现了复用机制,减少重复握手
提供缓存机制。
有几个拦截器,分别是干什么的?
client.intercepters():应用拦截器
RetryAndFollowUpIntercepter:重试和重定向机制,最大重试次数为20,构造StreamAllocation,创建缓存池,复用
BridgeIntercepter:将用户构造的请求转化为服务器识别的请求,将服务器返回的响应转化为用户识别的响应,添加keep-alive,供连接池复用
CacheIntercepter:缓存读取和更新
ConnectIntercepter:dns解析与服务器建立连接(握手结束),它利用 Okio 对 Socket 的读写操作进行封装,它对 java.io 和 java.nio 进行了封装,让我们更便捷高效的进行 IO 操作
client.networkIntercepter:网络拦截器
CallServerIntercepter:最后一个拦截器,负责向服务器发送请求和接收服务器的响应
采用责任链模式,将请求和发送分别处理,并且可以动态添加中间的处理方实现对请求的处理、短路等操作。
addNetworkInterceptor() (网络拦截器)和addInterceptor() (应用拦截器)
区别就是一个靠前一个靠后,其中经过的拦截器会导致不一样的结果
RetryAndFollowUpIntercepter中怎么进行重定向?
最大重试次数为20次
case HTTP_PERM_REDIRECT://307
case HTTP_TEMP_REDIRECT://308
// "If the 307 or 308 status code is received in response to a request other than GET
// or HEAD, the user agent MUST NOT automatically redirect the request"
if (!method.equals("GET") && !method.equals("HEAD")) {
return null;
}
// fall-through
case HTTP_MULT_CHOICE://300
case HTTP_MOVED_PERM://301
case HTTP_MOVED_TEMP://302
case HTTP_SEE_OTHER://303
// Does the client allow redirects?
if (!client.followRedirects()) return null;
String location = userResponse.header("Location");
if (location == null) return null;
HttpUrl url = userResponse.request().url().resolve(location);
// Don't follow redirects to unsupported protocols.
if (url == null) return null;
// If configured, don't follow redirects between SSL and non-SSL.
boolean sameScheme = url.scheme().equals(userResponse.request().url().scheme());
if (!sameScheme && !client.followSslRedirects()) return null;
// Most redirects don't include a request body.
Request.Builder requestBuilder = userResponse.request().newBuilder();
if (HttpMethod.permitsRequestBody(method)) {
final boolean maintainBody = HttpMethod.redirectsWithBody(method);
if (HttpMethod.redirectsToGet(method)) {
requestBuilder.method("GET", null);
} else {
RequestBody requestBody = maintainBody ? userResponse.request().body() : null;
requestBuilder.method(method, requestBody);
}
if (!maintainBody) {
requestBuilder.removeHeader("Transfer-Encoding");
requestBuilder.removeHeader("Content-Length");
requestBuilder.removeHeader("Content-Type");
}
}
// When redirecting across hosts, drop all authentication headers. This
// is potentially annoying to the application layer since they have no
// way to retain them.
if (!sameConnection(userResponse, url)) {
requestBuilder.removeHeader("Authorization");
}
return requestBuilder.url(url).build();
如果返回响应code是307和308,则只对get和head类型的请求进行重定向。
如果返回请求为PROPFIND,则重新发送的请求都为保持原状,如果不是propFind,则重新请求的都会被设置为get请求,且请求信息为空
RetryAndFollowUpInterceptor的intercept中先是创建了StreamAllocation对象,然后开启while(true)无限循环。接着在这个循环中先调用下层拦截器去网络请求,若请求期间发生异常,判断能否重试,能就continue进行下一轮循环,否则抛异常退出循环结束方法。如果下层拦截器请求完成返回response,通过response的状态码判断是否需要重定向,若需要重定向,修改request后进行下一轮循环,否则返回response结束方法。
什么是连接池?
OkHttp的底层是通过Java的Socket发送HTTP请求与接受响应的(这也好理解,HTTP就是基于TCP协议的),但是OkHttp实现了连接池的概念,
即对于同一主机的多个请求,其实可以公用一个Socket连接,而不是每次发送完HTTP请求就关闭底层的Socket,这样就实现了连接池的概念。
简述连接池的复用?
okhttp中所有的请求都被抽象为RealConnection,而ConnectionPool就是管理这些connection的,共享一个Address的链接可以复用
ConnectionPool,默认大小是5,每个链接存储5分钟,使用keep-alive,达到久连接,所以默认keep-alive是5分钟,也可以自定义
excutor:线程池,监测时间并释放连接的后台线程
connections:缓存池。是一个双端列表,这里用作栈
routeDatabase:记录连接失败router(路由)
使用put方法将连接放入缓存池,并清除闲置的线程,对缓存池进行排序(对比最大闲置时间),使用StreamAllocation复用请求
StreamAllocation的初始化在RetryAndFllowUpIntercepter。
在StreamAllocation调用newStream进行初始化,其中使用get方法在缓存池中查找相同的请求,如果找到就复用这条请求,没找到就新建连接并put到缓存池
连接池的工作就这么多,并不负责,主要就是管理双端队列Deque,可以用的连接就直接用,然后定期清理连接,同时通过对StreamAllocation的引用计数实现自动回收。
简述StreamAllocation
StreamAllocation是用来协调connections,stream和Call(请求)的。
HTTP通信执行网络请求Call需要在连接Connection上建立一个新的流Stream,我们将StreamAllocation称之流 的桥梁,它负责为一次请求寻找连接并建立流,从而完成远程通信。
其初始化在RetryAndFllowUpIntercepter,再次使用在CallServerInterceptor,复用机制使用该方法调用,减少一个三次握手的时间(不需要握手)
OKIO的优势
- 更加轻便,速度更快,使用更快
- 实现缓存结构,对cpu和内存进行优化,避免频繁gc(Segment链表实现)
- 功能强大,支持阻塞和非阻塞IO
- 支持多种类型,想比较于java.io和java.nio,不需要庞大的装饰类
Dispatcher的理解
内部维护了三个队列,分别为:
- runningAsyncCalls:正在请求的异步队列
- readyAsyncCalls:准备请求的异步队列\等待请求的异步队列
- runningSyncCalls:正在请求的同步队列
maxRequest:默认64。这是okhttp允许的最大请求数量。
maxRequestsPerHost :默认5。这是okhttp对同一主机允许的最大请求数量。
同步执行源码:
@Override public Response execute() throws IOException {
synchronized (this) {
//此处除去一些其他代码
//...
try {
//通知Dispatcher这个Call正在被执行,同时将此Call交给Dispatcher
//Dispatcher可以对此Call进行管理
client.dispatcher().executed(this);
//请求的过程,注意这行代码是阻塞的,直到返回result!
Response result = getResponseWithInterceptorChain();
if (result == null) throw new IOException("Canceled");
return result;
} finally {
//此时这个请求已经执行完毕了,通知Dispatcher,不要再维护这个Call了
client.dispatcher().finished(this);
}
}
try 中的return执行完成后,执行finally语句,所以不论请求成功或者失败,都会关闭这个请求
异步执行源码:
@Override public void enqueue(Callback responseCallback) {
synchronized (this) {
//判断是否已经执行过了
if (executed) throw new IllegalStateException("Already Executed");
executed = true;
}
//捕获调用栈的信息,用来分析连接泄露
captureCallStackTrace();
//封装一个AsyncCall交给Dispatcher调度
client.dispatcher().enqueue(new AsyncCall(responseCallback));
}
synchronized void enqueue(AsyncCall call) {
//判断正在执行的异步请求数没有达到阈值,并且每一个Host的请求数也没有达到阈值
if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {
//加入到正在执行队列,并立即执行
runningAsyncCalls.add(call);
executorService().execute(call);
} else {
//加入到等待队列
readyAsyncCalls.add(call);
}
}
简述OKhttp的缓存机制
okhttp有具备网络缓存机制,短时间内重复请求会复用缓存的数据,这样节省流量,应用也会很流畅。但是okhttp本身默认是不打开缓存机制的,需要配置后才能启动。
okhttp的缓存机制是以DiskLruCache(最近最少使用算法(Least recently used))为基础的,仅支持文件存储。
MD5(url)作为key,value是存储的服务端响应数据
默认不开启缓存机制
文件存储
DiskLruCache写入是依赖于okio的,内部实现类似于LinkedHashMap,键值对获取。
使用DiskLruCache,仅支持get请求的缓存
- 如果服务器支持缓存,即response携带Cache-control属性,则当你打开okhttp缓存即开始缓存,通过属性控制类型
- 如果服务器不支持缓存或者okhttp不想按照服务器缓存策略来存储,通过自定义拦截器重写response的头部即可
- 客户端不支持缓存,则可以不缓存,不理会服务器的cache-control属性
可以直接使用CacheControl类,包含
- CacheControl.FORCE_NETWORK,即强制使用网络请求
- CacheControl.FORCE_CACHE,即强制使用本地缓存,如果无可用缓存则返回一个code为504的响应
max-stale指示客户机可以接收超出超时期间的响应消息。如果指定max-stale消息的值,那么客户机可以接收超出超时期指定值之内的响应消息。
no-cache:不做缓存。
max-age:这个参数告诉浏览器将页面缓存多长时间,超过这个时间后才再次向服务器发起请求检查页面是否有更新。对于静态的页面,比如图片、CSS、Javascript,一般都不大变更,因此通常我们将存储这些内容的时间设置为较长的时间,这样浏览器是不会向服务器反复发起请求,也不会去检查是否更新了。
添加自定义网络拦截器,在其中改变Response的响应头,添加Cache-control,后续回到CacheIntercepter中时,就会执行缓存策略。
CacheControl.Builder
- noCache();//不使用缓存,用网络请求
- noStore();//不使用缓存,也不存储缓存
- onlyIfCached();//只使用缓存
- noTransform();//禁止转码
- maxAge(10, TimeUnit.MILLISECONDS);//设置超时时间为10ms。
- maxStale(10, TimeUnit.SECONDS);//超时之外的超时时间为10s
- minFresh(10, TimeUnit.SECONDS);//超时时间为当前时间加上10秒钟。
CacheStrategy:缓存策略类,通过响应头信息与服务器端信息进行对比,最后返回是否使用新的网络请求还是直接使用缓存。其中存储的是Request请求体和Response响应体的具体内容
/** 如果不使用网络,则 networkRequest为 null */
//客户端请求
public final @Nullable Request networkRequest;
/** 如果不使用缓存,则 cacheResponse为 null */
//服务端返回
public final @Nullable Response cacheResponse;
根据输出的networkRequest和cacheResponse的值是否为null给出不同的策略,如下:
| networkRequest | cacheResponse | result 结果 |
|---|---|---|
| null(不使用网络) | null(不使用缓存) | only-if-cached (表明不进行网络请求,且缓存不存在或者过期,一定会返回503错误) |
| null | non-null | 不进行网络请求,直接返回缓存,不请求网络 |
| non-null | null | 需要进行网络请求,而且缓存不存在或者过去,直接访问网络 |
| non-null | non-null | Header中包含ETag/Last-Modified标签,需要在满足条件下请求,还是需要访问网络(根据情况使用) |
如果网络不为null,则使用网络请求,如果网络为null,当缓存不为null,则使用缓存,当缓存为null时,返回503错误
为什么只做get的缓存?
其他响应成本大,效率低
线程池
okhttp其中有一个dispatcher对最大连接数(并发),host最大访问量做了定义,维护了3个队列(同步正在执行,准备执行,异步正在执行)和一个线程池(0~max)
该线程池类似于CachedThreadPool,没有核心线程,全是非核心线程,超时时间是60s,即60s后回收该线程,其队列为空,没有容量,是一种特殊的队列,适用于执行短时的大量任务。
okhttp的优势?
最大特点是,intercepter拦截器,连接池复用,okio io处理,线程池处理(全是非核心线程),支持http1.0,1.1,2.0
okio内部封装链表数据存储,比较之前的数组存储,更加节省空间,还可以复用
长连接(websocket)和久连接(keep-alive)的区别
调用的是equeue异步方法,将长连接放入线程池中不会被释放掉
- 1.1推出keep-alive机制,服务器不会主动发送请求,一个request返回一个response。
- 减少了握手的次数而已
- 久连接是同步串行处理的,当某一个请求因为网络,服务器等原因阻塞时,那么后面的请求都得不到处理
- http头部太大,传输耗时
- 实时性得不到保证
http是单向的,websocket属于应用层协议,使用http1.1的101码进行握手状态判断
websocket建立连接是使用https连接,三次握手,在通信过程中
- 以ws开头
- 握手成功后,复用连接发送请求和接收
- 不需要发送header信息
- 服务端客户端平等,可以相互建立连接,http久连接是基于http的,符合http协议。
最开始使用get请求进行握手,携带Upgrade: websocket ,告知服务器上升为websocket协议,成功后,使用web socket数据流(帧)进行通信,设置超时时间为永不超时,客户端设置循环,一直从服务端取消息。
使用http的get请求进行3次握手协议,使用http1.1版本的101状态码返回成功后,就不需要http交互了,后续采用web socket流进行通信,减少包体
使用标准的HTTP协议无法实现WebSocket,只有支持那些协议的专门浏览器才能正常工作。
websocket的握手和http的握手有什么区别?
使用http的get请求进行握手,基本一致,额外传输了header的信息标记为websocket。
MQTT理解
发布订阅者模式,低带宽,低开销的即时通信协议,基于tcp/ip协议,成为IOT通讯标准
消息体如下:固定头部+可变头部+消息体,整个消息体比较轻便,便于交互及时
| 固定报头(fixed header) | 可变报头(variable header) | 荷载(payload) |
|---|---|---|
| 所有报文都包含,数据包类型及数据包的分组类标识 | 部分报文包含,数据包类型决定了可变头是否存在及其具体内容 | 部分报文包含,表示客户端收到的具体内容 |
基于二进制实现,MQTT运行于http上,所以明文传输,如果位于https中,则可以使用TLS加密传输
发布者,订阅者模式:客户端是发布者和订阅者,服务端是代理服务器
MQTT和websocket的区别?
MQTT面向原生设备,基于二进制实现,提供一对多的通信方式,采用发布/订阅模式传输
websocket面向web设备,是全双工通信
Retrofit
简述
基于Okhttp的RESTFUL Api请求工具,Retrofit可以让你简单到调用一个Java方法的方式去请求一个api,这样App中的代码就会很简洁方便阅读
Retrofit通过java接口及注解来描述网络请求,并用动态代理的方式生成网络请求的Request,通过调用相应的网络框架(默认Okhttp)去发起网络请求,并将返回的Response通过converterFactory转化成相应的model,最后通过CallAdapter转换成其他的数据方式(Rxjava Observable)
Retrofit.create()方法是Retrofit的核心,其中,使用Proxy.newProxyInstance()方法创建ServiceMethod,具体实现是在InvocationHandler类中的invoke方法,实现了动态代理的形式,而这个InvocationHandler对象就是代理对象,这个对象是在运行时动态生成的。
Retrofit中的动态代理
public <T> T create(final Class<T> service) {
...
return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class<?>[] { service },
new InvocationHandler() {
...
ServiceMethod<Object, Object> serviceMethod =
(ServiceMethod<Object, Object>) loadServiceMethod(method);
OkHttpCall<Object> okHttpCall = new OkHttpCall<>(serviceMethod, args);
return serviceMethod.callAdapter.adapt(okHttpCall);
}
});
}
通过动态代理生成InvocationHandler类,在内部,创建一个ServiceMethod,存储接口的请求信息,构建一个OKhttpCall对象,初始化网络请求
Retrofit的优势
Retrofit是对okhttp的二次封装,解决okhttp中接口请求、数据结果返回和接口调用的短板。
- 规划了interface接口类,整合所有接口调用的详细内容,便于调用,降低耦合性。
- 配合RxJava,okhttp,职责明确。RxJava负责异步处理,Retrofit负责请求的数据和结果的展示,okhttp负责接口请求和返回的具体过程
- Retrofit主要负责应用层面的封装,就是说主要面向开发者,方便使用,比如请求参数,响应数据的处理,错误处理等等。
- OkHttp主要负责socket部分的优化,比如多路复用,buffer缓存,数据压缩等等。
- 相对于okhttp来说,使用动态代理生成Request对象,不用每次调用自己实现
- 网络结果线程切换库(RxJava,普通),网络结果格式化库(Gson,xml)等可以做到随意替换和支持
动态代理和静态代理的区别
静态代理:
由程序员创建或工具生成的代理类,在运行前就存在代理类的字节码文件,代理类和委托类的关系已经确定
动态代理:
在程序运行过程中,通过反射实现对代理类的动态创建,可以代理多个方法。(InvocationHandler、CGLib)
Butterknife———view的注入
简述Butterknife
初始化控件会写大量的findViewById(),setOnClickListener()方法,很繁琐,该框架使用注解的方式实现辅助代码的生成,简化这些代码。
该框架是基于java注解机制实现的,也就是在编译期间就初始化好了一个viewBinding类(view和点击事件的处理),生成findViewById来绑定布局,不用开发者每次去初始化
Butterknife为什么初始化控件不能用private和static
因为在编译期间构建view的绑定事件会报错,无法访问private变量,否则,要加入反射,导致性能问题
static可能会导致内存泄漏,而且外部可以访问。
ButterKnife为什么执行效率为什么比其他注入框架高?它的原理是什么
解析注解处理器, 对比Butterknife,Dagger2,DBFlow。
没有反射机制,使用自定义注解框架
继承问题
butterknife继承后,子类可以使用父类控件,但是必须在setContView之后进行绑定。如果在子view进行绑定控件,但是父类找不到子类的控件,因为生成的是子view_ViewBinding类,父类获取不到
缺点
- 每个Activity会生成一个类,增大包体积
- butterknife可以称之为view的注入,对findviewById包装更加简单,功能单一
ViewBinding最终最好的解决方案
简析ViewBinding
在build.gradle中开启ViewBinding
android {
//...
viewBinding.enabled = true
}
android studio会将xml文件中所有文件在编译过程中生成xxxBinding类,这个类有三种初始化方法
binding = LayoutSecondBinding.inflate(getLayoutInflater());
在Activity或Fragment中调用inflate方法进行初始化
inflate(LayoutInflater inflater)
inflate(LayoutInflater inflater,ViewGroup parent,boolean attachToParent)
bind(View rootView)
最终三个方法都会走到bind方法中,在这个方法中,对View进行findViewById操作
ViewBinding和Butterknife的区别
ViewBinding会处理空安全,类型安全,还可以兼容java和kotlin。最新版本Gradle中设置对R文件的修改,R文件中的id不再是final的,(Fragment中R文件不是final的时候,Butterknife使用生成R2文件的做法围魏救赵)这样就会影响注解的使用,butterknife就被迫下台
Viewbinding根正苗红,官方支持就是最大优势
Viewbinding如何处理include布局?
xml布局中存在include布局的,需要给include布局添加id,生成一个includexxxBinding文件,在xxxBinding类中做映射体现。
内部view由final修饰,保证view不能被重新创建的view替换(引用不可修改),但是其内部的值可以修改
注解原理
简析
元注解:修饰注解的注解,
- @Target:注解的作用目标(修饰方法,类还是字段)
- @Retention:注解的生命周期
- SOURCE:仅存在java源文件中,经过编译器后就丢弃,适用于一些检查行的操作,比如@Override
- CLASS:编译class文件时生效,适用于在编译时做一些预处理操作,比如Butterknife的@BindView,在编译时,通过注解器生成一些辅助代码,完成完整的功能
- RUNTIME:保留在运行时VM中可以通过反射获取注解。适用于一些需要运行时动态获取注解信息,类似反射获取注解等,比如EventBus的@Subscribe
- @Documented:注解是否应当被包含在JavaDoc文档中
- @Inherited:是否允许子类继承该注解
- AnnotationInvocationHandler:专门处理注解的Handler
代码的生命周期包含:编码(SOURCE)---->编译(CLASS)---->运行(RUNTIME)
默认时注解在编译阶段,即CLASS阶段
本质:一个继承了Annotation接口的接口
- 运行时处理:使用反射获取当前的所需要的东西
- 编译时处理:APT技术,即编译期扫描java文件的注解,并传递到注解处理器,注解处理器可根据注解生成新的java文件
APT(Annotation Processing Tool)编译期解析注解
注解的种类
- JDK提供的注解(源码注解)
- 自定义注解
- 元注解
注解的用处
- 降低项目的耦合
- 自动完成一些规律性代码
- 自动生成java代码,减少开发工作量
注解器
注解器通常是以Java代码(或者编译过的字节码)作为输入,生成.java文件作为输出
AbstractProcessor
// 源于javax.annotation.processing;
public abstract class AbstractProcessor implements Processor {
// 集合中指定支持的注解类型的名称(这里必须时完整的包名+类名)
public Set<String> getSupportedAnnotationTypes() {
SupportedAnnotationTypes sat = this.getClass().getAnnotation(SupportedAnnotationTypes.class);
if (sat == null) {
if (isInitialized())
processingEnv.getMessager().printMessage(Diagnostic.Kind.WARNING,
"No SupportedAnnotationTypes annotation " +
"found on " + this.getClass().getName() +
", returning an empty set.");
return Collections.emptySet();
}
else
return arrayToSet(sat.value());
}
// 指定当前正在使用的Java版本
public SourceVersion getSupportedSourceVersion() {
SupportedSourceVersion ssv = this.getClass().getAnnotation(SupportedSourceVersion.class);
SourceVersion sv = null;
if (ssv == null) {
sv = SourceVersion.RELEASE_6;
if (isInitialized())
processingEnv.getMessager().printMessage(Diagnostic.Kind.WARNING,
"No SupportedSourceVersion annotation " +
"found on " + this.getClass().getName() +
", returning " + sv + ".");
} else
sv = ssv.value();
return sv;
}
// 初始化处理器
public synchronized void init(ProcessingEnvironment processingEnv) {
if (initialized)
throw new IllegalStateException("Cannot call init more than once.");
Objects.requireNonNull(processingEnv, "Tool provided null ProcessingEnvironment");
this.processingEnv = processingEnv;
initialized = true;
}
/**
* 这些注解是否由此 Processor 处理,该方法返回ture表示该注解已经被处理, 后续不会再有其他处理器处理; 返回false表示仍可被其他处理器处理
*/
public abstract boolean process(Set<? extends TypeElement> annotations,
RoundEnvironment roundEnv);
}
自定义注解
如果是单一属性,可以使用value字段
@interface MyAnno1 {
//格式:类型名 属性名()
String value();
}
@MyAnno1("kang")
@interface MyAnno2 {
//格式:类型名 属性名()
String name();
}
@MyAnno2(name = "kang")
如果不是value字段的话,需要(指定属性 = 值)
注解中只允许八种基本数据类型、字符串、类类型,注解类型,枚举类型及其一维数组
Glide和Picasso
简述LruCache和DiskLruCache
内部都实现了LRU算法,即:优先淘汰那些最近最少使用的缓存对象
LruCache:
内部采用LinkedHashMap,是线程安全的(get、put、remove方法都是用synchronized),双向链表(保持LinkedList规定顺序+HashMap便于查找) 将最近使用的节点放置在链表尾部,当超过大小时,删除第一个头节点。
存储了前一个元素和后一个元素的引用,get方法后,就查出值删除,然后放置在头部,如果超过Lru算法的大小,直接遍历删除尾节点,直到大小在范围内
简述Glide的缓存过程
默认打开缓存,内存缓存使用LruCache+弱引用实现,磁盘缓存使用DiskLruCache实现
使用内存缓存+磁盘缓存的策略,生成key时,图片只要发生变化,就算长宽发生变化也会导致缓存不同的key。Glide将内存缓存划分为两个区域:
- LruResourceCache:使用LruCache算法,LinkedHashMap(不在使用的)
- activeResources:添加弱引用机制,HashMap(正在使用的图片)
磁盘缓存之存在DiskLruCache,因为Glide可以压缩图片(尺寸压缩),所以磁盘缓存中可以设置缓存原始图片还是压缩后的图片,压缩图片可以有效避免大图和超大图带来的OOM,Glide没有使用google提供的DiskLruCache,而是使用自己开发的,不过原理都一样
在首次访问时,将正在使用的图片信息会存储在activeResources的弱引用中,当引用次数为0时(调用release方法),会将其放入LruResourceCache中,执行Lru算法,移除后会存入DiskLruCache中。
所以,先去activeResources中寻找,找到后,将引用对象索引+1(active.aquire();)(计算引用次数)。然后去LruResourceCache中寻找了,若找到了,在LruResourceCache中移除,并将其放入activeResources中。然后去DiskLruCache中寻找,若找到了,在DiskLruCache中删除,并将其放入activeResources中。
Glide 缓存的是imageView的所需图片的大小,若大小不同,重新缓存
Picasse 缓存图片原图大小
Glide是如何绑定生命周期的?
- Application参数:如果传入的是Application对象,那么这里就会调用带有Context参数的get()方法重载,调用getApplicationManager()方法来获取一个RequestManager对象。其实这是最简单的一种情况,因为Application对象的生命周期即应用程序的生命周期,因此Glide并不需要做什么特殊的处理,它自动就是和应用程序的生命周期是同步的,如果应用程序关闭的话,Glide的加载也会同时终止。
- 非Application参数:不管传入的是Activity、FragmentActivity、v4包下的Fragment、还是app包下的Fragment,最终的流程都是一样的,那就是会向当前的Activity当中添加一个隐藏的Fragment。因为Glide需要知道加载的生命周期。很简单的一个道理,如果你在某个Activity上正在加载着一张图片,结果图片还没加载出来,Activity就被用户关掉了,那么图片还应该继续加载吗?当然不应该。可是Glide并没有办法知道Activity的生命周期,于是Glide就使用了添加隐藏Fragment的这种小技巧,因为Fragment的生命周期和Activity是同步的,如果Activity被销毁了,Fragment是可以监听到的,这样Glide就可以捕获这个事件并停止图片加载了。
- 如果我们是在非主线程当中使用的Glide,那么不管你是传入的Activity还是Fragment,都会被强制当成Application来处理。
- RequestManagerFragment:实现一个无UI的fragment。
- ActivityFragmentLifecycle:无UI的fragment通过它,去调用RequestManager
- RequestManager:实现关键的几个方法,去调用glide 的操作
- RequestManagerRetriever:作为一个桥梁,将RequestManagerFragment 和RequestManager给联系起来
空RequestManagerFragment 的生命周期调用 ActivityFragmentLifecycle,然后ActivityFragmentLifecycle 调用 RequestManager ,RequestManager 再去调用RequestTracker 的glide操作,最终实现gilde的操作,能够根据页面的生命周期做相应的处理。
Glide中Fragment中是怎么绑定生命周期的?
Glide中into指定view,再次刷新view会发生什么?
Glide内部通过HttpUrlConnection进行通信,也可切换为okhttp/volley
- 根据ScaleType进行相应的设置
- 根据传入的类型对Glide加载进行配置,asBitmap,asGif,asDrawable
- 根据target(View)创建Request请求,根据生命周期管控Request的暂停和下载
target就是view,先判断target是之前已经绑定了请求,如果旧请求和新请求一样且处于请求完成或者正在请求状态就直接复用旧请求。如果不复用,就RequestManager先移除和旧请求绑定的target对象,Target再重新和Request对象进行一个绑定,调用requestManager.track(target, request)再加入请求队列,开启请求,最后返回经过处理的traget对象。
Glide和Picasso对比
- Glide较Picasso庞大的多
- Glide绑定生命周期,onPause时暂停加载,onResume时再启动,Picasso只存在context
- Glide会缓存imageView图片大小,尺寸不同,key不同,会缓存两份,Picasso是缓存完整大小,使用时会重新设置大小
- Glide首次加载快于Picasso,而后每次加载慢于Picasso,因为Glide需要改变图片的大小再缓存到内存,时间会慢。picasso拿到缓存后需要对图片重新设置大小,耗时较长。
- Glide支持gif
- Glide加载的图片质量略差,因为bitmap的格式内存开销小,但是很难察觉
- Glide可以配置图片显示的动画,而picasso只有默认的一种动画
- Glide缓存方式更优,减少OOM的发生
Glide:RGB565
Picasso:ARGB8888
Picasso
缓存机制:LruCache,DiskLruCache
内存缓存占用一个app的15%内存
网络请求使用的okhttp,内部缓存也使用okhttp,一般大小不超过50M
网络机制:network
Fresco比较
缓存机制:
-
BitmapMemoryCache缓存,已解码的内存缓存
-
EncodedMemoryCache缓存,
-
CountingLruMap,Lru算法清除缓存
分层处理,producer层层处理,每层处理结果通过Consumer向上传递,Producer-consumer链
- 从已编码缓存中获取bitmap缓存
- 从未编码缓存中获取EncodedImage类型
- 从磁盘中获取
- 从网络中获取
DraweeView:
动图播放,多级图层,渐进显示,画面剪裁
- 解码器优化,避免频繁解码导致内存抖动,使用pool内存池复用
适合各个android版本的解码器
优点:
- 内存管理,LRU算法,缓存和磁盘管理
- 加载大图和高清图时可以先加载低清晰度图和缩略图
- 加载gif
- 图片渐进式处理:,渐进式图片格式先呈现大致的图片轮廓,然后随着图片下载的继续,呈现逐渐清晰的图片
包体积
Fresco>Glide>Picasso
LeakCanary
检测内存泄漏
ActivityLifecycleCallbacks 与 FragmentLifeCycleCallbacks
通过application.registerActivityLifecyleCallbacks获取Activity的生命周期
通过fragmentManager.registerFragmentLifecyleCallbacks获取Fragment的生命周期
注册接口,拿到Activity和Fragment的各种生命周期回调信息
如何做到内存泄漏检测
Activity在onDestory后会将Activity生成一个唯一的key后存储在弱引用队列中,在主线程空闲时(IdleHandler)触发gc机制,垃圾回收,整理弱引用队列,查看弱引用队列中没有被回收的对象,即是内存泄漏的对象,打印出栈堆信息以供分析dump
缺陷
只能监听Activity和Fragment的内存泄漏检测,无法检测Service
其他内存泄漏检测工具
Profiler:android studio自带,可以查看内存的整体过程,分析是否发生内存泄漏
ASM函数插桩
简述
简析为字节码插桩,可以直接修改已经存在的class文件或者生成class文件,相比较于AspectJ,ASM更加偏向于底层,他是直接操作字节码的,在设计中更小,更快
class文件本质是16进制数据
ClassVisitor
MethodVistor