view的刷新机制

365网站世界杯怎么进 📅 2025-08-26 10:41:38 👤 admin 👁️ 755 ❤️ 724
view的刷新机制

前言

平时只知道在调用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信号后能够第一时间进行处理。

相关推荐

华为路由器通过 Web 界面配置 DMZ 主机
365bet娱乐游戏

华为路由器通过 Web 界面配置 DMZ 主机

📅 07-15 👁️ 9964
qq邮箱怎么设置密码
365bet娱乐游戏

qq邮箱怎么设置密码

📅 08-24 👁️ 855