javaee论坛

普通会员

225648

帖子

335

回复

349

积分

楼主
发表于 2019-11-03 06:38:22 | 查看: 282 | 回复: 2

文章目录概述源码ViewRootImpl与DecorView的绑定ViewRootImpl#scheduleTraversalsChoreographer过滤一帧内重复的刷新请求同步屏障消息postSyncBarrier()刷新控制者ViewRootImpl总结常见问题

概述

屏幕刷新包括三个步骤:

CPU计算屏幕数据,,把计算好数据交给GPU。GPU会对图形数据进行渲染,渲染好后放到buffer里存起来。接下来display负责把buffer里的数据呈现到屏幕上。

显示过程,简单的说就是CPU/GPU准备好数据,存入buffer,display每隔一段时间去buffer里取数据,然后显示出来。display读取的频率是固定的,比如每个16ms读一次,但是CPU/GPU写数据是完全无规律的。

对于Android而言:CPU计算屏幕数据指的也就是View树的绘制过程,也就是Activity对应的视图树从根布局DecorView开始层层遍历每个View,分别执行测量、布局、绘制三个操作的过程。

也就是说,我们常说的Android每隔16.6ms刷新一次屏幕其实是指:底层以固定的频率,比如每16.6ms将buffer里的屏幕数据显示出来。

Display这一行可以理解成屏幕,所以可以看到,底层是以固定的频率发出VSync信号的,而这个固定频率就是我们常说的每16.6ms发送一个VSync信号,至于什么叫VSync信号,我们可以不用深入去了解,只要清楚这个信号就是屏幕刷新的信号就可以了。

CPU蓝色的这行,上面也说过了,CPU这块的耗时其实就是我们app绘制当前View树的时间,而这段时间就跟我们自己写的代码有关系了,如果你的布局很复杂,层次嵌套很多,每一帧内需要刷新的View又很多时,那么每一帧的绘制耗时自然就会多一点。

我们常说的Android每隔16.6ms刷新一次屏幕其实是指底层会以这个固定频率来切换每一帧的画面。这个每一帧的画面也就是我们的app绘制视图树(View树)计算而来的,这个工作是交由CPU处理,耗时的长短取决于我们写的代码:布局复不复杂,层次深不深,同一帧内刷新的View的数量多不多。CPU绘制视图树来计算下一帧画面数据的工作是在屏幕刷新信号来的时候才开始工作的,而当这个工作处理完毕后,也就是下一帧的画面数据已经全部计算完毕,也不会马上显示到屏幕上,而是会等下一个屏幕刷新信号来的时候再交由底层将计算完毕的屏幕画面数据显示出来。当我们的app界面不需要刷新时(用户无操作,界面无动画),app就接收不到屏幕刷新信号所以也就不会让CPU再去绘制视图树计算画面数据工作,但是底层仍然会每隔16.6ms切换下一帧的画面,只是这个下一帧画面一直是相同的内容。

为什么界面不刷新时app就接收不到屏幕刷新信号了?为什么绘制视图树计算下一帧画面的工作会是在屏幕刷新信号来的时候才开始的?

源码

ViewRootImpl与DecorView的绑定.

View#invalidate()是请求重绘的一个操作,所以我们切入点可以从这个方法开始一步步跟下去。

Android设备呈现到界面上的大多数情况下都是一个Activity,真正承载视图的是一个Window,每个Window都有一个DecorView,我们调用setContentView()其实是将我们自己写的布局文件添加到以DecorView为根布局的一个ViewGroup里,构成一颗View树。

每个Activity对应一颗以DecorView为根布局的View树,但其实DecorView还有mParent,而且就是ViewRootImpl,而且每个界面上的View的刷新,绘制,点击事件的分发其实都是由ViewRootImpl作为发起者的,由ViewRootImpl控制这些操作从DecorView开始遍历View树去分发处理。

ViewRootImpl与DecorView的绑定

跟着invalidate()一步步往下走的时候,发现最后跟到了ViewRootImpl#scheduleTraversals()就停止了。

Android设备呈现到界面上的大多数情况下都是一个Activity,真正承载视图的是一个Window,每个Window都有一个DecorView,我们调用setContentView()其实是将我们自己写的布局文件添加到以DecorView为根布局的一个ViewGroup里,构成一颗View树。

每个Activity对应一颗以DecorView为根布局的View树,但其实DecorView还有mParent,而且就是ViewRootImpl,而且每个界面上的View的刷新,绘制,点击事件的分发其实都是由ViewRootImpl作为发起者的,由ViewRootImpl控制这些操作从DecorView开始遍历View树去分发处理。

