屏幕刷新机制 笔记整理

参阅文章:Android 屏幕刷新机制

需要先仔细阅读一下原文,我这里只是把一些讲解步骤抽炼出来,使得看起来更直观。

涉及到的源码,基本上都是按照调用顺序贴出来的,且版本为 SDK 27。

根据原文可以知道, View#invalidate() 最终会走到 ViewRootImpl#scheduleTraversals()

其中,invalidate 的字面意思是 vt. 使无效;使无价值,而 View#invalidate() 作用如下:
在这里插入图片描述
简单的说,就是使 View 原本的内容无效,从而使得其被刷新。


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

1. ViewRootImpl 与 DecorView 的绑定

Activity 的启动是在 ActivityThread 里完成的,handleLaunchActivity() 会依次间接的执行到 Activity 的 onCreate(), onStart(), onResume()。在执行完这些后 ActivityThread 会调用 WindowManager#addView(),而这个 addView() 最终其实是调用了 WindowManagerGlobal 的 addView() 方法。然后接下来做的事如下:

  • WindowManagerGlobal.java
// 传入的 view 即为 DecorView 实例
void addView(view,....) {
	// 实例化 ViewRootImpl
	root = new ViewRootImpl(view.getContext(), display);
	
	root.setView(view, wparams, panelParentView);
}
  • ViewRootImpl.java
// 传入的 view 即为 DecorView
void setView(view,....) {
	requestLayout();//第一次请求布局,其内部调用 scheduleTraversals();
	
	// assignParent() 方法的作用就将当前 viewRootImpl 实例
	// 做为 view(即 decorView) 的 parent,
	// 使之绑定在一起
	view.assignParent(this);
}
  • DecorView.java
    该方法继承自 View
void assignParent(ViewParent parent) {
    if (mParent == null) {
        mParent = parent;
    } else if (parent == null) {
        mParent = null;
    } else {
        throw new RuntimeException("view " + this + " being added, but"
                + " it already has a parent");
    }
}

经过上面的步骤,可以了解到 decorView 是怎么与 viewRootImpl 进行绑定的。


2. ViewRootImpl # scheduleTraversals()

从原文可以了解到,调用一个 View 的 invalidate() 请求重绘操作,内部原来是要层层通知到 ViewRootImpl 的 scheduleTraversals() 里去。而且打开一个新的 Activity,它的界面绘制原来是在 onResume() 之后也层层通知到 ViewRootImpl 的 scheduleTraversals() 里去。

下面就整理了一下 scheduleTraversals() 的逻辑流程。

  • ViewRootImpl.java
void scheduleTraversals() {
	...

	mTraversalScheduled = true;
	
	// 在主线程的消息队列中添加一个 Barrier msg
	mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
	
	// 通过 mChoreographer,会根据实际情况来判断是将 mTraversalRunnable 作为一个
	// 异步 msg 添加到主线程的消息队列中,还是会马上触发后面会说到的 scheduleVsyncLocked() 
	// 方法
	// mTraversalRunnable 的 run() 方法调用 doTraversal()
	// 简单的说 mTraversalRunnable 是一个执行遍历 View 树的任务(runnable)
	mChoreographer
		.postCallback(Choreographer.CALLBACK_TRAVERSAL, 
					  mTraversalRunnable,
					  null)
	
	...
}

// ViewRootImpl 的内部类
final class TraversalRunnable implements Runnable {
    @Override
    public void run() {
        doTraversal();
    }
}
void doTraversal() {
    if (mTraversalScheduled) {
        mTraversalScheduled = false;
        
        // 移除主线程 MessageQueue 中的 Barrier msg
        mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
        
        // 执行遍历
        performTraversals();
        
        ...
    }
}
void performTraversals() {
	// 根据实际情况发起测量、布局、绘制三大流程
}
  • Choreographer.java
