当前位置:网站首页>vie的刷新机制
vie的刷新机制
2022-06-24 23:12:00 【东东旭huster】
前言
平时只知道在调用invalidate, requestLayout,或者 通过动画能够刷新屏幕,实现想要的ui效果,但是对view的刷新机制并不了解,本文记录了我对以下几个问题的思考和总结。
- view多长时间刷新一次
- view什么时机刷新
- view如何刷新
- 如果界面不变化,还需要刷新吗
- 只要调用invalidate, requestLayout等函数就会会被立即刷新吗
view多长时间刷新一次
Android系统每隔16ms会发出VSYNC信号重绘我们的界面(Activity).
为什么是16ms, 因为Android设定的刷新率是60FPS(Frame Per Second), 也就是每秒60帧的刷新率, 约合16ms刷新一次.
对于一个系统来说,可以分为CPU,GPU和显示器三个部分,CPU负责计算,GPU对计算的数据进行渲染,然后放到缓冲区中存起来,显示器每隔一个固定的频率去取渲染好的数据显示出来。显示器刷新频率是固定的,但是CPU和GPU的计算和渲染时间却是没有规律的,假设GPU的渲染速率是瞬间完成的,那主要的时间因素就取决于CPU,CPU计算的过程其实就是View树的绘制过程,即从根布局开始,遍历所有的view分别执行测量、布局、绘制的过程,如果我么的界面过于复杂,在16ms内没有计算完成,那显示器取到的就是旧的数据,这就是掉帧,对用户来说就会觉得卡顿。

上图就是发生了一次典型的掉帧过程。
view刷新时机
我们都知道invalidate(), requestLayout()函数会让view进行重绘,但是一旦调用就立即开始重新绘制了吗?上面提到的VSYNC信号有什么作用?
我们从View的invalidate函数开始看看整个执行流程
public void invalidate(Rect dirty) {
final int scrollX = mScrollX;
final int scrollY = mScrollY;
invalidateInternal(dirty.left - scrollX, dirty.top - scrollY,
dirty.right - scrollX, dirty.bottom - scrollY, true, false);
}
invalidateInternal()函数中会递归的调用parent的invaldateChild()函数,最终会调用到ViewRootImpl的invalidate()方法
void invalidate() {
mDirty.set(0, 0, mWidth, mHeight);
if (!mWillDrawSoon) {
scheduleTraversals();
}
}
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
// 插入一个同步消息屏障
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}
mTraversalRunnable是一个Runnable对象,它内部执行了doTraversal()方法,这个方法中才真正的开始遍历和绘制操作,从上面代码中可以知道,它并不是立即开始绘制,而是通过mChoreographer对象注册了一个回调,在这个postCallback中会请求同步VSYNC信息。
private void postCallbackDelayedInternal(int callbackType,
Object action, Object token, long delayMillis) {
if (DEBUG_FRAMES) {
Log.d(TAG, "PostCallback: type=" + callbackType
+ ", action=" + action + ", token=" + token
+ ", delayMillis=" + delayMillis);
}
synchronized (mLock) {
final long now = SystemClock.uptimeMillis();
final long dueTime = now + delayMillis;
mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);
if (dueTime <= now) {
// 申请同步信号
scheduleFrameLocked(now);
} else {
Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
msg.arg1 = callbackType;
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, dueTime);
}
}
}
private void scheduleFrameLocked(long now) {
if (!mFrameScheduled) {
mFrameScheduled = true;
if (USE_VSYNC) {
if (DEBUG_FRAMES) {
Log.d(TAG, "Scheduling next frame on vsync.");
}
// If running on the Looper thread, then schedule the vsync immediately,
// otherwise post a message to schedule the vsync from the UI thread
// as soon as possible.
if (isRunningOnLooperThreadLocked()) {
scheduleVsyncLocked();
} else {
Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_VSYNC);
msg.setAsynchronous(true);
mHandler.sendMessageAtFrontOfQueue(msg);
}
} else {
final long nextFrameTime = Math.max(
mLastFrameTimeNanos / TimeUtils.NANOS_PER_MS + sFrameDelay, now);
if (DEBUG_FRAMES) {
Log.d(TAG, "Scheduling next frame in " + (nextFrameTime - now) + " ms.");
}
Message msg = mHandler.obtainMessage(MSG_DO_FRAME);
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, nextFrameTime);
}
}
}
因为加了同步消息屏障,所以这里创建了一个异步的MSG_DO_FRAME消息,这里要看一个很重要的类FrameDisplayEventReceiver
private final class FrameDisplayEventReceiver extends DisplayEventReceiver
implements Runnable {
private boolean mHavePendingVsync;
private long mTimestampNanos;
private int mFrame;
private VsyncEventData mLastVsyncEventData = new VsyncEventData();
public FrameDisplayEventReceiver(Looper looper, int vsyncSource) {
super(looper, vsyncSource, 0);
}
// TODO(b/116025192): physicalDisplayId is ignored because SF only emits VSYNC events for
// the internal display and DisplayEventReceiver#scheduleVsync only allows requesting VSYNC
// for the internal display implicitly.
@Override
public void onVsync(long timestampNanos, long physicalDisplayId, int frame,
VsyncEventData vsyncEventData) {
try {
if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW,
"Choreographer#onVsync " + vsyncEventData.id);
}
// Post the vsync event to the Handler.
// The idea is to prevent incoming vsync events from completely starving
// the message queue. If there are no messages in the queue with timestamps
// earlier than the frame time, then the vsync event will be processed immediately.
// Otherwise, messages that predate the vsync event will be handled first.
long now = System.nanoTime();
if (timestampNanos > now) {
Log.w(TAG, "Frame time is " + ((timestampNanos - now) * 0.000001f)
+ " ms in the future! Check that graphics HAL is generating vsync "
+ "timestamps using the correct timebase.");
timestampNanos = now;
}
if (mHavePendingVsync) {
Log.w(TAG, "Already have a pending vsync event. There should only be "
+ "one at a time.");
} else {
mHavePendingVsync = true;
}
mTimestampNanos = timestampNanos;
mFrame = frame;
mLastVsyncEventData = vsyncEventData;
Message msg = Message.obtain(mHandler, this);
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}
@Override
public void run() {
mHavePendingVsync = false;
doFrame(mTimestampNanos, mFrame, mLastVsyncEventData);
}
}
这个类会接收到底层的VSYNC信号,VSync信号由SurfaceFlinger实现并定时发送,FrameDisplayEventReceiver收到信号后,调用onVsync方法组织消息发送到主线程处理。这个消息主要内容就是run方法里面的doFrame()方法。也就是说,我们必须先提前注册,底层的VSYNC信号产生时才能回调到我们的app中,否则接受不到这个信号。
所以说,当我们调用了 invalidate(),requestLayout(),等之类刷新界面的操作时,并不是马上就会执行这些刷新的操作,而是通过 ViewRootImpl 的 scheduleTraversals() 先向底层注册监听下一个屏幕刷新信号事件,然后等下一个屏幕刷新信号来的时候,才会去通过 performTraversals() 遍历绘制 View 树来执行这些刷新操作。
总结
综上,可以总结出
- 只有同步信号VSYNC到来界面才会刷新
- UI如果没有变化,则不会请求同步信号,界面不会刷新
- 同步信号VSYNC需要申请才会有。
- 同一帧内,如果有多个重绘的请求,scheduleTraversals() 会将其过滤掉,只需要安排一次绘制任务就行了,在下一次VSYNC信号到来时才会调用**performTraversals()**遍历view树并重绘。
- 为了保证绘制任务优先被执行,ViewRootImpl会插入一个同步消息屏障,同步消息不会被处理,以此尽可能的保证在接收到VSYNC信号后能够第一时间进行处理。
边栏推荐
- vim的Dirvish中文文档
- leecode学习笔记-机器人走到终点的最短路径
- 【直播回顾】战码先锋第七期:三方应用开发者如何为开源做贡献
- Experience of epidemic prevention and control, home office and online teaching | community essay solicitation
- 【Proteus仿真】Arduino UNO+数码管显示4x4键盘矩阵按键
- Uncaught Error: [About] is not a <Route> component. All component children of <Routes> must be a <Ro
- 3 years of testing experience. I don't even understand what I really need on my resume. I need 20K to open my mouth?
- AI clothing generation helps you complete the last step of clothing design
- Talking about the advantages of flying book in development work | community essay solicitation
- Software testing salary in first tier cities - are you dragging your feet
猜你喜欢