View#invalidate()时,也可以看到内部其实是有一个do{}while()循环来不断寻找mParent,所以最终才会走到ViewRootImpl里去,那么可能大伙就会疑问了,为什么DecorView的mParent会是ViewRootImpl呢?换个问法也就是,在什么时候将DevorView和ViewRootImpl绑定起来?

Activity的启动是在ActivityThread里完成的,handleLaunchActivity()会依次间接的执行到Activity的onCreate(),onStart(),onResume()。在执行完这些后ActivityThread会调用WindowManager#addView(),而这个addView()最终其实是调用了WindowManagerGlobal的addView()方法,我们就从这里开始看:

//WindowManagerGlobal#addViewpublicvoidaddView(Viewview,ViewGroup.LayoutParamsparams,Displaydisplay,WindowparentWindow){...ViewRootImplroot;...synchronized(mLock){...//1.实例化一个ViewRootImpl对象root=newViewRootImpl(view.getContext(),display);...mViews.add(view);mRoots.add(root);...}try{//2.调用ViewRootImpl的setView(),并将DecorView作为参数传递进去root.setView(view,wparams,panelParentView);}...}

WindowManager维护着所有Activity的DecorView和ViewRootImpl。这里初始化了一个ViewRootImpl,然后调用了它的setView()方法,将DevorView作为参数传递了进去。所以看看ViewRootImpl中的setView()做了什么:

//ViewRootImpl#setViewpublicvoidsetView(Viewview,WindowManager.LayoutParamsattrs,ViewpanelParentView){synchronized(this){if(mView==null){//1.view是DecorViewmView=view;...//2.发起布局请求requestLayout();...//3.将当前ViewRootImpl对象this,作为参数调用了DecorView的assignParentview.assignParent(this);...}}}

在setView()方法里调用了DecorView的assignParent()方法,所以去看看View的这个方法:

//View#assignParentvoidassignParent(ViewParentparent){if(mParent==null){mParent=null;}elseif(parent==null){mParent=null;}else{thrownewRunTimeException("view"+this+"isalreadyhasaparent")}}

参数是ViewParent,而ViewRootImpl是实现了ViewParent接口的,所以在这里就将DecorView和ViewRootImpl绑定起来了。每个Activity的根布局都是DecorView,而DecorView的parent又是ViewRootImpl,所以在子View里执行invalidate()之类的操作,循环找parent时,最后都会走到ViewRootImpl里来。

跟界面刷新相关的方法里应该都会有一个循环找parent的方法,或者是不断调用parent的方法,这样最终才都会走到ViewRootImpl里,也就是说实际上View的刷新都是由ViewRootImpl来控制的。

即使是界面上一个小小的View发起了重绘请求时,都要层层走到ViewRootImpl,由它来发起重绘请求,然后再由它来开始遍历View树,一直遍历到这个需要重绘的View再调用它的onDraw()方法进行绘制。

重新看回ViewRootImpl的setView()这个方法,这个方法里还调用了一个requestLayout()方法:

//ViewRootImpl#requestLayout@OverridepublicvoidrequestLayout(){if(!mHandingLayoutInLayoutRequest){//1.检查该操作是否是在主线程中执行checkThread();mLayoutRequested=true;//2.安排一次遍历绘制View树的任务scheduleTraversals();}}

这里调用了一个scheduleTraversals(),还记得当View发起重绘操作invalidate()时,最后也调用了scheduleTraversals()这个方法么。其实这个方法就是屏幕刷新的关键,它是安排一次绘制View树的任务等待执行,具体后面再说。

也就是说,其实打开一个Activity,当它的onCreate—onResume生命周期都走完后,才将它的DecoView与新建的一个ViewRootImpl对象绑定起来,同时开始安排一次遍历View任务也就是绘制View树的操作等待执行,然后将DecoView的parent设置成ViewRootImpl对象。

这也就是为什么在onCreate—onResume里获取不到View宽高的原因,因为在这个时刻ViewRootImpl甚至都还没创建,更不用说是否已经执行过测量操作了。

还可以得到一点信息是,一个Activity界面的绘制,其实是在onResume()之后才开始的。

ViewRootImpl#scheduleTraversals

调用一个View的invalidate()请求重绘操作,内部原来是要层层通知到ViewRootImpl的scheduleTraversals()里去。

//ViewRootImpl#scheduleTraversalsvoidscheduleTraversals(){if(!mTraversalScheduled){mTraversalScheduled=true;mTraversalBarrier=mHandler.getLooper().getQueue().postSyncBarrier();mChoreographer.postCallback(Choreograhper.CALLBACK_TRAVERSAL,mTraversalRunnable,null);...}}