// 传入的 action 即前面的 mTraversalRunnable
postCallback(..., Runnable action, ...) {
	//	最终调用 postCallbackDelayedInternal(..., delayMillis == 0, token == null)
}
// 传递进来的 action 即前面的 mTraversalRunnable,
// callbackType 即前面的 Choreographer.CALLBACK_TRAVERSAL
// 传入的 token 为 null
void postCallbackDelayedInternal(int callbackType,
        Object action, Object token, long delayMillis) {
    ...
    synchronized (mLock) {
        final long now = SystemClock.uptimeMillis();
        final long dueTime = now + delayMillis;
        
        // 把前面传入的 mTraversalRunnable 添加进 
        // 	mCallbackQueue[Choreographer.CALLBACK_TRAVERSAL] 队列里
        // 在添加到队列里的时候,会结合 token 把 mTraversalRunnable 封装成一个 
        //	CallbackRecord 对象,且该对象的成员变量 token==null
        // 这个队列跟 MessageQueue 很相似,里面待执行的任务都是根据一个时间戳来排序
        mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);
        
        if (dueTime <= now) {
      		// 因为 delayMillis == 0,所以 dueTime == now
      		// 马上执行 if 中的逻辑 
            scheduleFrameLocked(now);
        } else {
        	// 如果有延迟,则设置一个异步 msg 到主线程的消息队列中
        	// 等时间到了再取出来执行该 msg 对应的逻辑,其实最终
        	//	也是执行 scheduleFrameLocked(now)
            Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
            msg.arg1 = callbackType;
            msg.setAsynchronous(true);
            mHandler.sendMessageAtTime(msg, dueTime);
        }
        
        // 因此对于 if-else 语句,最终都会调用 scheduleFrameLocked()
    }
}

//处理上述 MSG_DO_SCHEDULE_CALLBACK 类型的 msg 的逻辑
private final class FrameHandler extends Handler {
    
    @Override
    public void handleMessage(Message msg) {
        switch (msg.what) {
            ...
            case MSG_DO_SCHEDULE_CALLBACK:
                doScheduleCallback(msg.arg1);
                break;
        }
    }
}

void doScheduleCallback(int callbackType) {
    synchronized (mLock) {
        if (!mFrameScheduled) {
            final long now = SystemClock.uptimeMillis();
            if (mCallbackQueues[callbackType].hasDueCallbacksLocked(now)) {
                scheduleFrameLocked(now);
            }
        }
    }
}

// Choreographer 的静态内部类
private static final class CallbackRecord {
    public CallbackRecord next;
    public long dueTime;
    public Object action; // Runnable or FrameCallback
    public Object token;
    public void run(long frameTimeNanos) {
        if (token == FRAME_CALLBACK_TOKEN) {
            ((FrameCallback)action).doFrame(frameTimeNanos);
        } else {
            // 当为 TraversalRunnable 时,对应当 token 为 null
            ((Runnable)action).run();
        }
    }
}
void scheduleFrameLocked() {
    if (!mFrameScheduled) {
        mFrameScheduled = true;
        if (USE_VSYNC) { // USE_VSYNC 默认为 true
            // 判断是不是在主线程中
            if (isRunningOnLooperThreadLocked()) {
                // 如果在主线程中则直接执行 scheduleVsyncLocked()
                scheduleVsyncLocked();
            } else {
            	// 如果不是在主线程则通过主线程的 handler 切换到主线程
            	
            	// 如果代码走了 else 这边来发送一个消息,那么这个消息做的事肯定很重要,因为
            	// 对这个 Message 设置了异步的标志而且用了sendMessageAtFrontOfQueue() 
            	// 方法,这个方法是将这个 Message 直接放到 MessageQueue 队列里的头部,
            	// 可以理解成设置了这个 Message 为最高优先级
                scheduleVsyncLocked()
                Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_VSYNC);
                msg.setAsynchronous(true);
                mHandler.sendMessageAtFrontOfQueue(msg);
            }
            
			// 因此对于 if-else 语句,最终都会调用 scheduleVsyncLocked()
        } else {
            ...
        }
    }
}

// 处理 MSG_DO_SCHEDULE_VSYNC 类型的 msg (部分源码)
case MSG_DO_SCHEDULE_VSYNC: doScheduleVsync();
    
void doScheduleVsync() {
    synchronized (mLock) {
        if (mFrameScheduled) {
            scheduleVsyncLocked();
        }
    }
}
void scheduleVsyncLocked() {
	mDisplayEventReceiver.scheduleVsync();
}
  • DisplayEventReceiver.java
