系列文章索引
并发系列:线程锁事
新系列:Android11系统源码解析
-
Android11源码分析:binder是如何实现跨进程的?(创作中)
-
Android11源码分析:SurfaceFlinger是如何对vsync信号进行分发的?(创作中)
经典系列:Android10系统启动流程
前言
前几年,Android经常被吐槽不如IOS系统流畅,给人的感觉就是一卡一卡的
直到近一两年Android高刷屏开始普及,才从体验上真正有了一个质的提升
针对UI刷新的优化,系统层也一直在做相关的工作
今天我们从应用层的是使用出发,去深挖下源码,看看UI卡顿的根源到底是什么
下面,正文开始!
注: 本文源码基于
android11.0.0-r2
,保证逻辑清晰,部分源码有删减
UI是怎么刷新的?从requestLayout()
说起
在应用层想要刷新UI,触发重绘,只需要调用requestLayout()
函数即可
最终会调用的ViewRootImp
的requestLayout()
函数,执行scheduleTraversals()
方法
这里就涉及到一个问题,假如短时间重复的调用requestLayout()
,会使屏幕不断刷新吗?
在这个函数里,其实做了过滤处理,设置mTraversalScheduled
标识是否执行了Traversal的流程,如果没有执行完成,则一直是ture的状态,直到接收到VSYNC信号,对当前的Traversal流程执行完毕,才会将其设置为false
也就是说,在一个VSYNC信号的周期范围内,只会触发一次重绘的流程
具体代码如下
/frameworks/base/core/java/android/view/ViewRootImpl.java
void scheduleTraversals() {
if (!mTraversalScheduled) { //判断是否执行scheduleTraversals内的逻辑,确保在一个vsync时钟周期内只出发一次scheduleTraversals
mTraversalScheduled = true; //如果执行过则置为ture,避免多次调用requestlayout时重复出发的情况
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();//发送屏障消息(对异步消息没有影响)
mChoreographer.postCallback( //post一个异步消息,等vsyn信号来的时候优先进行处理
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);//mTraversalRunnable会在下一个vysc信号来的时候执行
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}
在这个函数中,调用了Choregrapher
的postCallback()
函数注册VSYNC信号的回调消息,其中真正处理VSNC事件的是mTraversalRunnable
,其中经过doTraversal()
--> performTraversals()
的函数调用对view进行重新绘制
此处我们先暂时忽略绘制的细节,继续跟进postCallback()
函数看看他是怎么注册Vsync
信号回调的
在Choregrapher
中,最终调用到了postCallbackDelayedInternal()
其中使用维护了一个集合mCallbackQueues
用来存储不同类型(callbackType
)的CallbackQueue
其中CallbackQueue
使用`单向链表存储,并按照需要执行的先后顺序进行排列
如果callback的执行时间戳小于当前时间,则调用scheduleFrameLocked()
去请求vsync信号触发绘制
如果大于当前时间则通过handler发送异步延迟消息稍后处理
具体代码如下
/frameworks/base/core/java/android/view/Choreographer.java
private void postCallbackDelayedInternal(int callbackType,
Object action, Object token, long delayMillis) {
//...
synchronized (mLock) {
final long now = SystemClock.uptimeMillis();
final long dueTime = now + delayMillis;
//通过callbackType获取到对应的mCallbackQueues,再根据dueTime插入到合适的位置,按时间顺序进行排序
mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);
if (dueTime <= now) {
scheduleFrameLocked(now);//最终执行到DisplayEventReceiver的requestNextVsync函数,注册vsync信号
} else {
Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
msg.arg1 = callbackType;
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, dueTime);
}
}
}
我们先跟踪需要立即执行的分支,看看它是如何请求vsync信号的
这里需要先提一下Choregrapher
的创建,是通过getInstance()
函数获取的,但其中并不是单利的实现,而是用ThreadLocal
存储的线程本地变量
在初始化时,会调用Looper.myLooper()
对Looper进行初始化,如果获取到的Looper对象是主线程的Looper,则将其Choregrapher
实例保存在mMainInstance
中
也就是说,不同的线程,创建的Choregraher
对象也不一样
Choregrapher
的初始化代码如下
// Thread local storage for the choreographer. choregrapher为线程本地变量
private static final ThreadLocal<Choreographer> sThreadInstance =
new ThreadLocal<Choreographer>() {
@Override
protected Choreographer initialValue() {
Looper looper = Looper.myLooper(); //创建当前线程的Looper对象
//...
Choreographer choreographer = new Choreographer(looper, VSYNC_SOURCE_APP);//给应用层处理的VSYNC信号
if (looper == Looper.getMainLooper()) { //如果looper为主线程looper,则将choreographer设置mMainInstance
mMainInstance = choreographer;
}
return choreographer;
}
};
再回到vysnc信号的注册流程中来,这里会判断Chorographer
所在的线程是否是它的工作线程(根据是否是同一个Looper判断,注意是工作线程,不是主线程),如果是在异步线程中,则需要通过hander切换到工作线程去执行
代码如下
/frameworks/base/core/java/android/view/Choreographer.java
private void scheduleFrameLocked(long now) {
if (!mFrameScheduled) {
mFrameScheduled = true;
if (USE_VSYNC) {
if (isRunningOnLooperThreadLocked()) {//如果当前线程是Chorographer的工作线程(根据是否是同一个线程的Looper来判断)
scheduleVsyncLocked();
} else {
Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_VSYNC); //发送消息到主线程执行
msg.setAsynchronous(true); //发送异步消息
mHandler.sendMessageAtFrontOfQueue(msg);
}
}
//略...
}
}
我们这里暂时关注在工作线程的情况,这里会调用到scheduleVsyncLocked()
函数,交给mDisplayEventReceiver
的scheduleVsync()
函数
此处的mDisplayEventReceiver
是在Choreographer
构造初始化的时候创建的,对应的类FrameDisplayEventReceiver
是DisplayEventReceiver
的子类对象
其初始化过程又调用了nativeInit()
创建出对应的native层的DisplayEventReceiver
对象,真正的函数调用是在这个类中实现的,scheduleVsync()
函数也是交给native层去实现的,
其中会调用native层中会调用requestNextVsync()
函数,请求下一个vsync信号,这里又涉及到另一个数据结构,DisplayEventConnection
这里面用到的mEventConnection
是在DisplayEventReceiver
的构造中通过SurfaceFlinger的binderProxy对象调用到createDisplayEventConnection()
创建的
在SurfaceFlinger
中,又调用到了Scheduler
的createDisplayEventConnection()
,最终的创建交给了EventThread
来执行,代码如下
/frameworks/native/services/surfaceflinger/Scheduler/EventThread.cpp
sp<EventThreadConnection> EventThread::createEventConnection( //创建Connection对象(其实是一个bpbinder)
ResyncCallback resyncCallback, ISurfaceComposer::ConfigChanged configChanged) const {
return new EventThreadConnection(const_cast<EventThread*>(this), std::move(resyncCallback),
configChanged);
}
Connection对应到的数据结构是EventThreadConnection
,这个connection其实是一个binder的实体对象,用来跨进程传输的(在系统层和应用层之间传输), EventThread
会将connection维护在一个mDisplayEventConnections
集合中
EventThread
是在SurfaceFlinger
中创建的,创建后会在在其工作线程(threadMain)循环获取mDisplayEventConnections
中需要处理的connection回调,并调用dispatchEvent()
函数向DisplayEventReceiver
分发vsync事件,这里通过调用postEvent()
-->sendEvents()
进行事件的分发,此处使用BitTue
进行写操作,持有receiveFd
的一端即可收到消息进行读操作
/frameworks/native/libs/gui/DisplayEventReceiver.cpp
ssize_t DisplayEventReceiver::sendEvents(gui::BitTube* dataChannel,
Event const* events, size_t count)
{ //此处使用BitTube通信,类似于管道,一个读描述符,一个写描述符,此处通过bitTube写入event数据,读的一端即可收到event数据
return gui::BitTube::sendObjects(dataChannel, events, count);
}
此时,receiver便能收到注册的vsync信号的回调,执行到应用端FrameDisplayEventReceiver
的onVsync()
函数进行绘制了
总结
前面分析了vsync信号的分发和接收的原理,在一个sync时钟周期内,只会触发一次重绘操作
整体上看,vsync的事件分发涉及应用端(Client)和服务端(SurfaceFlinger)两个进程的交互,SurfaceFlinger接收到硬件的vsync信号后,通过应用端注册的eventConnection,将其分发给应用端,处理具体的绘制流程
在应用端(Client),又涉及到java层和native层的通信,这里主要是在DisplayEventReceiver
的实现中有涉及,通过native层调用去请求sync信号
在服务端(SurfaceFlinger)会不断进行loop循环取到应该执行的回调函数进行vsync信号的分发,最终分发给客户端
UI为什么会卡顿?
分析完屏幕的重绘流程,我们再回过头来看,UI卡顿的原因,其实是因为在一个vsync时钟周期内没能完成绘制导致的
在开发过程中,一般是由于在主线程执行了大量的block操作导致的;另外也可能由于频繁创建内存导致频繁GC而导致主线程无法执行绘制的卡顿情况
在系统层,如果某一帧执行的绘制执行时间过长,就会产生跳帧
的现象,如果绘制时间超过了30帧*16.6mills,系统会打印日志提示可能在主线程执行了过多的耗时操作
在activity中执行耗时操作超过5s会触发ANR(在ActivityThread中进行处理,弹出Dialog框进行提示)
如何解决UI卡顿?
以上说了卡顿在应用层面的两点原因
-
主线程block导致卡顿
-
频繁GC导致卡顿
针对到具体的优化业务中时,首先要分析是哪里产生了block导致卡顿。
比如四大组件的各个生命周期都是在主线程执行的,就要注意不要在主线程执行耗时操作,而是放到异步线程中执行
针对频繁GC导致的卡顿,一般是重复调用某个占用内存大的函数导致的,比如view的ondraw方法中不要频繁创建大内存的对象
如何监测UI卡顿?
针对卡顿监测,大概有以下几种思路可以做
-
通过替换主线程Looper的日志打印,计算事件的完成事件,如果超过了16mills,说明发生了卡顿问题
-
通过
Choregrapher
的getFps()
获取到当前的帧率 -
要测量某个函数在主线程中耗时的时常,可以使用AOP切面进行无侵入的监测,计算各个函数的耗时
-
在发生ANR后,可以通过
traces.text
日志进行分析
写在最后
今天针对屏幕刷新机制进行了详细的源码探究,针对设备的不同,block的节点可能也会不同
在发生卡顿问题时,要分析性能的瓶颈点是在什么地方,先针对大量耗时的地方进行优化,再去针对细致的地方进行分析
我是释然,我们下期文章再见!