mTraversalScheduled这个boolean变量的作用等会再来看,先看看mChoreographer.postCallback()这个方法,传入了三个参数,第二个参数是一个Runnable对象,先来看看这个Runnable:

//ViewRootImpl$TraversalRunnablefinalclassTraversalRunnableimplementsRunnable{@Overridepublicvoidrun(){doTraversal();}}//ViewRootImpl成员变量finalTraversalRunnablemTraversalRunnable=newTraversalRunnable();

做的事很简单,就调用了一个方法,doTraversal():

//ViewRootImpl#doTraversalvoiddoTraversal(){if(mTraversalScheduled){mTraversalScheduled=false;mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);...//1.遍历绘制View树performTraversals();...}}

看看这个方法做的事,跟scheduleTraversals()正好相反,一个将变量置成true,这里置成false,一个是postSyncBarrier(),这里是removeSyncBarrier(),具体作用等会再说,继续先看看performTraversals(),这个方法也是屏幕刷新的关键:

//ViewRootImpl#performTraversalsprivatevoidperformTraversals(){//该方法实在太过复杂,所以将无关代码全部都省略掉,只留下关键代码和代码结构...if(...){...if(...){if(...){...//1.测量performMeasure(childWidthMeasureSpec,childHeightMeasureSpec);...layoutRequested=true;}}}...finalbooleandidLayout=layoutRequested&&(!mStopped||mReportNextDraw);...if(didLayout){//2.布局performLayout(lp,mWidth,mHeight);...}...booleancancelDraw=mAttachInfo.mTreeObserver.dispatchOnPreDraw()||!isViewVisible;if(!cancelDraw&&!newSurface){...//3.绘制performDraw();}......}

View的测量、布局、绘制三大流程都是交由ViewRootImpl发起,而且还都是在performTraversals()方法中发起的,所以这个方法的逻辑很复杂,因为每次都需要根据相应状态判断是否需要三个流程都走,有时可能只需要执行performDraw()绘制流程,有时可能只执行performMeasure()测量和performLayout()布局流程(一般测量和布局流程是一起执行的)。不管哪个流程都会遍历一次View树,所以其实界面的绘制是需要遍历很多次的,如果页面层次太过复杂,每一帧需要刷新的View又很多时,耗时就会长一点。

测量、布局、绘制这些流程在遍历时并不一定会把整颗View树都遍历一遍,ViewGroup在传递这些流程时,还会再根据相应状态判断是否需要继续往下传递。

了解了performTraversals()是刷新界面的源头后,接下去就需要了解下它是什么时候执行的,和scheduleTraversals()又是什么关系?

performTraversals()是在doTraversal()中被调用的,而doTraversal()又被封装到一个Runnable里,那么关键就是这个Runnable什么时候被执行了?

Choreographer

scheduleTraversals()里调用了Choreographer的postCallback()将Runnable作为参数传了进去

//Choreograhper#postCallbackpublicvoidpostCallback(intcallbackType,Runnableaction,Objecttoken){postCallbackDelayed(callbackType,action,token,0);}//Choreograhper#postCallbackDelayedpubicvoidpostCallbackDelayed(intcallbackType,Runnableaction,Objecttoken,longdelayMillis){...postCallbackDelayedInternal(callbackType,action,token,delayMillis);}//Choreograhper#postCallbackDelayedInternalprivatevoidpostCallbackDelayedInternal(intcallbackType,Objectaction,Objecttoken,longdelayMillis){...synchronized(mLock){//1.获取当前时间戳finallongnow=SystemClock.uptimeMillis();finallongdueTime=now+delayMillis;//2.根据时间戳将Runnable任务添加到指定的队列中mCallbackQueues[callbackType].addCallbackLocked(dueTime,action,token);//3.因为postCallback默认传入delay=0,所以代码会走进if里面if(dueTime<=now){scheduleFrameLocked(now);}else{...}}}

因为postCallback()调用postCallbackDelayed()时传了delay=0进去,所以在postCallbackDelayedInternal()里面会先根据当前时间戳将这个Runnable保存到一个mCallbackQueue队列里,这个队列跟MessageQueue很相似,里面待执行的任务都是根据一个时间戳来排序。然后走了scheduleFrameLocked()方法这边,看看做了些什么:

//Choreograhper#scheduleFrameLockedprivatevoidscheduleFrameLocked(longnow){if(!mFrameScheduled){mFrameScheduled=true;//1.系统4.0之后该变量默认为true,所以会走进if里if(USE_VSYNC){...if(isRunningOnLooperThreadLocked()){scheduleVsyncLocked();}else{Messagemsg=mHandler.obtainMessage(MSG_DO_SCHEDULE_VSYNC);msg.setAsynchronous(true);mHandler.sendMessageAtFrontOfQueue(msg);}}...}}

如果代码走了else这边来发送一个消息,那么这个消息做的事肯定很重要,因为对这个Message设置了异步的标志而且用了sendMessageAtFrontOfQueue()方法,这个方法是将这个Message直接放到MessageQueue队列里的头部,可以理解成设置了这个Message为最高优先级,那么先看看这个Message做了些什么:

//Choreograhper#scheduleFrameLockedprivatevoidscheduleFrameLocked(longnow){if(!mFrameScheduled){mFrameScheduled=true;//1.系统4.0之后该变量默认为true,所以会走进if里if(USE_VSYNC){...if(isRunningOnLooperThreadLocked()){scheduleVsyncLocked();}else{Messagemsg=mHandler.obtainMessage(MSG_DO_SCHEDULE_VSYNC);msg.setAsynchronous(true);mHandler.sendMessageAtFrontOfQueue(msg);}}...}}

这个Message设置了异步的标志而且用了sendMessageAtFrontOfQueue()方法,这个方法是将这个Message直接放到MessageQueue队列里的头部,可以理解成设置了这个Message为最高优先级,先看看这个Message:

//Choreograhper$FrameHandler#handleMessageprivatefinalclassFrameHandlerextendsHandler{publicFrameHandler(Looperlooper){super(looper);}@OverridepublicvoidhandleMessage(Messagemsg){switch(msg.what){...caseMSG_DO_SCHEDULE_VSYNC:doScheduleVsync();break;...}}}//Choreographer#doScheduleVsyncvoiddoScheduleVsync(){synchronized(mLock){if(mFrameScheduled){scheduleVsyncLocked();}}}

所以这个Message最后做的事就是scheduleVsyncLocked()。我们回到scheduleFrameLocked()这个方法里,当走if里的代码时,直接调用了scheduleVsyncLocked(),当走else里的代码时,发了一个最高优先级的Message,这个Message也是执行scheduleVsyncLocked()。既然两边最后调用的都是同一个方法,那么为什么这么做呢?

关键在于if条件里那个方法是用来判断当前是否是在主线程的,我们知道主线程也是一直在执行着一个个的Message,那么如果在主线程的话,直接调用这个方法,那么这个方法就可以直接被执行了,如果不是在主线程,那么post一个最高优先级的Message到主线程去,保证这个方法可以第一时间得到处理。

那么这个方法是干嘛的呢,为什么需要在最短时间内被执行呢,而且只能在主线程?

//Choreographer#scheduleVsyncLockedprivatevoidscheduleVsyncLocked(){mDisplayEventReceiver.scheduleVsync();}//DisplayEventReceiver#scheduleVsync/***Schedulesasingleverticalsyncpulsetobedeliveredwhenthenext*displayframebegins.*/publicvoidscheduleVsync(){if(mReceiverPtr==0){Log.w(TAG,"Attemptedtoscheduleaverticalsyncpulsebutthedisplayevent"+"receiverhasalreadybeendisposed.");}else{nativeScheduleVsync(mReceiverPtr);}}

调用了native层的一个方法。

到这里为止,我们知道一个View发起刷新的操作时,会层层通知到ViewRootImpl的scheduleTraversals()里去,然后这个方法会将遍历绘制View树的操作performTraversals()封装到Runnable里,传给Choreographer,以当前的时间戳放进一个mCallbackQueue队列里,然后调用了native层的一个方法就跟不下去了。所以这个Runnable什么时候会被执行还不清楚。那么,下去的重点就是搞清楚它什么时候从队列里被拿出来执行了?

既然这个Runnable操作被放在一个mCallbackQueue队列里,那就从这个队列着手,看看这个队列的取操作在哪被执行了:

//Choreographer$CallbackQueueprivatefinalclassCallbackQueue{privateCallbackRecordmHead;...//1.取操作publicCallbackRecordextractDueCallbacksLocked(longnow){...}//2.入队列操作publicvoidaddCallbackLocked(longdueTime,Objectaction,Objecttoken){...}...}//Choreographer#doCallbacksvoiddoCallbacks(intcallbackType,longframeTimeNanos){CallbackRecordcallbacks;synchronized(mLock){...//1.这个队列跟MessageQueue很相似,所以取的时候需要传入一个时间戳,因为队头的任务可能还没到设定的执行时间callback=mCallbackQueues[callbackType].extractDueCallbacksLocked(now/TimeUtils.NANOS_PER_MS);...}}//Choreographer#doFramevoiddoFrame(longframeTimeNanos,intframe){...try{...//1.这个参数跟ViewRootImpl调用mChoreographer.postCallback()时传进的第一个参数是一致的doCallbacks(Choreograhper.CALLBACK_TRAVERSAL,frameTimeNanos);...}...}