void scheduleVsync() {
    if (mReceiverPtr == 0) {
        Log.w(TAG, "Attempted to schedule a vertical sync pulse but the display event "
                + "receiver has already been disposed.");
    } else {
        nativeScheduleVsync(mReceiverPtr);
    }
}

nativeScheduleVsync(mReceiverPtr) 为 native 方法,其作用根据原文也可以知道,大概就是向底层注册监听下一帧 VSync 信号,有点类似于观察者模式,或者说发布-订阅模式,这样底层在发信号的时候,直接去找这些观察者通知它们就行了。

看到这里,那么在 scheduleTraversals() 中调用 mChoreographer.postCallback(...,mTraversalRunnable...) 的意图就很明显,即将执行 performTraversals()(遍历 View 树)操作的任务 ViewRootImpl#mTraversalRunnable 放进队列 mCallbackQueues[CALLBACK_TRAVERSAL] 中,并注册监听下一帧 VSync 信号。

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

但是这里还只是半部分内容,因为后面还会说明 ViewRootImpl#mTraversalRunnable 该任务(即遍历 View 树)会在什么时候被触发。


3. 底层回调触发 ViewRootImpl#mTraversalRunnable

根据原文,可以知道在成功向底层注册监听之后,在下一帧 Vsync 发生时会由底层回调 app 层的 FrameDisplayEventReceiver#onVsync() 方法:

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

FrameDisplayEventReceiver 是 Choreographer 的内部类

public class Choreographer {
	...

	void doFrame(){...}

	private final class FrameDisplayEventReceiver 
		extends DisplayEventReceiver implements Runnable {
		...

		// 这里 mTimestampNanos 是信号到来的时间参数
    	public void onVsync(long timestampNanos, 
    						int builtInDisplayId, int frame) {
    	    
        	mTimestampNanos = timestampNanos;
        	mFrame = frame;
        	// 因为 FrameDisplayEventReceiver 继承了 Runnable
    	    // 所以这里获得一个 msg 并设置其 callback 为当前 
    	    // frameDisplayEventReceiver 实例
        	Message msg = Message.obtain(mHandler, this);
        	msg.setAsynchronous(true);
        	mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);	
    	}
    	
		public void run() {
			mHavePendingVsync = false;
			// odFrame() 为 Choreographer 的成员方法
   	 		doFrame(mTimestampNanos, mFrame);
		}
	}
}

上述逻辑会在主线程对应的 MessageQueue 中添加一个 msg(目的应该是为了从底层回调 onVsync() 的线程切换到 app 所在的主线程),且该 msgcallback 即为 FrameDisplayEventReceiverFrameDisplayEventReceiver 实现了 Runnable 接口)。

因此当轮询到这个 msg 的时候,就会执行 ChoreographerdoFrame() 方法。

  • Choreographer.java
void doFrame() {
	...
	
	doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);
	
	...
}
void doCallbacks(int callbackType, long frameTimeNanos) {
	CallbackRecord callbacks;
	... 
	// now 用于判断对头任务是否达到可执行时刻
	final long now = System.nanoTime();
	// 根据 now 时间戳取出 mCallbackQueues[Choreographer.CALLBACK_TRAVERSAL] 队列中
	//  第一个符合条件的封装有 mTraversalRunnable 的 CallbackRecord 实例
    callbacks = mCallbackQueues[callbackType].extractDueCallbacksLocked(
    										now / TimeUtils.NANOS_PER_MS);
	if (callbacks == null) {
   		return;
	}
	...
	
	// 从前面得到的第一个符合 now 时间戳条件的 callbackRecord 实例开始,
	// 一次性执行该 callbackRecord 实例以及其后面的 callbackRecord 实例
	for (CallbackRecord c = callbacks; c != null; c = c.next) {
		// 根据前面的内容可以知道 callbackRecord 封装了 mTraversalRunnable 且
		//	其 token == null
		// 因此根据 CallbackRecord 的源码,c.run() 会触发 mTraversalRunnable 
		// 	的 run() 方法(该 run() 方法是做啥的心里总有点逼数吧!!!)
    	c.run(frameTimeNanos);
	}
}

