0.相关分享:
Android从屏幕刷新到View的绘制(一)之 Window、WindowManager和WindowManagerService之间的关系
Android从屏幕刷新到View的绘制(二)之Choreographer、Vsync与屏幕刷新
1. 相关类
Choreographer 编舞者
Android4.1之后新增的机制,用于配合系统的 Vsync (Vertical Synchronization,垂直同步)信号,编舞者可以接受系统的 Vsync信号,统一管理引用的输入、动画、绘制等任务的执行时机,使得整体显得协调连贯。业界一般通过它来监控应用的帧率。
CallbackQueue
Choreographer添加的任务最后都被封装为CallbackRecord,以链表的形式保存在CallbackQueue中,Choreographer的mCallbackQueues是一个数组,长度为4,保存着四个CallbackQueue链表(Android8)。分别处理:输入、动画、遍历绘制等任务。
FrameDisplayEventReceiver
Android4.1之后,默认开启了Vsync功能,FrameDisplayEventReceiver用于接收Vsync事件,并将事件提交给looper(一般为主线程)。它会通过JNI创建一个IDisplayEventConnection的Vsync监听者。
FrameHandler
处理主线程中关于Vsync的事件。执行异步消息,有延迟的任务发延迟消息、不在原线程的发到原线程。
2. 源码分析
在前面的分析我们知道,Activity的onResume()执行完之后,会通知WMS进行window的添加。过程中会调用到ViewRootImpl的setView(),最后又会走到scheduleTraversals()方法。
另外,其实我们使用View.invalidate()、requestLayout()等方法时,最后都会走到ViewRootImpl的scheduleTraversals()方法。也就是说,所有UI的变化最后都会走到ViewRootImpl的scheduleTraversals()方法
在Android的framework源码阅读过程在,我们会遇到很多类如一一对应的 scheduleXXX() 与 performXXX(),这可以理解为“发起请求”以及得到响应之后的“执行任务”。在这里,scheduleTraversals() 就是进行发起绘制请求,等到 Vsync 信号到来之后,执行绘制performTraversals()。
先来挂一张流程图,可能会一脸懵,但看完后续源码解析后,这张流程图就一目了然了:
2.1 Choreographer的创建
在ViewRootImpl对象创建的时候,会获取到一个Choreographer实例:
//ViewRootImpl
Choreographer mChoreographer;
//ViewRootImpl实例是在添加window时创建的
public ViewRootImpl(Context context,Display display){
//...
mChoreographer = Choreographer.getInstance();
//...
}
Choreographer维护了一个looper和handler:
//Choreograper
//通过ThreadLocal获取线程私有的Choreographer
private static final ThreadLocal<Choreographer> sThreadInstance =
new ThreadLocal<Choreographer>() {
@Override
protected Choreographer initialValue() {
//当前线程的Looper
Looper looper = Looper.myLooper();
if (looper == null) {
throw new IllegalStateException("The current thread must have a looper!");
}
return new Choreographer(looper, VSYNC_SOURCE_APP);
}
};
public static Choreographer getInstance() {
return sThreadInstance.get();
}
我们来看到Choreographer的构造方法,它做了:
- 维护了所在线程的Looper
- 为该looper设置了一个FrameHandler,处理绘制消息
- Android4.1之后,还有一个Vsync监听者 FrameDisplayEventReceiver()
- 初始化了不同事件的回调队列
private Choreographer(Looper looper, int vsyncSource) {
//所在线程的looper
mLooper = looper;
//handler消息处理,只能收到当前线程Looper的消息
mHandler = new FrameHandler(looper);
//Vsync消息监听者,Android4.1之后默认开启USE_VSYNC
mDisplayEventReceiver = USE_VSYNC
? new FrameDisplayEventReceiver(looper, vsyncSource)
: null;
// 计算一帧的时间,Android手机屏幕是60Hz的刷新频率,就是16ms
mFrameIntervalNanos = (long)(1000000000 / getRefreshRate());
//数组长度为4 (在android8.0中)
mCallbackQueues = new CallbackQueue[CALLBACK_LAST + 1];
for (int i = 0; i <= CALLBACK_LAST; i++) {
mCallbackQueues[i] = new CallbackQueue();
}
}
我们可以按经验来看着几个对象,猜测当Vsync信号到来时,可以进行刷新,FrameHandler收到了这个消息,回调给CallbackQueue中的各个View任务。
2.2 向Choreographer添加任务
mChoreographer.postCallback()就是向CallbackQueue中添加View任务的方法,这个方法有三个参数,第一个参数有四种类型(Android8中是四种类型),表示的是这个任务的类型:
//Choreographer
//输入事件,优先级最高
public static final int CALLBACK_INPUT = 0;
//动画任务
public static final int CALLBACK_ANIMATION = 1;
//绘制任务
public static final int CALLBACK_TRAVERSAL = 2;
//提交,优先级最低,最后执行
public static final int CALLBACK_COMMIT = 3;
我们之前谈到,所有UI的变化都走到了ViewRootImpl的scheduleTraversals()中,这个方法向Choreographer添加了任务申请:
//ViewRootImpl
void scheduleTraversals() {
if (!mTraversalScheduled) {
//标志位防重复提交,也就是多次requestLayout、invalidate在一次绘制申请中,只会生效一次
mTraversalScheduled = true;
//添加同步屏障,挡住后续所有的同步消息,让Looper优先分发异步事件。Vsync来到时,发布的是异步消息,优先处理
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
//添加任务
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
if (!mUnbufferedInputDispatch) {
scheduleConsumeBatchedInput();
}
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}
来到scheduleTraversals(),核心做了几件事,这几件事都很重要,和后续有紧密联系:
- 将申请标志位置为true,避免重复提交,在一次绘制申请中,多次requestLayout/invalidate只会生效一次 我们知道invalidate等方法都只是一个申请,方法内没有数据变化,在Vsync到来之前,View的数据可以变化,最终只会生效最后的情况,所以没必要重复接收多个invalidate()请求,就好像你对妈妈说:“我饿了,我想吃一碗饭”,妈妈收到了,说“好的”,妈妈就去煮饭了,你又说“我饿了,我想吃两碗饭”,妈妈不会理会你,反正最后你吃几碗是几碗,妈妈直到你饿了已经去煮了
- 向Looper的MessageQueue中添加同步屏障,保证Vsync来到后异步消息能够立即执行。
- 向Choreographer添加mTraversalRunnable,注册绘制任务的callback
我们来看一下这个mTraversalRunnable具体做了什么来执行绘制:
//ViewRootImpl.java
final class TraversalRunnable implements Runnable {
@Override
public void run() {
//只做了一件时,调用外部类,也就是ViewRootImpl对象的doTraversal()方法
doTraversal();
}
}
//ViewRootImpl只管绘制,所以TraversalRunnable订好了只做doTraversal()
final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
void doTraversal() {
if (mTraversalScheduled) {
//标志设为false,可以继续接受scheduleTraversal()任务
mTraversalScheduled = false;
//取出Looper的MessageQueue中的同步屏障,恢复同步消息的分发
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
//进入ViewRootImpl下的DecorView的绘制流程
performTraversals();
}
}
这里有几个点注意:
- mTraversalScheduled字段又被设置为false,和之前的故事相照应:妈妈把电饭煲端出来了,你可以吃饭了,怎么吃是你的事了,妈妈很疼你,如果你还要吃,允许你再请妈妈煮饭
- 之前由于添加了同步屏障,为了保证Vsync来到后立即绘制,但屏蔽了正常的同步消息,所以这里需要把同步屏障取消掉
- Vsync到来之后,由于之前注册的回调,所以会执行TraversalRunnable->run()->ViewRootImpl.doTraversal()->performTraversals()->View的绘制流程。
那么这个回调在注册过程中发生了什么呢?
mChoreographer.postCallback()内部调用了postCallbackDelayed(),紧接着调用了postCallbackDelayedInternal():
//Choreographer
//这里的action,就是传入的Runnable,由于类型不一定,所以改成了Object
private void postCallbackDelayedInternal(int callbackType,
Object action, Object token, long delayMillis) {
synchronized (mLock) {
//当前时间
final long now = SystemClock.uptimeMillis();
//执行时间
final long dueTime = now + delayMillis;
//将传入的action包装在CallbackRecord中,插入到队列里
mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);
//如果时间已经到了,其实就是delayMillis==0的情况
if (dueTime <= now) {
//立即执行scheduleFrameLocked()
scheduleFrameLocked(now);
} else {
//如果这个请求时延迟任务,那么就将其设置为异步任务,并添加上时间,放在Message中,发送Message。
//复用了 MSG_DO_SCHEDULE_CALLBACK的message,将action传入
Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
msg.arg1 = callbackType;
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, dueTime);
}
}
}
这里主要做了几件事:
- 计算时间,如果需要立即执行,则调用scheduleFrameLocked()
- 如果是延迟消息,先去复用池获取一个msg,将action传入,这里的action是之前传入的mTraversalRunnable
- 由于这是绘制任务,为了通过同步屏障,我们需要设置消息为异步消息
msg.setAsynchronous(true)
- message需要定一个
int what
让handler来决定处理方式
CallbackQueue.addCallbackLocked()就是把dueTime、action、token(这里的token为null)等信息存入CallbackRecord后,放在CallbackQueue链表中。
我们先来看一下直接发送的 scheduleFrameLocked()
//Choreographer
private void scheduleFrameLocked(long now) {
if (!mFrameScheduled) {
//似乎成了一个约定,只要是schedule申请,就会有一个标志来表示是否已接受过这个申请。
mFrameScheduled = true;
if (USE_VSYNC) {//Android4.1之后默认开启Vsync垂直同步
//线程判断
if (isRunningOnLooperThreadLocked()) {
//如果就是本线程发起的,就进入这里
scheduleVsyncLocked();
} else {
//如果是其他线程调用的这个方法,则通过Message来将任务的线程切换到本线程执行。最终还是执行scheduleVsyncLocked()
Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_VSYNC);
msg.setAsynchronous(true);
mHandler.sendMessageAtFrontOfQueue(msg);
}
} else {
//没有垂直同步,就直接doFrame()
Message msg = mHandler.obtainMessage(MSG_DO_FRAME);
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, nextFrameTime);
}
}
}
如果开启了Vsync,那么进入到scheduleVsyncLocked():
//Choreographer
private void scheduleVsyncLocked() {
//调用了DisplayEventReceiver的方法
mDisplayEventReceiver.scheduleVsync();
}
//DisplayEventReceiver
public void scheduleVsync() {
//调用了本地方法
nativeScheduleVsync(mReceiverPtr);
}
//这是一个本地方法
private static native void nativeScheduleVsync(long receiverPtr);
我们之前介绍直到,DisplayEventReceiver它会通过JNI创建一个IDisplayEventConnection的Vsync监听者。我们来到framework/base/core/jni包下看看:
//android_view_DisplayEventReceiver.cpp
static void nativeScheduleVsync(JNIEnv* env, jclass clazz, jlong receiverPtr) {
sp<NativeDisplayEventReceiver> receiver =
reinterpret_cast<NativeDisplayEventReceiver*>(receiverPtr);
status_t status = receiver->scheduleVsync();
if (status) {
String8 message;
message.appendFormat("Failed to schedule next vertical sync pulse. status=%d", status);
jniThrowRuntimeException(env, message.string());
}
}
NativeDisplayEventReceiver继承自DisplayEventDispatcher
status_t DisplayEventDispatcher::scheduleVsync() {
if (!mWaitingForVsync) {
//...
//又由Dispatcher让DisplayEventReceiver去requestNextVsync()
status_t status = mReceiver.requestNextVsync();
//...
mWaitingForVsync = true;
}
return OK;
}
顾名思义,让DisplayEventReceiver去申请下一个Vsync,最后来到/gui/DisplayEventReceiver.cpp:
//gui/DisplayEventReceiver.cpp
status_t DisplayEventReceiver::requestNextVsync() {
if (mEventConnection != NULL) {
mEventConnection->requestNextVsync();
return NO_ERROR;
}
return NO_INIT;
}
我们可以看到,DisplayEventReceiver通过IDisplayEventConnection对象去申请下一个Vsync
class BpDisplayEventConnection : public SafeBpInterface<IDisplayEventConnection> {
void requestNextVsync() override {
callRemoteAsync<decltype(&IDisplayEventConnection::requestNextVsync)>(
Tag::REQUEST_NEXT_VSYNC);
}
}
看名字就知道,这个DisplayEventConnection只是一个binder引用,需要远程调用其实体去requestNextVsync,我们来看到BnDisplayEventConnection::onTransact是如何处理这个事务的:
status_t BnDisplayEventConnection::onTransact(uint32_t code, const Parcel& data, Parcel* reply,
uint32_t flags) {
if (code < IBinder::FIRST_CALL_TRANSACTION || code > static_cast<uint32_t>(Tag::LAST)) {
return BBinder::onTransact(code, data, reply, flags);
}
auto tag = static_cast<Tag>(code);
switch (tag) {
case Tag::STEAL_RECEIVE_CHANNEL:
return callLocal(data, reply, &IDisplayEventConnection::stealReceiveChannel);
case Tag::SET_VSYNC_RATE:
return callLocal(data, reply, &IDisplayEventConnection::setVsyncRate);
case Tag::REQUEST_NEXT_VSYNC:
//通过callLocalAsync()来request_next_vsync
return callLocalAsync(data, reply, &IDisplayEventConnection::requestNextVsync);
}
}
接下来来到SafeInterface.h中的callLocalAsync():
//SafeInterface.h
template <typename Method>
status_t callLocalAsync(const Parcel& data, Parcel* /*reply*/, Method method) {
using ParamTuple = typename SafeInterface::ParamExtractor<Method>::ParamTuple;
typename RawConverter<std::tuple<>, ParamTuple>::type rawArgs{};
//发送parcel数据
status_t error = InputReader<ParamTuple>{mLogTag}.readInputs(data, &rawArgs);
//方法返回
MethodCaller<ParamTuple>::callVoid(this, method, &rawArgs);
return NO_ERROR;
}
我只跟到这里,这里应该只是发送Vsync通知SurfaceFlinger刷新的信号,下面两篇文章对Vsync做了更深层的底层探究
这有一个native层Vsync分发的文章: https://blog.csdn.net/Android062005/article/details/123090139
推荐一个SurfaceFlinger的学习文章 : https://www.jianshu.com/p/c954bcceb22a
本文只关注到,最后Vsync信号会被发送给DisplayEventReceiver::AttachedEvent::handleEvent():
//displayservice/DisplayEventReceiver.cpp
int DisplayEventReceiver::AttachedEvent::handleEvent(int fd, int events, void* /* data */) {
constexpr size_t SIZE = 1;
ssize_t n;
FwkReceiver::Event buf[SIZE];
while ((n = mFwkReceiver.getEvents(buf, SIZE)) > 0) {
for (size_t i = 0; i < static_cast<size_t>(n); ++i) {
const FwkReceiver::Event &event = buf[i];
uint32_t type = event.header.type;
uint64_t timestamp = event.header.timestamp;
switch(buf[i].header.type) {
case FwkReceiver::DISPLAY_EVENT_VSYNC: {
//回调onVsync()方法
mCallback->onVsync(timestamp, event.vsync.count);
} break;
case FwkReceiver::DISPLAY_EVENT_HOTPLUG: {
mCallback->onHotplug(timestamp, event.hotplug.connected);
} break;
default: {
LOG(ERROR) << "AttachedEvent handleEvent unknown type: " << type;
}
}
}
}
return 1; // keep on going
}
最终回调到Java层的DisplayEventReceiver.onVsync(),跟了这么深,还记得DisplayEventReceiver在java层的实现类是谁么?真棒,是Choreographer中的内部类FrameDisplayEventReceiver,我们来看看它:
//Choreographer
private final class FrameDisplayEventReceiver extends DisplayEventReceiver
implements Runnable {
private boolean mHavePendingVsync;
private long mTimestampNanos;
private int mFrame;
public FrameDisplayEventReceiver(Looper looper, int vsyncSource) {
super(looper, vsyncSource);
}
@Override
public void onVsync(long timestampNanos, int builtInDisplayId, int frame) {
if (builtInDisplayId != SurfaceControl.BUILT_IN_DISPLAY_ID_MAIN) {
scheduleVsync();
return;
}
long now = System.nanoTime();
if (timestampNanos > now) {
timestampNanos = now;
}
if (mHavePendingVsync) {
} else {
mHavePendingVsync = true;
}
mTimestampNanos = timestampNanos;
mFrame = frame;
//将本身作为runnable传入msg,被调用执行后,会进入run()->doFrame()
Message msg = Message.obtain(mHandler, this);
//设置为异步消息,越过同步屏障优先执行
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
}
@Override
public void run() {
mHavePendingVsync = false;
doFrame(mTimestampNanos, mFrame);
}
}
onVsync()方法中,将接收器本身作为Runnable传入异步消息msg,可以越过同步屏障,优先执行,但它并不一定立即执行,仅仅是越过同步屏障罢了,如果前面有较为耗时的操作,仍然需要等待,然后才会执行本次doFrame()。这也说明主线程不应当有耗时操作,否则就会影响屏幕输入事件、绘制等任务,出现ANR问题。
最后来到 doFrame() 我们来看看具体是如何逐步调用各种事件响应的:
//Choreographer
void doFrame(long frameTimeNanos, int frame) {
final long startNanos;
synchronized (mLock) {
if (!mFrameScheduled) {
return; // no work to do
}
//...
// 预期执行时间
long intendedFrameTimeNanos = frameTimeNanos;
startNanos = System.nanoTime();
// 超时时间是否超过一帧的时间(这是因为MessageQueue虽然添加了同步屏障,但是还是有正在执行的同步任务,导致doFrame延迟执行了)
final long jitterNanos = startNanos - frameTimeNanos;
if (jitterNanos >= mFrameIntervalNanos) {
// 计算掉帧数
final long skippedFrames = jitterNanos / mFrameIntervalNanos;
if (skippedFrames >= SKIPPED_FRAME_WARNING_LIMIT) {
// 掉帧超过30帧打印Log提示
Log.i(TAG, "Skipped " + skippedFrames + " frames! "
+ "The application may be doing too much work on its main thread.");
}
final long lastFrameOffset = jitterNanos % mFrameIntervalNanos;
//...
frameTimeNanos = startNanos - lastFrameOffset;
}
//...
mFrameInfo.setVsync(intendedFrameTimeNanos, frameTimeNanos);
// Frame标志位恢复
mFrameScheduled = false;
// 记录最后一帧时间
mLastFrameTimeNanos = frameTimeNanos;
}
try {
// 按类型顺序 执行任务
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "Choreographer#doFrame");
AnimationUtils.lockAnimationClock(frameTimeNanos / TimeUtils.NANOS_PER_MS);
//首先处理输入事件
mFrameInfo.markInputHandlingStart();
doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos);
//然后执行动画
mFrameInfo.markAnimationsStart();
doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);
//接下来执行View的绘制
mFrameInfo.markPerformTraversalsStart();
doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);
//最后提交
doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos);
}
//...
}
接下来就到doCallbacks()把各个CallbackQueue中的任务回调执行,还记得吗?最初我们会提交任务到Choreographer,然后通过postCallback来等待回调,现在就是回调的时机了:
//Choreorapher
void doCallbacks(int callbackType, long frameTimeNanos) {
CallbackRecord callbacks;
synchronized (mLock) {
//执行其中时间ok的任务,也就是查找到达执行时间的CallbackRecord
callbacks = mCallbackQueues[callbackType].extractDueCallbacksLocked(
now / TimeUtils.NANOS_PER_MS);
if (callbacks == null) {
return;
}
mCallbacksRunning = true;
//一些log。。。
}
try {
//遍历执行CallbackRecord的run()方法,进一步调用里面的action的方法。例如传入的比如mTraversalRunnable的run()方法
for (CallbackRecord c = callbacks; c != null; c = c.next) {
//内部回调callback的run
c.run(frameTimeNanos);
}
} finally {
synchronized (mLock) {
mCallbacksRunning = false;
do {
final CallbackRecord next = callbacks.next;
//回收这个CallbackRecord
recycleCallbackLocked(callbacks);
callbacks = next;
} while (callbacks != null);
}
}
}
我们最后再来看一下CallbackRecord这个类:
private static final class CallbackRecord {
public CallbackRecord next;
public long dueTime;
public Object action; // Runnable or FrameCallback
public Object token;
@UnsupportedAppUsage
public void run(long frameTimeNanos) {
if (token == FRAME_CALLBACK_TOKEN) {
// 通过postFrameCallback 或 postFrameCallbackDelayed,会执行这里
((FrameCallback)action).doFrame(frameTimeNanos);
} else {
//取出Runnable执行run()
((Runnable)action).run();
}
}
}
例如ViewRootImpl发起的绘制任务时传递token为null,所以进入到了mTraversalRunnable.run()。接下去就是我们熟悉的去除内存屏障,performTraversals()->View的绘制流程。
此外,我们注意到token还有可能是 FRAME_CALLBACK_TOKEN,这一般被用来计算丢帧情况。通过Choreographer.postFrameCallback()。
我们最后再来回顾流程图,应当是一目了然了:
-
ViewRootImpl可以接受来自View的invalidate、requestLayout等请求
-
ViewRootImpl通过scheduleTraversals()发起请求,向Choreographer编舞者添加绘制回调
-
Choreographer通过FrameDisplayEventReceiver来注册接受Vsync信号。这是个DisplayEventReceiver的binder引用,是个BpBinder。通过Binder通信,让BnDisplayEventReceiver去接受Vsync信号。
-
可以注意到,虽然屏幕16ms刷新,但如果屏幕布局元素一直没有变化,那么就不会申请重绘,也就一直显示当前的页面。我们平时屏幕大部分时间还是静态的,所以不会那么高频地去进行刷新。除非我们在看电影、打游戏等需要持续变化的,才会高频地刷新屏幕。因为需要不断重绘元素或者画面。(此外,这里我做了一个测试,在动画过程中,执行耗时操作,并不会报ANR,只会跳帧/丢帧?)
Choreographer: Skipped 599 frames! The application may be doing too much work on its main thread.
其实也是因为ANR只发生在:
- Service Timeout:前台服务在20s内未执行完成
- BroadcastQueue Timeout:前台广播在10s内未执行完成
- ContentProvider Timeout:内容提供者在publish过超时10s;
- InputDispatching Timeout:输入事件分发超时5s,包括按键和触摸事件。
对于重绘、动画任务被主线程耗时操作堵住时不会报ANR,也是因为这个这两个事件超时执行了并不会报错,仅是在Choreographer中做了个JANK记录,打印出来。
//Choreographer if (jitterNanos >= mFrameIntervalNanos) { final long skippedFrames = jitterNanos / mFrameIntervalNanos; if (skippedFrames >= SKIPPED_FRAME_WARNING_LIMIT) { Log.i(TAG, "Skipped " + skippedFrames + " frames! " + "The application may be doing too much work on its main thread."); } final long lastFrameOffset = jitterNanos % mFrameIntervalNanos; if (DEBUG_JANK) { Log.d(TAG, "Missed vsync by " + (jitterNanos * 0.000001f) + " ms " + "which is more than the frame interval of " + (mFrameIntervalNanos * 0.000001f) + " ms! " + "Skipping " + skippedFrames + " frames and setting frame " + "time to " + (lastFrameOffset * 0.000001f) + " ms in the past."); } frameTimeNanos = startNanos - lastFrameOffset;
但输入任务是有定时炸弹的,可以看到这篇文章:
-
后续就进入到了View的绘制流程。具体如何绘制的,如何通过局部重绘来优化性能的,我们在View的绘制中探讨。
参考文献
https://blog.csdn.net/qq_34519487/article/details/113030181
https://www.jianshu.com/p/86d00bbdaf60
https://blog.csdn.net/Android062005/article/details/123090139