我们说过在ViewRootImpl的scheduleTraversals()里会将遍历View树绘制的操作封装到Runnable里,然后调用Choreographer的postCallback()将这个Runnable放进队列里么,而当时调用postCallback()时传入了多个参数,这是因为Choreographer里有多个队列,而第一个参数Choreographer.CALLBACK_TRAVERSAL这个参数是用来区分队列的,可以理解成各个队列的key值。

那么这样一来,就找到关键的方法了:doFrame(),这个方法里会根据一个时间戳去队列里取任务出来执行,而这个任务就是ViewRootImpl封装起来的doTraversal()操作,而doTraversal()会去调用performTraversals()开始根据需要测量、布局、绘制整颗View树。所以剩下的问题就是doFrame()这个方法在哪里被调用了。

//Choreographer$FrameDisplayEventReceiverprivatefinalclassFrameDisplayEventReceiverextendsDisplayEventReceiverimplementsRunnable{...@OverridepublicvoidonVsync(longtimestampNanos,intbuiltInDisplayId,intframe){...//1.这个这里的this,该message做的事其实是下面的run()方法Messagemsg=Message.obtain(mHandler,this);msg.setAsynchronous(true);mHandler.sendMessageAtTime(msg,timestampNanos/TimeUtils.NANOS_PER_MS);}@Overridepublicvoidrun(){mHavePendingVsync=false;doFrame(mTimestampNanos,mFrame);}}

这个继承自DisplayEventReceiver的FrameDisplayEventReceiver类的作用很重要。

FrameDisplayEventReceiver继承自DisplayEventReceiver接收底层的VSync信号开始处理UI过程。VSync信号由SurfaceFlinger实现并定时发送。FrameDisplayEventReceiver收到信号后,调用onVsync方法组织消息发送到主线程处理。这个消息主要内容就是run方法里面的doFrame了,这里mTimestampNanos是信号到来的时间参数。

也就是说,onVsync()是底层会回调的,可以理解成每隔16.6ms一个帧信号来的时候,底层就会回调这个方法,当然前提是我们得先注册,这样底层才能找到我们app并回调。当这个方法被回调时,内部发起了一个Message,注意看代码对这个Message设置了callback为this,Handler在处理消息时会先查看Message是否有callback,有则优先交由Message的callback处理消息,没有的话再去看看Handler有没有callback,如果也没有才会交由handleMessage()这个方法执行。

onVsync()是由底层回调的,那么它就不是运行在我们app的主线程上,毕竟上层app对底层是隐藏的。但这个doFrame()是个ui操作,它需要在主线程中执行,所以才通过Handler切到主线程中。

前面分析scheduleTraversals()方法时,最后跟到了一个native层方法就跟不下去了,现在再回过来想想这个native层方法的作用是什么,应该就比较好猜测了。

//DisplayEventReceiver#scheduleVsync/***Schedulesasingleverticalsyncpulsetobedeliveredwhenthenext*displayframebegins.*/publicvoidscheduleVsync(){if(mReceiverPtr==0){Log.w(TAG,"Attemptedtoscheduleaverticalsyncpulsebutthedisplayevent"+"receiverhasalreadybeendisposed.");}else{nativeScheduleVsync(mReceiverPtr);}}

大体上是说安排接收一个vsync信号。而根据我们的分析,如果这个vsync信号发出的话,底层就会回调DisplayEventReceiver的onVsync()方法。

如果只是这样的话,就有一点说不通了,首先上层app对于这些发送vsync信号的底层来说肯定是隐藏的,也就是说底层它根本不知道上层app的存在,那么在它的每16.6ms的帧信号来的时候,它是怎么找到我们的app,并回调它的方法呢?

这就有点类似于观察者模式,或者说发布-订阅模式。既然上层app需要知道底层每隔16.6ms的帧信号事件,那么它就需要先注册监听才对,这样底层在发信号的时候,直接去找这些观察者通知它们就行了。