对于触发 ViewRootImpl#mTraversalRunnable 部分的内容,比较好理解了,当由底层回调了 app 相关的方法后,就会进一步触发 ViewRootImpl#mTraversalRunnable ,从而实现遍历 View 树。


对于屏幕刷新机制,简单的概括的话总共分为两大步:

  • 第一步是将遍历 View 树的任务保存起来,且向底层注册监听下一帧 VSync 信号
  • 第二步是当 VSync 来临的时候,由底层回调相关方法,从而取出前面保存的任务并执行

接下来就要说一下相关的知识点了:

1、有关 Barrier Message

在执行 ViewRootImpl#scheduleTraversals() 的时候,就有一行代码:

mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();

该行代码的作用就是在主线程的消息队列中添加一个 Barrier msg,如下演示图:
在这里插入图片描述
当轮训到 Barrier msg 的时候,就会以此点开始,遍历接下来的数个 msgs 中的异步 msg。

而这些异步 msg 都是在进行与屏幕刷新相关的逻辑时添加的。之所以这样做,是为了以最快的速度处理这些异步 msg,从而不影响到正常的屏幕刷新的相关逻辑。

(有关具体的 Barrier msg 的原理,可以参阅:Handler 笔记总结

而异步 msg 添加的时机,主要有如下几个:

  • Choreographer#postCallbackDelayedInternal
    这里是为了有延迟时能够等时间到了再触发 scheduleFrameLocked()

  • Choreographer#scheduleFrameLocked
    这里是为了保证在主线程中马上触发 scheduleVsyncLocked(),因为这里涉及到 msg 是异步的,而且是直接放在队首的(比前面设置的 Barrier msg 的位置还要靠前)

  • FrameDisplayEventReceiver#onVsync
    这里是为了触发 mCallbackQueues[CALLBACK_TRAVERSAL] 中被封装成 CallbackRecordViewRootImpl#mTraversalRunnable(即最终与屏幕刷新相关的任务——遍历 View 树)

而移除 Barrier msg 是在 ViewRootImpl#doTraversal() 中,执行 performTraversals() 前。

2、在下一帧 VSync 到来前对多次请求重绘的过滤

引用自原文:

在执行 scheduleTraversals()的时候,会把一个变量值 mTraversalScheduled 置为 true,而只在三个地方被赋值为 false,一个是前面提及的 doTraversal(),还有就是声明时默认为 false,剩下一个是在取消遍历绘制 View 操作 unscheduleTraversals() 里。

也就是说,当我们调用了一次 scheduleTraversals()之后,直到下一个屏幕刷新信号来的时候,doTraversal() 被取出来执行。在这期间重复调用 scheduleTraversals() 都会被过滤掉的。

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

performTraversals() 会被执行的前提是调用了 scheduleTraversals() 来向底层注册监听了下一个屏幕刷新信号事件,所以在同一个 16.6ms 的一帧内,只需要第一个发起刷新请求的 View 来走一遍 scheduleTraversals() 干的事就可以了,其他不管还有多少 View 发起了刷新请求,没必要再去重复向底层注册监听下一个屏幕刷新信号事件了,反正只要有一次遍历绘制 View 树的操作就可以对它们进行刷新了。

自己的理解:

在同一刷新帧内,多次发起刷新请求,只有第一次的有效 ,后面的则会因为特殊的一个标记值而失效,但这并不影响所有 View 刷新到第一次请求之后的后序请求对应的状态,因为这些状态值会实时的更新在 View 对象中并得以保存。

比如设置了一个 View 的 width 为 x1,然后请求刷新,此时在同一刷新帧内,又重新设置为 x2,再次请求刷新,这一次请求是不会生效的,但是此时 View 的 width 却已经更新为了 x2,而不是第一次的 x1,因此在刷新的时候会根据 x2 来展示 View 的宽。

另外,在 Choreographer 中也有一个成员变量,mFrameScheduled,其作用也跟 mTraversalScheduled 类似,用于过滤多余的触发

private void scheduleVsyncLocked() {
    mDisplayEventReceiver.scheduleVsync();
}

的操作。

3. 有关 CPU 的操作

在这里插入图片描述

当下一帧信号来临的时候,底层会回调触发 app 的 “遍历 View 树的操作”,但是在触发操作前,需要先在 mCallbackQueues[Choreographer.CALLBACK_TRAVERSAL] 中找到对应的包含 mTraversalRunnableCallbackRecord 进行处理,才能进一步触发操作。

因此,从更加小的时间粒度来看,并不是信号来了就能马上开始执行遍历 View 树的操作,之间还是会存在一些 “间隙”。

另外,也可能由于某些原因,比如 CPU 资源被占用,导致没有及时的去处理,也会导致 “间隙” 的产生。

而 “遍历 View 树的操作” ,其实就是上图中 CPU 对应的那一横排,简单的说,CPU 对应的操作就是测量,绘制,布局等(即一系列的指令),并将这些操作数据化(而不是直接将内容绘制到屏幕上),然后 CPU 会把这些数据传递给 GPU,GPU 将其转换为 Display 所需要的数据,然后 Display 将其读取并展示出来。

CPU 的操作+ GPU 的操作需要在下一帧开始前完成,不然 Display 就取不到该帧的内容且无法展示。

CPU负责包括Measure,Layout,Record,Execute的计算操作,
GPU 负责Rasterization(栅格化)操作。
引用自:Android性能优化第(四)篇—Android渲染机制

4. 加了同步屏障到移除前的时间段

加了同步屏障到移除前这段时间是干不了我们自己通过主线程 Handler 来分配的工作的,但这些工作最多是被延迟一帧而已,到下一帧都会被处理掉。

这里可以关注一下 Handler 笔记总结 提到的 Idle Handler,其可以在 MessageQueue#next() 中取不到符合条件的 msg 时被调用执行。

5. 补充

前面说的,对于注册监听 Vsync 信号,只会监听下一帧的 Vsync 信号,而不是所有的,因此推测除了 Activity 首次初始化的时候会主动注册关于下一帧 Vsync 信息的监听,剩下的,因为都是在 View 的内容需要刷新的时候才被动去注册的。

关于 “被动”,指是,假设整个 Activity 只有一个 TextView,最初设置的 text 为 “Default”,如果 TextView 一直没有变化,则就不会去注册对于下一帧 Vsync 信号的监听,除非使得 TextView 的内容发生变化,此时设置其变化的代码里面应该会包含类似于 View#invalidate() 的逻辑(即被动设置),所以能够注册关于下一帧 Vsync 信号的监听,从而在下一帧中实现内容的刷新。

而如果没有设置对下一帧 Vsync 信号的监听,此时呈现的内容一直是之前的,也就没有变化。

还有一种推测就是系统会自己实现在处理了新的一帧 Vsync 的信号之后,再自动去实现关于下一帧信号的监听。而如果是这样的话,则相当于一环扣一环的监听每一帧 Vsync 信号,此时,程序猿也没有必要再手动调用 View#invalidate() 了,还有一个点可以从侧面来推到这个,那就是 View 的 onDraw() 方法并不是一直实时在调用的。

而且,还有一点,就是 View 树中的某一个 View 触发 invalidate(),导致最终触发 ViewRootImpl#performTraversals() 的时候,并不会导致所有的子 View 都重绘(通过针对自定义的 View 的 onDraw() 方法测试的),大概的原因就是每个 View 对应的绘制内容都会有缓存,只有需要刷新内容的时候,才会更新缓存,进行重绘。

关于这点补充,可以通过源码证明,即在首次启动 Activity 的时候,需要将 Activity resume,涉及到的源码部分即ActivityThread#handleResumeActivity()
(1)在其中有执行 wm.addView() 这样一句代码,而这里的 wm 具体为 WindowManagerGlobal,这句代码的作用之一就是为了将 DecorViewViewRootImp 绑定,
(2)并且在 wm.addView() 中会进一步执行 root.setView()rootViewRootImpl
(3)而在 root.setView() 中,就会首次调用 ViewRootImpl#requestLayout(),从而进一步触发 scheduleTraversals()

而且上述步骤是在执行了 Activity#onResume() 之后才执行的。


额外阅读内容:
1、android屏幕刷新显示机制
2、Android应用程序UI硬件加速渲染的Display List构建过程分析
3、Android图形系统之 VSync

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值