It is said that Yijia will soon update the product line of TWS earplugs, smart watches and bracelets

Intranet learning notes (5)

DDD concept is complex and difficult to understand. How to design code implementation model in practice?

Getting started with unityshader - Surface Shader

What is the reason for the disconnection of video playback due to the EHOME protocol access of easycvr platform?

Unity存档系统——Json格式的文件

华为、阿里等大厂程序员真的好找对象吗?

The role of software security testing, how to find a software security testing company to issue a report?

会自动化—10K,能做自动化—20K,你搞懂自动化测试没有?

Intranet learning notes (7)
随机推荐
3年测试经验,连简历上真正需要什么都没搞明白,张口就要20k?
[live review] battle code pioneer phase 7: how third-party application developers contribute to open source
产业互联网的概念里有「互联网」字眼,但却是一个和互联网并不关联的存在
NPM package publishing tutorial
同花顺是正规平台吗?同花顺开户安全吗
jwt
Post competition summary of kaggle patent matching competition
LINQ query (3)
对进程内存的实践和思考
PE文件基础结构梳理
[day 26] given the ascending array nums of n elements, find a function to find the subscript of target in nums | learn binary search
Random list random generation of non repeating numbers
电脑端微信用户图片DAT格式解码为图片(TK版)
Network planning | [four network layers] knowledge points and examples
当人们用互联网式的思维和视角来看待产业互联网的时候,其实已陷入到了死胡同
Computing service network: a systematic revolution of multi integration
PE file infrastructure sorting
Uncaught Error: [About] is not a <Route> component. All component children of <Routes> must be a <Ro
Centos7.3 modifying MySQL default password_ Explain centos7 modifying the password of the specified user in MySQL
Redis