这是我的理解,所以,这样一来,scheduleVsync()这个调用到了native层方法的作用大体上就可以理解成注册监听了,这样底层也才找得到上层app,并在每16.6ms刷新信号发出的时候回调上层app的onVsync()方法。这样一来,应该就说得通了。

还有一点,scheduleVsync()注册的监听应该只是监听下一个屏幕刷新信号的事件而已,而不是监听所有的屏幕刷新信号。比如说当前监听了第一帧的刷新信号事件,那么当第一帧的刷新信号来的时候,上层app就能接收到事件并作出反应。但如果还想监听第二帧的刷新信号,那么只能等上层app接收到第一帧的刷新信号之后再去监听下一帧。

梳理一下目前的信息

我们知道一个View发起刷新的操作时,最终是走到了ViewRootImpl的scheduleTraversals()里去,然后这个方法会将遍历绘制View树的操作performTraversals()封装到Runnable里,传给Choreographer,以当前的时间戳放进一个mCallbackQueue队列里,然后调用了native层的方法向底层注册监听下一个屏幕刷新信号事件。当下一个屏幕刷新信号发出的时候,如果我们app有对这个事件进行监听,那么底层它就会回调我们app层的onVsync()方法来通知。当onVsync()被回调时,会发一个Message到主线程,将后续的工作切到主线程来执行。切到主线程的工作就是去mCallbackQueue队列里根据时间戳将之前放进去的Runnable取出来执行,而这些Runnable有一个就是遍历绘制View树的操作performTraversals()。在这次的遍历操作中,就会去绘制那些需要刷新的View。所以说,当我们调用了invalidate(),requestLayout(),等之类刷新界面的操作时,并不是马上就会执行这些刷新的操作,而是通过ViewRootImpl的scheduleTraversals()先向底层注册监听下一个屏幕刷新信号事件,然后等下一个屏幕刷新信号来的时候,才会去通过performTraversals()遍历绘制View树来执行这些刷新操作。过滤一帧内重复的刷新请求

整体上的流程我们已经梳理出来,但还有几点问题需要解决。

我们在一个16.6ms的一帧内,代码里可能会有多个View发起了刷新请求,这是非常常见的场景了,比如某个动画是有多个View一起完成,比如界面发生了滑动等等。

按照我们上面梳理的流程,只要View发起了刷新请求最终都会走到ViewRootImpl中的scheduleTraversals()里去,是吧。而这个方法又会封装一个遍历绘制View树的操作performTraversals()到Runnable然后扔到队列里等刷新信号来的时候取出来执行。

那如果多个View发起了刷新请求,岂不是意味着会有多次遍历绘制View树的操作?

其实,这点不用担心,还记得我们在最开始分析scheduleTraverslas()的时候先跳过了一些代码么?现在我们回过来继续看看这些代码:

//ViewRootImpl#scheduleTraversalsvoidscheduleTraversals(){if(!mTraversalScheduled){//1.注意这个boolean类型的变量mTraversalScheduled=true;mTraversalBarrier=mHandler.getLooper().getQueue().postSyncBarrier();mChoreographer.postCallback(Choreograhper.CALLBACK_TRAVERSAL,mTraversalRunnable,null);...}}

我们上面分析的scheduleTraversals()干的那一串工作,前提是mTraversalScheduled这个boolean类型变量等于false才会去执行。那这个变量在什么时候被赋值被false了呢:

一个是上图的doTraversal(),还有就是声明时默认为false,剩下一个是在取消遍历绘制View操作unscheduleTraversals()里。

doTraversal()这个方法,就是在scheduleTraversals()中封装到Runnable里的那个方法。

当我们调用了一次scheduleTraversals()之后,直到下一个屏幕刷新信号来的时候,doTraversal()被取出来执行。在这期间重复调用scheduleTraversals()都会被过滤掉的。那么为什么需要这样呢?

View就是在执行performTraversals()遍历绘制View树过程中层层遍历到需要刷新的View,然后去绘制它的。既然是遍历,那么不管上一帧内有多少个View发起了刷新的请求,在这一次的遍历过程中全部都会去处理的。这也是我们从代码上看到的,每一个屏幕刷新信号来的时候,只会去执行一次performTraversals(),因为只需遍历一遍,就能够刷新所有的View了。

同步屏障消息postSyncBarrier()

当我们的app接收到屏幕刷新信号时,来不及第一时间就去执行刷新屏幕的操作,这样一来,即使我们将布局优化得很彻底,保证绘制当前View树不会超过16ms,但如果不能第一时间优先处理绘制View的工作,那等16.6ms过了,底层需要去切换下一帧的画面了,我们app却还没处理完,这样也照样会出现丢帧了吧。而且这种场景是非常有可能出现的吧,毕竟主线程需要处理的事肯定不仅仅是刷新屏幕的事而已,那么这个问题是怎么处理的呢?

