Android framework : UI刷新机制:Vsync and Choreographer
慕课网 剖析framework 笔记
6-1 说说android的UI刷新机制
这个和界面优化有关系,卡顿会影响用户体验,
理解UI刷新机制对解决问题有帮助的
问题:
1,丢帧是什么原因引起的?
2,Android的刷新频率是60帧/s,是每隔16ms就调用onDraw绘制一次?
3,onDraw之后屏幕会马上刷新吗?
4,如果界面没有重绘,还会每隔16ms刷新屏幕嘛》?
5,如果屏幕块刷新时才去onDraw绘制,会丢帧吗?
AP从系统拿到buffer,画完后返回给系统,
系统服务把buffer写到缓冲区,屏幕以一定频率刷新,每次刷新读缓冲区并显示,
如果缓冲区没有新数据,屏幕就一直使用老的数据。
屏幕的图像缓存不只有一个,如果只有一个,一边读一边写,会导致tearing,一般第一帧,一半第二帧,
所以一般有2个或者以上,一直swap就行了
下个问题,AP端什么时候开始绘制
屏幕每次收到vsync就从buffer拿数据显示,
绘制 是AP端发起的,随时可能发起,
所以1st脉冲显示0帧图像,2nd脉冲显示1st帧图像,
第三个时钟周期还是显示1st帧,因为2nd帧还没有画完,没画完不是因为它画的时间长,是因为它画的太晚了,vsync快来才画。
如果这种现象发生的非常频繁,用户就能感觉到,界面非常卡顿。
优化:
如果绘制也和显示一个节奏就行了,
每帧图像的绘制都控制在16ms以内,就行了,
但是有一个问题,AP层的View的重绘调RequestLayout,
这个函数什么时刻都可以调用。,怎么控制它真正绘制的时机?? : Choreographer
Android怎么处理的、?关键是一个类:Choreographer,舞蹈指导,
你往里发一个消息,它最快也要下一个vsync来的时候触发,
如,绘制随时发起,封装一个Runnable,丢个Choreographer,下个vsync来,处理这个消息,开始界面重绘。
相当于绘制节奏完全由Choreographer控制,
看看Choreographer的原理:
从requestLayout说起,它我们比较熟,是用来发起UI重绘的,
public void requestLayout(){ ... scheduleTraversals();}void scheduleTraversals(){ //它没直接绘制,做了2件事情, //1,往线程消息队列插了一个syncBarrier mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier(); //2,往Choreographer消息队列插了一个callback mChoreographer.postCallback(Choregrapher.CALLBACK_TRAVERSAL, mTraversalRunnable, null); .....}
1,syncBarrier就是一个屏障,把屏障插入消息队列,屏障后面的普通消息就要等着,屏障撤出才可以处理,
但是屏障对异步消息没有作用,这么做的原因:
因为有些类型的消息非常紧急,要马上处理。如果消息队列里面普通消息过多,耽误事,所以插了屏障,优先处理异步消息。
这里往Choreographer丢的Runnnable就是一个异步消息,
下个 Vsync来的时候,异步消息是要紧急处理的。
2,Choreographer,是和ViewRootImpl一起创建的,
创建:
//有getInstance,是单例吗?不是public static Choreographer getInstance(){ //他是一个ThreadLocal,ThreadLocal //也就是说在不同的线程调用getInstance返回的是不同的Choreographer对象 return SThreadInstance.get();}
假如有人一口气调用了10次requestLayout,那么下次vsync到来前,会引发10次UI重绘吗?
不会
void scheduleTraversals(){ //每次判bool变量,是false才进,什么时候置的false?在mTraversalRunnable => doTraversal if(!mTraversalScheduled){ mTraversalScheduled = true; mChoreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null); .... }}//下次vsync来的时候执行doTraversal,里面会给bool置flasevoid doTraservsal(){ if(mTraversalScheduled){ mTraversalScheduled = false; performTraversals(); }}
看看这个callback是怎么加入到Choreographer的
private void postCallbackDelayedInternal(int callback Type, ...){ .... //Choreographer里面有一个数组叫mCallBackQueues,数组里每个元素都是一个callback单链表 //一方面要根据callback类型插入对应的单链表 //另一方面要根据callback要执行的时间顺序排序,dueTime,越是马上发生的callback越放到链表的前面 mCallbackQueue[callbackType].addCallbackLocked(dueTime, ...); .... scheduleFrameLocked(now);}private void scheduleFramelocked(long now){ //如果当前线程就是Choreographer的工作线程,直接调shceduleVsyncLocked if(isRunningOnLooperThreadLocked()){ scheduleVsyncLocked(); }else{ //否则要发消息到Choreographer的工作线程里面去, Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_VSYNC); //消息是异步消息,不受到屏障的影响, msg.setAsynchronous(true); //消息要插入到消息队列的头,非常紧急, //因为要告诉SF,下一个vsync来时,第一时间通知我们,因为如果错过这个vysnc,就又要等一个周期 mHandler.sendMessageAtFrontOfQueue(msg); }}
scheduleVsyncLocked后会发生什么?
是告诉SF,我们要关注下一个vsync信号了,
当下个vsync信号发生,SF通知我们,然后回调到FrameDisplayEventReciver里面的onVsync()
class FrameDisplayEventReceiver extends DisplayEventReciver implements Runnable{ @Override //timestamNanos是vsync信号的时间戳 public void onVsync(long timestampNanos, int builtInDisplayId, int frame){ .... mTimestampNanos = timestampNanos; mFrame = frame; //这个消息this其实是一个runnable,就是后面的run() Message msg = Message.obtain(mHandler.this); mgs.setAsynchronous(true); //发了一个消息到Choreographer的工作线程里面去了, //封装一个消息丢出去干嘛?它不是切换工作线程,因为onVsync本身就是调到Choreographer里面的, //这个mHandler也是,这里发消息丢出去,它带了一个时间戳,表示消息要触发的时间,这样就可以按照时间戳顺序处理消息。 mHandler.sendMessageAtTime(msg, timestampNanos/TimeUils.NANOS_PER_MS); } @Overide public void run(){ //到时间了,消息被处理,就是执行doFrame doFrame(mTimestampNanos, mFrame); }}
doFrame分两个阶段
//阶段1//frameTimeNanos是这帧的时间戳void doFrame(long frameTimeNanos, int frame){ 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){ Log.i(TAG, "Skipped" + skippedFrames + "frames! " + "the appliction may be doing too much work on main thread"); } ..... }}//阶段2,处理callback,//calback有4中类型,每种对应一个callback链表void doFrame(long frameTimeNanos, int frame){ .... doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos); doCallbacks(CHoreographer.CALBACK_ANIMATION, frameTImeNanos); doCAllbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos); doCallBacks(Choreographer.CALLLBACK_COMMIT, frameTimeNanos);'}//执行对应的doCallbacks,//callback有时间戳,到了时间才执行回调void doCallbacks(int callbackType, long frameTimeNanos){ CallbackRecord callbacks; //extractDueCallback作用就是从callbackQueue取出到了时间的callbacks, callbacks = mCallbackQueues[callbackType].extractDueCallbacksLocked(...); //for循环执行链表中的callback的run函数 for(CallbackRecode c = callbacks; c != null, c = c.next){ c.run(frameTimeNanos) }}
看看scheduleTraversals的时候传的callback是什么样的
//准备绘制,void scheduleTraversals(){ if(!mTraversalScheduled){ mTraversalScheduled = true; mChoreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null); .... }}//mTraversalRunnable,最后调的就是doTraversal() => performTraversals()final class TraversalRunnbale implements Runnable{ @Override public void run(){ doTraversal(); // => performTraversals,真正绘制的地方 }}
总结:
1,AP层的View调用requestLayout要求重绘,
2,其中是new了一个Runnable丢到Choreographer的消息队列,
3,Choreographer没马上处理消息,它是通过requestNextVsync向SurfaceFlinger请求下一个vSync信号,
4,SF会在下个vsync来的时候,通过postSyncEvent向choreographer发送一个通知,
5,choreographer收到通知之后就会处理消息队列里面的消息
6,之前的requestLayhout对应的Runnable里面执行的就是performTraversal,真正的执行绘制
下面看看scheduleVsync的实现
private void scheduleVsyncLocked(){ //会调到native的DisplayEventReceiver的shceduleVsync函数 mDisplayEventReceiver.scheduleVsync();}status_t NativeDisplayEventReceiver::scheuleVsync(){ status_t status = mReceiver.requestNextVsync();}status_t DisplayEventReceiver::requestNextVsync(){ //requestNextVsync,请求下一个vsync, //mEventConnection对象怎么创建的?在DisplayEventRecevier的构造函数创建的 mEventConnection->requestNextVsync();}DisplayEventReceiver::DisplayEventReceiver(){ //拿到SF的binder句柄:ISurfaceComposer sp sf(ComposerService::getComposerService()); if(sf != NULL){ //得到mEventConnection,这是另外一个binder句柄, //这种套路很常见,拿到系统服务binder句柄后,要么openSeesion,要么CreateConnection,总之要单独弄一个通道。 mEventConnection = sf->createDisplayEventConnection(); //Channel是connection创建时new的一个BitTube对象 mDataChannel = mEventConnection->getDataChannel(); }}sp EventThread::Connection::getDataChannel() const{ return mChannel;}
//这种套路很常见,拿到系统服务binder句柄后,要么openSeesion,要么CreateConnection,总之要单独弄一个通道。
//Channel是connection创建时new的一个BitTube对象
BitTube其实就是两个描述符,通过socketpair创建
mSendFd
mReceiverFd
机制:
和管道有点像,如果有个人拿到了读Fd-ReceiverFd,阻塞在这里,另一个人拿到SendFd 写Fd时,ReceiverFd就会被唤醒。
socketpair(AF_UNIX, SOCK_SEQPACKET, 0, sockets)
看看connection如何创建的
//它的实现在SF进程里面sp SurfaceFlinger::createDisplayEventConnection(){ //eventThread是一个线程:等待、处理event return mEventThread->createEventConnection();}sp EventThread::createEventConnection() const{ //new Connection and return return new Connection(const_cast(this));}EventThread::Connection::Connection(const sp&eventThread) //new BitTube :count(-1), mEventThread(eventThread),mChannel(new BitTube()){} void EventThread::Connection::onFirstRef(){ //向EventThread注册自己,这样EventThread有event的时候,就可以分发connection, //connection又调到我们应用进程里面 mEventThread->registerDisplayEventConnection(this);} //看看connection是怎么注册到eventThread的status_t EventThread::registerDisplayEventConnection(const sp & connection){ //把conection add到DisplayEventConnection列表 mDisplayEventConnections.add(connection); //发广播,类似java的notifyAll,对应的有wait才对, //wait是在eventThread里面。 mCondition.broadcast(); //notifyAll}
看看EventThread是在哪里创建的
void SurfaceFlinger::init(){ //vysnc信号源,传给了EventThread sp vsyncSrc = new DispSyncSource(&mPrimaryDispSync,..); //创建EventThread mEventThread = new EventThread(vsyncSrc); ...}//thread启动后不停的执行threadLoopbool EventThread::threadLoop(){ DisplayEventReceiver::Event event; Vector< spsignalConnections; //等待事件,返回Connection列表,里面都是event signalConnections = waitForEvent(&event); const size_t count = signalConnections.size(); //遍历connection for(size_t i=0; i & conn(signalConnections[i]); //通过postEvent分发事件,典型的如vsync事件 conn->postEvent(event); }}
看看waitForEvent函数,这个函数很长,细节很多
Vector> EventThread::waitForEvent(..){ Vector>signalConnections; do{ //看是否已经有vysnc信号来了,(不用关心vysnc怎么来,只关心怎么分发到应用层)) //有,就准备connection列表返回 //没有,等待vysnc }while(signalConnections.isEmpty()); return signalConnctions;}//注意void EventThread::requestNextVsync(const sp::Connection>&connection){ //connection里面有一个字段count,当count>=0时,才表示connection关注vsyncEvent, //所以应用端调用requestNextVsync就是把count从-1变成0, if(connection->count < 0){ connection->count = 0; mCondition.broadcast(); }}
当我们把connection加会signalConnections列表时,它有把count设置为-1.
这样你想接受下个vsync,就又要调一次requestNextVsync,把它重新置0
再看看事件如何分发出去
status_t EventThread::Connection::postEvent(const DispplayEeventReceiver::Event& event){ ssize_t size = DIslayEventReceiver::sendEvents(mChannel, &event, 1); return size < 0 ? statuc_t(size):status_t(NO_ERROR);}ssize_t DisplayEventReceiver::sendEvnets(const sp&dataChannel, Event const* events, size_t count){ return BitTube::sendObjects(dataChannel, events, count);}ssize_t BitTube::snedObjects(const sp& tube,...){ const char* vaddr = reinterpret_cast(events); ssize_t size = tube->wiret(vaddr, count *objSize); return size < 0 ? size:size/static_cast(objSize);}
BitTube就是socket管道,有两个fb,w/r,write就是写,那读的一端就会收到通知。
BitTube的描述符是在SF端创建的,写的一端是在SF进程,
1,那读的一端是如何传给应用进程的?
2,应用进程如何监听读FD的?
解答他们要看DislayEventRecevier的构造函数
DisplayEventRecevier::DisplayEventRecevier(){ sp sf(ComposerService::getComposerService()); mEventConnection = sf->createDisplayEventConnection(); //得到mDataChannel,它就是BitTube mDataChannel = mEventConnection->getDataCannel();}//看看BitTube如何跨进程传递//应用端拿到Connection的proxy端,getDataCannel把请求transact出去,virtual sp getDataChannel() const { Parcel data, reply; data.writeInterfaceToken(IDisplayEventConnection::getInterfaceDescriptor()); remote()->trascat(GET_DATA_CHANNEL, data, &reply); //BitTube根据reply把描述符还原出来, return new BitTube(reply);}//SF收到getDataChannel请求,会返回mChannel,Channel就是bitTube,//Channel放到parcel,跨进程传递给应用进程,放到reply里面sp EventThread::Connection::getDataChannel() const{ return mChannel;}
问题2,AP进程如何监听BitTube读事件?
要从Choreographer的构造函数说起,
private Choreographer(Looper looper){ ... //如果用vsync信号,就会创建DisplayEventReceiver,FrameDisplayEventReceiver继承了DisplayEventReceiver, mDisplayEventReceiver = new FrameDisplayEventReceiver(looper);}public DisplayEventReceiver(Looper looper){ .... //构造函数调到了nativeInit函数,它是native的函数, mRecevierPtr = nativeInit(new WeakReference(this),...);}static jlong nativeInit(JNIEnv* env, jclass clazz, jobject recevierWeak,...){ .... //native层,创建一个NativeDisplayEventReceiver对象 sp recevier = new NativeDisplayEventRecevier(...); //看看initialize status_t status = recevier->initialize(); return reinterpret_castgetLooper()->addFd(mRecevier.getFd(), 0, Looper::EVENT_INPUT, this, NULL); return OK; } DisplayEventReceiver::DisplayEventReceiver(){ //拿到SF的binder句柄:ISurfaceComposer sp sf(ComposerService::getComposerService()); if(sf != NULL){ //得到mEventConnection,这是另外一个binder句柄, //这种套路很常见,拿到系统服务binder句柄后,要么openSeesion,要么CreateConnection,总之要单独弄一个通道。 mEventConnection = sf->createDisplayEventConnection(); //Channel是connection创建时new的一个BitTube对象 mDataChannel = mEventConnection->getDataChannel(); }}int DisplayEventRecevier::getFd() const{ //前面讨论过,DataChannel就是BitTube return mDataChannel->getFd();}//BitTube有两个描述符,发送和接收,这里返回的是接收的,//发送的Fd在SurfaceFlinger,int BitTube::getFd() const{ return mReceiveFd;}
看看getlooer()->addFd,Fd如何添加到looper的,
int Looper::addFd(int fd, int ident, int events, ...){ Request request; request.fd = fd; .... //new了epoll_event,加入epoll struct epoll_event eventItem; request.initEventItem(&eventItem); int epollResult = epoll_ctl(mEpollFd, EPOLL_CTL_ADD, fd, &eventItem); //把request加到mRequest列表 mRequests.add(fd, request);}
看看Looper如何检测FD的读事件
int Looper::pollnner(int timeoutMillis){ ... //epoll_wait返回后在for里处理事件 int eventCount = epoll_wait(mEpollFd, eventItems,...); //有两种事件, for(int i = 0; i< eventCount; i++){ int fd = eventItems[i].data.fd; uint32_t epollEvents = eventItems[i].events; //1,消息队列的事件 if(fb == mWakeEventFd){ ... }else{//2,其他fd事件, if(epollEvents & EPOLLIN) events |= EVENT_INPUT; //放到response列表,for结束后,统一处理responses pushResponse(events, mRequestl.valueAt(requestIndex)); } } //统一处理Responses for(size_t i = 0 ; i < mResponses.size(); i++){ Response& response = mResponsese.editItemAt(i); int fd = response.request.fd; intevents = response.evdnts; void *data = responose.request.data; //调用callback的handleEvent,返回值很重要, int callbackResult = response.request.callback->handleEvent(fd, events, data); if(callbackResult == 0){ //返回0则删除这个fd removeFd(fd, response.request.seq); } response.request.callback.clear(); result = POLL_CALLBACK; } return result;}
我们添加的BitTube的Fd的回调是什么?
int NativeDisplayEventRecevier::handleEvent(int receiveFd, int events,...){ //把SF发的event读进来, if(processPendingEvents(&vsyncTimestamp, &vsyncDisplayId, ...)){ mWaitingForVsync = false; //分发vysnc, dispatchVsync(vsyncTimestamp, vsyncDisplayId, vysncCount); } //返回1,Fd会一直被Looper监听,不会删除掉。 return 1;}void NatvieDisplayEventRecevier::dispatchVsync(nsecs_t timestamp,...){ //jni调用,调到java层的dispatchVysnc env->CallVoidMethod(recevierObj.get()),gDisplayEventReceiverClassInfo.dispatchVsync, timestamp,...);}void displatchVsync(long timestampNanos, int builtInDisplayId, int frame){ //前面讲过,是处理Choreographer的callback的 onVysnc(timestampNanos, builtInDisplayId, frame);}
回到问题:
1,丢帧原因? mainthread有耗时操作,耽误了View的绘制
2,android的刷新频率是60帧/s,那每隔10ms调onDraw绘制一次?
60是vsync的频率,但不是每个vsync都发起绘制,需要AP端主动发起重绘,才会向SF请求接收Vsync信号,才会再下个vsync来的时候绘制。
3.onDraw后屏幕立刻刷新? 不是,下一个vsync来的时候花心
4,如果界面没有重绘,还会每隔17ms刷新屏幕? 会的,只是数据一直用的旧的
5,如果快刷新时才绘制会丢帧吗?
View重绘不会被马上执行,都是下个vsync来才开始的,所以何时发起重绘没什么关系
总结,述说Android的UI刷新机制,如何回答:
1,vsync的原理
2,Choreographer的原理
3,UI刷新的大致流程,应用和SurfaceFlinger的通信过程。
更多相关文章
- 史上最全Android开发资料:资源、UI、函数库、测试、构建全套教程
- Android Handler消息队列的实现原理
- 【android】手写一套Java的Handler程序,深入理解Android消息机制
- android异步线程利用Handler将消息发送至UI线程
- Android消息机制原理,仿写Handler Looper源码跨线程通信原理--之
- Android日志消息的生成详细步骤
- Android应用程序键盘(Keyboard)消息处理机制分析(16)
- android 测试读取LEB数据的函数