相关几个问题
- 丢帧一般是什么原因引起的?
- Android刷新频率60帧/秒,每隔16ms调ondraw绘制一次?
- onDraw完之后屏幕会马上刷新么?
- 如果界面没有重绘,还会每隔16ms刷新屏幕么?
- 如果屏幕快要刷新的时候才去onDraw绘制会丢帧么?
屏幕显示原理
- 首先应用从系统服务申请一段buffer,
- 然后系统服务返回这个buffer
- 应用在收到这个buffer之后就可以进行绘制,
- 绘制完之后就提交给系统服务
- 然后系统服务把这个buffer写到屏幕的缓冲区里
- 屏幕会以一定的频率刷新,刷新的时候就会从缓冲区里把图像数据读出来然后显示出来
- 如果缓冲区里没有新数据,那么屏幕就会一直用老的数据
屏幕有两个缓存,一个用来显示,另一个用来写入,当屏幕要显示下一张图像的时候就把两个缓存交换:
应用端是什么时候开始绘制的?
屏幕是根据Vsync信号周期性刷新的,vsync是一个固定频率的脉冲信号。屏幕每次收到一个vsync信号就会从缓冲区里读取一帧数据出来显示。而绘制是由应用端发起的,应用随机都可能发起绘制。上图中:
第一个脉冲周期:屏幕上显示的是第0帧图像;
第二个脉冲周期:屏幕上显示的是第一帧,第一个vsync信号来的时候第一帧已经画完了;
第三个脉冲周期:还是显示第一帧,因为第二个vsync信号来的时候第二帧还没画完。第二帧没画完并不是因为第二帧绘制太耗时,其实第二帧优化得还不错,它的耗时远远小于一个时钟周期,但是它比较倒霉,它是在vsync信号快来的时候才开始绘制的,因此它只能等到下一个周期,但第三个vsync信号来的时候第三帧已经画完了,因此第2帧会丢帧。
如果这种情况频发地发生,那么用户会明显感觉到卡顿,就算应用层优化得再好也没用, 因为这是底层刷新机制的缺陷导致的。
这种情况如何优化呢?
其实很简单,如果绘制也能跟vsync一个节奏的话那这个问题就可以解决了。
每次vsync信号过来的时候,咱们一方面屏幕获取图像数据刷新界面,另一方面应用开始绘制来准备下一帧数据,如果应用优化得好,使得每次绘制都能控制在16ms以内,那么显示就会非常流畅。
但是有一个问题:应用中的重绘一般都是调用requestLayout(),这个函数随时都能调用,怎么能够让它受你的控制让它只在vsync信号来的时候才开始重绘呢?Android系统是怎么做的呢?有一个关键的类:Choreographer
Choreographer是什么?
Choreographer 翻译过来就是舞蹈指导,它有什么用呢?它最大的作用是:你往里面发一个消息,这个消息最快也要等到下一个vsync信号来了才能触发。比如说,绘制是随时发起的,你封装一个runnable丢给Choreographer,然后下一个vsync信号来的时候就开始处理消息,然后就开始真正的界面重绘——相当于UI绘制的节奏完全由Choreographer来控制。下面说说Choreographer底层的实现原理。
Choreographer的实现原理
//这个函数主要用于发起UI重绘
public void requestLayout(){
...
scheduleTraversals();
}
void scheduleTraversals(){
//往消息队列里插了一个syncBarrier
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
//往Choreographer队列里插了一个CALLBACK_TRAVERSAL
mChoreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
...
}
先说说syncBarrier:
这其实是一个消息屏障,把这个屏障插到消息队列里之后,它后面的消息就不能处理,一直等到这个屏障撤除之后才能处理。但是这个屏障对异步消息是没有影响的。为什么需要这样一种机制呢?主要是因为有些消息的类型非常紧急必须马上处理,如果队列中的普通消息太多是非常耽误事的,所以这里插了一个屏障,把普通消息先挡在一边,优先处理这些异步消息。比如我们往Choreographer里丢的这个runnable它其实就是一个异步消息,等到下一个vsync信号来的时候这个异步消息是在紧急处理的。
mChoreographer的创建
这是跟ViewRootImpl一起创建的
public static Choreographer getInstance(){
return sThreadInstance.get();
}
这个getInstance看起来像是一个单例,但其实不是!sThreadInstance是一个ThreadLocal,也就是说你在不同的线程去调用getInstance()的时候它返回的是不同的Choreographer对象。
再说requestLayout()
如果有人一口气调用了10次requestLayout(),那么是否意味着到下一次vsync到来的时候会触发10次重绘呢?当然不是!
void scheduleTraversals(){
//mTraversalScheduled是一个bool变量,只有当它为false的时候才会向Choreographer里PostCallback
if(!mTraversalScheduled) {
//另外会在doTraversal里把mTraversalScheduled 置成false
mTraversalScheduled = true;
mChoreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
...
}
}
void doTraversal(){
if(mTraversalScheduled) {
mTraversalScheduled = false;
performTraversals();
}
}
- 强调一个mTraversalRunnable是在下一次vsync信号来的时候才会被执行的
- 只有在mTraversalScheduled 设置成false之后才代表它会接受新传进来的requestLayout
- 因此我们可以看到,在一个vsync周期里,最多只会有一个requestLayout有效,最终只触发一次界面重绘
callBack是如何加到Choreographer里的?
private void postCallbackDelayedInternal(int callbackType, ...) {
...
//mCallbackQueues中每个元素都是一个callback单链表
//这一方面要根据callbackType插入到对应的callback单链表里
//另一方面要根据执行的时间顺序来排序:越是马上要发生的callback越要排在链表的前面
mCallbackQueues[callbackType].addCallbackLocked(duTime, ...);
...
scheduleFrameLocked(now);
}
private void scheduleFrameLocked(long now) {
if (isRunningOnLooperThreadLocked()) {
//如果当前线程就是Choreographer的工作线程,直接调用scheduleVsyncLocked
scheduleVsyncLocked();
}else{
//否则发一个MSG_DO_SCHEDULE_VSYNC消息到Choreographer的工作线程中
Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_VSYNC);
msg.setAsynchronous(true);
mHandler.sendMessageAtFrontOfQueue(msg);
}
}
请注意这是一个异步消息,它不受屏障消息的影响,而且是插到队列的头部,可见这个消息非常紧急————因为要马上告知SurfaceFlinger:在下一个vsync信号来的时候一定要第一时间通知我,如果错过了这个vsync,绘制就得多等一个周期。
发过多MSG_DO_SCHEDULE_VSYNC之后将发生什么呢?
其实是要告诉SurfaceFlinger:我要关注下一个vsync信号。因此当下一个vsync到来的时候SurfaceFling就会通知我们并回调到FrameDisplayEventReceiver 的onVsync函数
class FrameDisplayEventReceiver extends DisplayEventReceiver implement Runnable {
@Override
public void onVsync(long timestampNanos, int builtInDisplayId, int frame) {
//timestampNanos是vsync信号的时间戳
...
mTimestampNanos = timestampNanos;
mFrame = frame;
Message msg = Message.obtain(mHandler, this);
msg.setAsynchronous(true);
//封装一个消息发到Choreographer的工作线程中,这并非切换工作线程:此时onVsync已经是在Choreographer的工作线程中mHandler也是。
//timestampNanos表示消息要触发的时间,有了这个时间戳就可以按时间戳的顺序来处理消息
//时间到了之后将调用下面的run函数
mHandler.sendMessageAtTime(msg, timestampNanos/TimeUtils.NANOS_PER_MS);
}
@Override
public void run() {
doFrame(mTimestampNanos, mFrame);
}
}
void doFrame(long frameTimeNanos, int frame) {
//frameTimeNanos表示这一帧的时间戳
long intendedFrameTimeNanos = frameTimeNanos;
long startNanos = System.nanoTime();
//先计算当前时间与时间戳有多大,结果越大表示这一帧已经延时得越厉害
final long jitterNanos = startNanos - frameTimeNanos;
//如果延时超过一个周期
if (jitterNanos >= mFrameIntervalNanos) {
//计算一个延时了多少个周期
final long skippedFrames = jitterNanos/mFrameIntervalNanos;
if (skippedFrames >= SKIPPED_FRAME_WARNING_LIMIT) {
//如果跳过的帧数大于某个阀值(默认30),将打印一个警告:
//应用在主线程做太多事情,导致把绘制耽误了,跳过了好多帧
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;
}
//处理callback:前面讲过有4种Callback,第种callback对应一个callbackQueue,也就是单链表
//这里把vsync信号分别发给4种callback,然后执行对应的doCallbacks函数
doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos);
doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);
doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);
doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos);
}
void doCallbacks(int callbackType, long frameTimeNanos) {
CallbackRecord callbacks;
//CallbackQueues中的callback是有时间戳的,只有时间到了才会执行对应的callback
//extractDueCallbacksLocked是从mCallbackQueues中取出那些到执行时间的callback
callbacks = mCallbackQueues[callbackType].extractDueCallbacksLocked(...);
for (CallbackRecord c = callbacks; c != null; c = c.next) {
//分别执行callback的run函数
c.run(frameTimeNanos);
}
}
再回顾一个前面scheduleTraversals传的callback是什么样的:
void scheduleTraversals(){
if(!mTraversalScheduled){
mTraversalScheduled = true;
//mTraversalRunnable见下面
mChoreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
...
}
}
final class TraversalRunnable implements Runnable{
@Override
public void run() {
//最终调用的是performTraversal:这才是真正执行绘制的函数!
doTraversal();
}
}
总结一下前面讲的流程
- 首先应用层的View调用了requestLayout要求重绘,其实是new了一个runnable丢到choreographer的队列里
- choreographer并没有马上去处理这个消息,它先通过requestNextVsync向SurfaceFlinger请求下一个vsync信号
- SurfaceFlinger会在下一个vsync信号来的时候通过postSyncEvent向choreographer发送通知
- choreographer收到通知后就会去处理消息队列里的消息
- 之前的requestLayout对应的runnbale执行的是performTraversal()函数去真正执行绘制。
scheduleVsyncLocked的实现
这个函数是用来告诉SurfaceFlinger:下一个vsync信号来的时候通知我
private void scheduleVsyncLocked() {
mDisplayEventReceiver.sheduleVsync();
}
//NativeDisplayEventReceiver是mDisplayEventReceiver在native层的一个对象
sttatus_t NativeDisplayEventReceiver::scheduleVsync() {
...
status_t status = mReceiver.requestNextVsync();
...
}
status_t DisplayEventReceiver::requestNextVsync() {
//请求下一个vsync
mEventConnection->requestNextVsync();
}
mEventConnection是啥?看看它的创建:
DisplayEventReceiver::DisplayEventReceiver(){
//首先拿到suffaceFlinger的binder句柄
sp<ISurfaceComposer> sf(ComposerService::getComposerService());
if(sf!=NULL){
//得到另一个句柄
//这种套路很常见:比如说拿到服务服务的binder句柄之后要么onpenSession,要么去create一个Connection
//总之就是单独搞一个通道!
mEventConnection = sf->createDisplayEventConnection();
//这个channel是在Connection创建的时候new的的一个BitTube对象,见下面
mDataChannel = mEventConnection->getDataChannel();
}
}
sp<BitTube> EventThread::Connection::getDataChannel() const {
return mChannel;
}
BitTube
socketpair(AF_UNIX, SOCK_SEQPACKET, 0, sockets);
- BiTube其实就是通过socketpair创建的两个描述符:mSendFd/mReceiverFd
- mSendFd用于发送,mReceiverFd用于接收
- 往mSendFd中写数据,将可从mReceiverFd中读出——跟管道类似
Connction是如何创建的
在SurfaceFlinger进程中
sp<IDisplayEventConnection> SurfaceFlinger::createDisplayEventConnection(){
//EventThread就是一个不停等待并处理event的thread
//vsync信号就是它要等待的event之一
return mEventThread->createEventConnection();
}
sp<EventThread::Connection> EventThread::createEventConnection() const {
return new Connection(const_cast<EventThread*>(this));
}
EventThread::Connection::Connection(const sp<EventThread>& eventThread)
:count(-1), mEventThread(eventThread), mChannel(new BitTub()){
}
void EventTHread::Connection::onFirstRef(){
//connection把自己注册到eventThread中,
//这样当eventThread中有事件的时候就能分发到connection,connection就能调到应用进程了
mEventThread->registerDisplayEventConnection(this);
}
conncetion的是如何注册到EventThread的:
status_t EventThread::registerDisplayEventConnction(const sp<EventThread::Connection>& connection){
mDisplayEventConnections.add(connection);
//类似java中的notifyAll:
mCondition.broadcast();
}
//EventThread的创建
void SurfaceFlinger::init() {
//创建vsync的信号源
sp<VsyncSource> vsyncSrc = new DispSyncSource(&mPrimaryDispSync, ...);
mEventThread = new EventThread(vsyncSrc);
...
}
//EventThread启动之后将会不停地执行threadLoop函数
bool EventThread::threadLoop(){
DisplayEventReceiver::Event event;
Vector< sp<EventThread::Connection>> signalConnections;
signalConnections = waitForEvent(&event);
const size_t count = signalConnections.size();
for (size_t i=0; i<count; i++){
const sp<Connection>& conn(signalConnections[i]);
//分发事件:典型的就是vsync事件
conn->postEvent(event);
}
return true;
}
waitForEvent
Vector<sp<EventThread::Connection>> EventThread::waitForEvent(...){
Vector<sp<EventThread::Connection>> signalConnections;
do{
//看是否已经有vsync信号来了
//如果有的话,就uwtlconnection列表返回
//如果没有的话,就等待vsync信号
...
}while(signalConnections.isEmpty());
return signalConnections;
}
```c
void EventThread::requestNextVsync(const sp<EventTHread::Connection>& connection){
if(connection->count < 0) {
connection->count = 0;
mCondition.broadcast();
}
}
- 注意connection中有一个count字段,只有count>=0的时候才表示它关注vsync事件,
- 应用端调用requestNextVsync的时候其实就是把其中的count从-1改成0
- 当我们把connection加到signalConnections列表中的时候又自动把count设置-1
- 如果想要断续关注vsync信号应得重新调用一次requestNextVsync函数
事件是如何通过Connection分发出去的
status_t EventThread::Connection::postEvent(const DisplayEventReceiver::Event& event{
size_t size = DisplayEventReceiver::sendEvents(mChanel, &event, 1);
return size < 0? status_t(size):status_t(NO_ERROR);
}
size_t DisplayEventReceiver::sendEvetns(const sp<Bittube>& DataChannel, Event const* events, size_t count){
return BitTube::sendObjects(dataChannel, events, count);
}
size_t BitTube::sendObjects(const sp<BitTube>& tube, ...){
const char* vaddr = reinterpret_cast<const char*>(events);
//bitTube像管道,有两个描述符一个读一个写,这里往管道的一端写,读的另一端将收到通知
ssize_t size = tube->write(vaddr, count*objeSize);
return size < 0? size:size/static_cast<size_t>(objSize);
}
两个问题:
- bitTube中的读句柄是如何传到应用端的
- 应用是如何监听读事件的
bitTube中的读句柄是如何传到应用端的
回到DisplayEventReceiver的构造函数:
DisplayEventReceiver::DisplayEventReceiver(){
sp<ISurfaceComposer> sf(ComposerService::getComposerService());
mEventConnection = sf->createDisplayEventConnection();
mDataChannel = mEventConnection->getDataChannel();
}
//应用端:
virtual sp<BitTube> getDataCannel() const {
Parcel data, reply;
//发出获取BitTube请求
data.writeInterfaceToken(IDisplayEventConnction::getInterfaceDescriptor());
remote()->transact(GET_DATA_CHANNEL, data, &reply);
//不原出BitTube
return new BitTube(reply);
}
//SurfaceFlinger端
sp<BitTube> EventThread::Connection::getDataChannel() const{
//直接返回mChannel
return mChannel;
}
应用是如何监听读事件的
private Choreographer(Looper looper){
...
//FrameDispalyEventReceiver继承了DesplayEventReceiver
mDisplayEventReceiver = new FrameDispalyEventReceiver(looper);
}
public DisplayEventReceiver(Looper looper) {
...
mReceiverPtr = nativeInit(new WeakReference<DisplayEventReceiver>(this);
}
static jLong nativeInit(JNIEnv* env, jclass clazz, jobject receiverWeak, ...){
...
sp<NativeDisplayEventReceiver> receiver = new NativeDispalyEventReceiver(...);
status_t status = receiver->initialize();
return reinterpret_cast<jlong>(receiver.get());
}
status_t NativeDisplayEventReceiver::initialize(){
//往Looper中添加一个描述符,就是让Looper一起监听读事件
//this是一个调回,表示当迷个fd有可读元事件的时候将触发这个回调
mMessageQueue->getLooper()->addFd(mReceiver.getFd(), 0, Looper::EVENT_INPUT, this, NULL);
return OK;
}
DisplayEventReceiver::DisplayEventReceiver(){
sp<ISurfaceComposer> sf(ComposerService::getComposerService());
mEventConnection = sf->createDisplayEventConnection();
mDataChannel = mEventConnetion->getDataChannel();
}
int DisplayEventReceiver::getFd() const {
return mDataChannel->getFd();
}
int BitTube::getFd() const {
return mReceiverFd;
}
mReceiverFd是如何添加到Looper中的:
int Looper::addFd(int fd, int ident, int events, ...){
Request request;
request.fd = fd;
...
struct epool_event eventItem;
request.intEventItem(&eventItem);
int epollResult = epoll_ctl(mEpoolFd, EPOLL_CTL_ADD, fd, & eventItem);
mRequests.add(fd, request);
}
Looper是怎么检测fd的读事件的:
int Looper::pollInner(int timeoutMillis){
...
int eventCount = epoll_wait(mEpoolFd, eventItems, ...);
for(int i = 0; i < eventCount; i++){
int fd = eventItems[i].data.fd;
uint32_t epollEvents = eventItems[i].events;
if(fd == mWakeEventFd){
//消息队列的事件
...
}else{
if(epollEvents&EPOLLIN) events |= Event_INPUT;
//先放到队列中,等for循环结束后统一处理
pushResponse(events, mRequests.valueAt(requestIndex));
}
}
//统一处理Responses
for (size_t i = 0; i < mResponses.size(); i++){
Response& response = mResponses.editItemAt(i);
int fd = response.request.fd;
int events = response.events;
void* data = response.request.data;
//调用每一个callback的handleEvent函数
int callbackResult = response.request.callback->handleEvent(fd, events, data);
if(callbackResult == 0){
//callback的返回值很重要,如果返回0,将把fd从移除,不再检测
removeFd(fd, response.request.seq);
}
response.equest.callback.clear();
result = POLL_CALLBACK;
}
return result;
}
前面BitTume添加的回调:
int NativeDisplayEventReceiver::handleEvent(int receiveFd, int events, ...){
//把SurfaceFlinger发过来的事件读出来
if(processPendingEvents(&vsyncTimestamp, &vsyncDisplayId, ...)){
mWaitingForVsync = false;
//把它分发出去
dispatchVsync(vsyncTimestamp, vsyncDisplayId, vsyncCount);
}
//返回1,表示将继续监听fd的事件
return 1;
}
void NativeDisplayEventReceiver::dispatchVsync(nsecs_t timestamp, ...){
//调用Java函数
env->CallVoidMethod(receiverObj.get(), gDisplayEventReceiverClassInfo.dispatchVsync, timestamp, ...);
}
void dispatchVsync(long timestampNanos, int builtInDisplayId, int frame){
//处理Choreographer的callback
onVsync(timestampNanos, builtInDisplayId, frame);
}
回归:回答这几个问题?
- 丢帧一般是什么原因引起的?
a)主线程耗时操作耽误了View的绘制,所以丢帧了 - Android刷新频率60帧/秒,每隔16ms调用onDraw绘制一次?
a)刷新频率其实是批vsyn信号发生的频率,但并不是每一次都会去绘制
b)需要应用端先主动发起重绘,才会向SurfaceFlinger请求接收vsync信号
c)等到下次vsync信号来的时候都会真正去绘制 - onDraw完之后屏幕会马上刷新么?
a)onDraw完了之后不会马上刷新,要等到下次vsync信号来的时候才刷新 - 如果界面没有重绘,还会每隔16ms刷新屏幕么?
a)如果没有重绘,应用就不会收到vsync信号,但是屏幕还是会以每秒60帧的频率刷新,只不过画面数据一直用的是旧的,所以看起来没什么变化。 - 如果在屏幕快要刷新的时候才去绘制会丢帧么?
a)代码中发起的重绘不会马上执行,等到下次vsync信号来的时候才开始,因此什么时候发起绘制并没有太大的关系
回归:说说Android的UI刷新机制
- Vsync的原理是怎样的?
- Choreographer的原理是怎样的?
- UI刷新的大致流程,应用和SurfaceFlinger的通信过程?