//ViewRootImpl#scheduleTraversalsvoidscheduleTraversals(){if(!mTraversalScheduled){mTraversalScheduled=true;//1.注意这行代码,往主线程的消息队列里发送了一个同步屏障消息mTraversalBarrier=mHandler.getLooper().getQueue().postSyncBarrier();mChoreographer.postCallback(Choreograhper.CALLBACK_TRAVERSAL,mTraversalRunnable,null);...}}//ViewRootImpl#doTraversalvoiddoTraversal(){if(mTraversalScheduled){mTraversalScheduled=false;//1.注意这行代码,移除消息队列里的同步屏障消息mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);...performTraversals();...}}

逻辑走进Choreographer前会先往队列里发送一个同步屏障,而当doTraversal()被调用时才将同步屏障移除。

这个同步屏障的作用可以理解成拦截同步消息的执行,主线程的Looper会一直循环调用MessageQueue的next()来取出队头的Message执行,当Message执行完后再去取下一个。

当next()方法在取Message时发现队头是一个同步屏障的消息时,就会去遍历整个队列,只寻找设置了异步标志的消息,如果有找到异步消息,那么就取出这个异步消息来执行,否则就让next()方法陷入阻塞状态。

如果next()方法陷入阻塞状态,那么主线程此时就是处于空闲状态的,也就是没在干任何事。所以,如果队头是一个同步屏障的消息的话,那么在它后面的所有同步消息就都被拦截住了,直到这个同步屏障消息被移除出队列,否则主线程就一直不会去处理同步屏幕后面的同步消息。

而所有消息默认都是同步消息,只有手动设置了异步标志,这个消息才会是异步消息。另外,同步屏障消息只能由内部来发送,这个接口并没有公开给我们使用。

最后,仔细看上面Choreographer里所有跟message有关的代码,你会发现,都手动设置了异步消息的标志,所以这些操作是不受到同步屏障影响的。这样做的原因可能就是为了尽可能保证上层app在接收到屏幕刷新信号时,可以在第一时间执行遍历绘制View树的工作。

刷新控制者ViewRootImpl

所有跟界面刷新相关的操作,其实最终都会走到ViewRootImpl中的scheduleTraversals()去的。

跟界面刷新有关的操作大概就是下面几种场景吧:

invalidate(请求重绘)

requestLayout(重新布局)

requestFocus(请求焦点)

startActivity(打开新界面)

onRestart(重新打开界面)

KeyEvent(遥控器事件,本质上是焦点导致的刷新)

Animation(各种动画,本质上是请求重绘导致的刷新)

RecyclerView滑动(页面滑动,本质上是动画导致的刷新)

setAdapter(各种adapter的更新)

//ViewRootImpl#requestChildFocus@OverridepublicvoidrequestChildFocus(Viewchild,Viewfocused){if(DEBUG_INPUT_RESIZE){Log.v(mTag,"Requestchildfocus:focusnow"+focused);}checkThread();scheduleTraversals();}//ViewRootImpl#clearChildFocus@OverridepublicvoidclearChildFocus(Viewchild){if(DEBUG_INPUT_RESIZE){Log.v(mTag,"Clearingchildfocus");}checkThread();scheduleTraversals();}//ViewRootImpl#requestLayout@OverridepublicvoidrequestLayout(){if(!mHandlingLayoutInLayoutRequest){checkThread();mLayoutRequested=true;scheduleTraversals();}}总结界面上任何一个View的刷新请求最终都会走到ViewRootImpl中的scheduleTraversals()里来安排一次遍历绘制View树的任务;scheduleTraversals()会先过滤掉同一帧内的重复调用,在同一帧内只需要安排一次遍历绘制View树的任务即可,这个任务会在下一个屏幕刷新信号到来时调用performTraversals()遍历View树,遍历过程中会将所有需要刷新的View进行重绘;接着scheduleTraversals()会往主线程的消息队列中发送一个同步屏障,拦截这个时刻之后所有的同步消息的执行,但不会拦截异步消息,以此来尽可能的保证当接收到屏幕刷新信号时可以尽可能第一时间处理遍历绘制View树的工作;发完同步屏障后scheduleTraversals()才会开始安排一个遍历绘制View树的操作,作法是把performTraversals()封装到Runnable里面,然后调用Choreographer的postCallback()方法;postCallback()方法会先将这个Runnable任务以当前时间戳放进一个待执行的队列里,然后如果当前是在主线程就会直接调用一个native层方法,如果不是在主线程,会发一个最高优先级的message到主线程,让主线程第一时间调用这个native层的方法;native层的这个方法是用来向底层注册监听下一个屏幕刷新信号,当下一个屏幕刷新信号发出时,底层就会回调Choreographer的onVsync()方法来通知上层app;onVsync()方法被回调时,会往主线程的消息队列中发送一个执行doFrame()方法的消息,这个消息是异步消息,所以不会被同步屏障拦截住;doFrame()方法会去取出之前放进待执行队列里的任务来执行,取出来的这个任务实际上是ViewRootImpl的doTraversal()操作;上述第4步到第8步涉及到的消息都手动设置成了异步消息,所以不会受到同步屏障的拦截;doTraversal()方法会先移除主线程的同步屏障,然后调用performTraversals()开始根据当前状态判断是否需要执行performMeasure()测量、perfromLayout()布局、performDraw()绘制流程,在这几个流程中都会去遍历View树来刷新需要更新的View;常见问题

Android每隔16.6ms刷新一次屏幕到底指的是什么意思?是指每隔16.6ms调用onDraw()绘制一次么?

如果界面一直保持没变的话,那么还会每隔16.6ms刷新一次屏幕么?

我们常说的Android每隔16.6ms刷新一次屏幕其实是指底层会以这个固定频率来切换每一帧的画面,而这个每一帧的画面数据就是我们app在接收到屏幕刷新信号之后去执行遍历绘制View树工作所计算出来的屏幕数据。

而app并不是每隔16.6ms的屏幕刷新信号都可以接收到,只有当app向底层注册监听下一个屏幕刷新信号之后,才能接收到下一个屏幕刷新信号到来的通知。而只有当某个View发起了刷新请求时,app才会去向底层注册监听下一个屏幕刷新信号。

也就是说,只有当界面有刷新的需要时,我们app才会在下一个屏幕刷新信号来时,遍历绘制View树来重新计算屏幕数据。如果界面没有刷新的需要,一直保持不变时,我们app就不会去接收每隔16.6ms的屏幕刷新信号事件了,但底层仍然会以这个固定频率来切换每一帧的画面,只是后面这些帧的画面都是相同的而已。

界面的显示其实就是一个Activity的View树里所有的View都进行测量、布局、绘制操作之后的结果呈现,那么如果这部分工作都完成后,屏幕会马上就刷新么?

我们app只负责计算屏幕数据而已,接收到屏幕刷新信号就去计算,计算完毕就计算完毕了。至于屏幕的刷新,这些是由底层以固定的频率来切换屏幕每一帧的画面。所以即使屏幕数据都计算完毕,屏幕会不会马上刷新就取决于底层是否到了要切换下一帧画面的时机了。

**网上都说避免丢帧的方法之一是保证每次绘制界面的操作要在16.6ms内完成,但如果这个16.6ms是一个固定的频率的话,请求绘制的操作在代码里被调用的时机是不确定的啊,那么如果某次用户点击屏幕导致的界面刷新操作是在某一个16.6ms帧快结束的时候,那么即使这次绘制操作小于16.6ms,按道理不也会造成丢帧么?这又该如何理解?**

代码里调用了某个View发起的刷新请求,这个重绘工作并不会马上就开始,而是需要等到下一个屏幕刷新信号来的时候才开始。

主线程耗时的操作会导致丢帧,但是耗时的操作为什么会导致丢帧?它是如何导致丢帧发生的?

造成丢帧大体上有两类原因,一是遍历绘制View树计算屏幕数据的时间超过了16.6ms;

二是,主线程一直在处理其他耗时的消息,导致遍历绘制View树的工作迟迟不能开始,从而超过了16.6ms底层切换下一帧画面的时机。

第一个原因就是我们写的布局有问题了,需要进行优化了。而第二个原因则是我们常说的避免在主线程中做耗时的任务。

针对第二个原因,系统已经引入了同步屏障消息的机制,尽可能的保证遍历绘制View树的工作能够及时进行,但仍没办法完全避免,所以我们还是得尽可能避免主线程耗时工作。


普通会员

0

帖子

319

回复

326

积分
沙发
发表于 2022-05-08 06:01:25

谢谢楼主分享

普通会员

0

帖子

321

回复

328

积分
板凳
发表于 2023-08-27 20:46:03

信春哥,得永生!

您需要登录后才可以回帖 登录 | 立即注册

触屏版| 电脑版

技术支持 历史网 V2.0 © 2016-2017