網站首頁 編程語言 正文
前言
屏幕刷新幀率不穩定,掉幀嚴重,無法保證每秒60幀,導致屏幕畫面撕裂;
今天我們來講解下VSYNC機制和UI刷新流程
一、 Vsync信號詳解
1、屏幕刷新相關知識點
- 屏幕刷新頻率: 一秒內屏幕刷新的次數(一秒內顯示了多少幀的圖像),單位 Hz(赫茲),如常見的 60 Hz。刷新頻率取決于硬件的固定參數(不會變的);
- 逐行掃:顯示器并不是一次性將畫面顯示到屏幕上,而是從左到右邊,從上到下逐行掃描,順序顯示整屏的一個個像素點,不過這一過程快到人眼無法察覺到變化。以 60 Hz 刷新率的屏幕為例,這一過程即 1000 / 60 ≈ 16ms;
- 幀率: 表示 GPU 在一秒內繪制操作的幀數,單位 fps。例如在電影界采用 24 幀的速度足夠使畫面運行的非常流暢。而 Android 系統則采用更加流程的 60 fps,即每秒鐘GPU最多繪制 60 幀畫面。幀率是動態變化的,例如當畫面靜止時,GPU 是沒有繪制操作的,屏幕刷新的還是buffer中的數據,即GPU最后操作的幀數據;
- 屏幕流暢度:即以每秒60幀(每幀16.6ms)的速度運行,也就是60fps,并且沒有任何延遲或者掉幀;
- FPS:每秒的幀數;
- 丟幀:在16.6ms完成工作卻因各種原因沒做完,占了后n個16.6ms的時間,相當于丟了n幀;
2、VSYNC機制
VSync機制: Android系統每隔16ms發出VSYNC信號,觸發對UI進行渲染,VSync是Vertical Synchronization(垂直同步)的縮寫,是一種在PC上很早就廣泛使用的技術,可以簡單的把它認為是一種定時中斷。而在Android 4.1(JB)中已經開始引入VSync機制;
VSync機制下的繪制過程;CPU/GPU接收vsync信號,Vsync每16ms一次,那么在每次發出Vsync命令時,CPU都會進行刷新的操作。也就是在每個16ms的第一時間,CPU就會響應Vsync的命令,來進行數據刷新的動作。CPU和GPU的刷新時間,和Display的FPS是一致的。因為只有到發出Vsync命令的時候,CPU和GPU才會進行刷新或顯示的動作。CPU/GPU接收vsync信號提前準備下一幀要顯示的內容,所以能夠及時準備好每一幀的數據,保證畫面的流暢;?
可見vsync信號沒有提醒CPU/GPU工作的情況下,在第一個16ms之內,一切正常。然而在第二個16ms之內,幾乎是在時間段的最后CPU才計算出了數據,交給了Graphics Driver,導致GPU也是在第二段的末尾時間才進行了繪制,整個動作延后到了第三段內。從而影響了下一個畫面的繪制。這時會出現Jank(閃爍,可以理解為卡頓或者停頓)。這時候CPU和GPU可能被其他操作占用了,這就是卡頓出現的原因;
二、UI刷新原理流程
1、VSYNC流程示意
當我們通過setText改變TextView內容后,UI界面不會立刻改變,APP端會先向VSYNC服務請求,等到下一次VSYNC信號觸發后,APP端的UI才真的開始刷新,基本流程如下:
setText最終調用invalidate申請重繪,最后會通過ViewParent遞歸到ViewRootImpl的invalidate,請求VSYNC,在請求VSYNC的時候,會添加一個同步柵欄,防止UI線程中同步消息執行,這樣做為了加快VSYNC的響應速度,如果不設置,VSYNC到來的時候,正在執行一個同步消息;
2、view的invalidate
View會遞歸的調用父容器的invalidateChild,逐級回溯,最終走到ViewRootImpl的invalidate
void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,
boolean fullInvalidate) {
// Propagate the damage rectangle to the parent view.
final AttachInfo ai = mAttachInfo;
final ViewParent p = mParent;
if (p != null && ai != null && l < r && t < b) {
final Rect damage = ai.mTmpInvalRect;
damage.set(l, t, r, b);
p.invalidateChild(this, damage);
}
ViewRootImpl.java
void invalidate() {
mDirty.set(0, 0, mWidth, mHeight);
if (!mWillDrawSoon) {
scheduleTraversals();
}
}
ViewRootImpl會調用scheduleTraversals準備重繪,但是,重繪一般不會立即執行,而是往Choreographer的Choreographer.CALLBACK_TRAVERSAL隊列中添加了一個mTraversalRunnable,同時申請VSYNC,這個mTraversalRunnable要一直等到申請的VSYNC到來后才會被執行;
3、scheduleTraversals
ViewRootImpl.java
// 將UI繪制的mTraversalRunnable加入到下次垂直同步信號到來的等待callback中去
// mTraversalScheduled用來保證本次Traversals未執行前,不會要求遍歷兩邊,浪費16ms內,不需要繪制兩次
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
// 防止同步柵欄,同步柵欄的意思就是攔截同步消息
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
// postCallback的時候,順便請求vnsc垂直同步信號scheduleVsyncLocked
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
<!--添加一個處理觸摸事件的回調,防止中間有Touch事件過來-->
if (!mUnbufferedInputDispatch) {
scheduleConsumeBatchedInput();
}
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}
4、申請VSYNC同步信號
Choreographer知識點在上個文章詳細介紹過;
Choreographer.java
private void postCallbackDelayedInternal(int callbackType,
Object action, Object token, long delayMillis) {
synchronized (mLock) {
final long now = SystemClock.uptimeMillis();
final long dueTime = now + delayMillis;
mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);
if (dueTime <= now) {
<!--申請VSYNC同步信號-->
scheduleFrameLocked(now);
}
}
}
5、scheduleFrameLocked
// mFrameScheduled保證16ms內,只會申請一次垂直同步信號
// scheduleFrameLocked可以被調用多次,但是mFrameScheduled保證下一個vsync到來之前,不會有新的請求發出
// 多余的scheduleFrameLocked調用被無效化
private void scheduleFrameLocked(long now) {
if (!mFrameScheduled) {
mFrameScheduled = true;
if (USE_VSYNC) {
if (isRunningOnLooperThreadLocked()) {
scheduleVsyncLocked();
} else {
// 因為invalid已經有了同步柵欄,所以必須mFrameScheduled,消息才能被UI線程執行
Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_VSYNC);
msg.setAsynchronous(true);
mHandler.sendMessageAtFrontOfQueue(msg);
}
}
}
}
- 在當前申請的VSYNC到來之前,不會再去請求新的VSYNC,因為16ms內申請兩個VSYNC沒意義;
- 再VSYNC到來之后,Choreographer利用Handler將FrameDisplayEventReceiver封裝成一個異步Message,發送到UI線程的MessageQueue;
6、FrameDisplayEventReceiver
private final class FrameDisplayEventReceiver extends DisplayEventReceiver
implements Runnable {
private boolean mHavePendingVsync;
private long mTimestampNanos;
private int mFrame;
public FrameDisplayEventReceiver(Looper looper) {
super(looper);
}
@Override
public void onVsync(long timestampNanos, int builtInDisplayId, int frame) {
long now = System.nanoTime();
if (timestampNanos > now) {
<!--正常情況,timestampNanos不應該大于now,一般是上傳vsync的機制出了問題-->
timestampNanos = now;
}
<!--如果上一個vsync同步信號沒執行,那就不應該相應下一個(可能是其他線程通過某種方式請求的)-->
if (mHavePendingVsync) {
Log.w(TAG, "Already have a pending vsync event. There should only be "
+ "one at a time.");
} else {
mHavePendingVsync = true;
}
<!--timestampNanos其實是本次vsync產生的時間,從服務端發過來-->
mTimestampNanos = timestampNanos;
mFrame = frame;
Message msg = Message.obtain(mHandler, this);
<!--由于已經存在同步柵欄,所以VSYNC到來的Message需要作為異步消息發送過去-->
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
}
@Override
public void run() {
mHavePendingVsync = false;
<!--這里的mTimestampNanos其實就是本次Vynsc同步信號到來的時候,但是執行這個消息的時候,可能延遲了-->
doFrame(mTimestampNanos, mFrame);
}
}
- 之所以封裝成異步Message,是因為前面添加了一個同步柵欄,同步消息不會被執行;
- UI線程被喚起,取出該消息,最終調用doFrame進行UI刷新重繪;
7、doFrame
void doFrame(long frameTimeNanos, int frame) {
final long startNanos;
synchronized (mLock) {
<!--做了很多東西,都是為了保證一次16ms有一次垂直同步信號,有一次input 、刷新、重繪-->
if (!mFrameScheduled) {
return; // no work to do
}
long intendedFrameTimeNanos = frameTimeNanos;
startNanos = System.nanoTime();
final long jitterNanos = startNanos - frameTimeNanos;
<!--檢查是否因為延遲執行掉幀,每大于16ms,就多掉一幀-->
if (jitterNanos >= mFrameIntervalNanos) {
final long skippedFrames = jitterNanos / mFrameIntervalNanos;
<!--跳幀,其實就是上一次請求刷新被延遲的時間,但是這里skippedFrames為0不代表沒有掉幀-->
if (skippedFrames >= SKIPPED_FRAME_WARNING_LIMIT) {
<!--skippedFrames很大一定掉幀,但是為 0,去并非沒掉幀-->
Log.i(TAG, "Skipped " + skippedFrames + " frames! "
+ "The application may be doing too much work on its main thread.");
}
final long lastFrameOffset = jitterNanos % mFrameIntervalNanos;
<!--開始doFrame的真正有效時間戳-->
frameTimeNanos = startNanos - lastFrameOffset;
}
if (frameTimeNanos < mLastFrameTimeNanos) {
<!--這種情況一般是生成vsync的機制出現了問題,那就再申請一次-->
scheduleVsyncLocked();
return;
}
<!--intendedFrameTimeNanos是本來要繪制的時間戳,frameTimeNanos是真正的,可以在渲染工具中標識延遲VSYNC多少-->
mFrameInfo.setVsync(intendedFrameTimeNanos, frameTimeNanos);
<!--移除mFrameScheduled判斷,說明處理開始了,-->
mFrameScheduled = false;
<!--更新mLastFrameTimeNanos-->
mLastFrameTimeNanos = frameTimeNanos;
}
try {
<!--真正開始處理業務-->
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "Choreographer#doFrame");
<!--處理打包的move事件-->
mFrameInfo.markInputHandlingStart();
doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos);
<!--處理動畫-->
mFrameInfo.markAnimationsStart();
doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);
<!--處理重繪-->
mFrameInfo.markPerformTraversalsStart();
doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);
<!--提交->
doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}
- doFrame也采用了一個boolean遍歷mFrameScheduled保證每次VSYNC中,只執行一次,可以看到,為了保證16ms只執行一次重繪,加了好多次層保障;
- doFrame里除了UI重繪,其實還處理了很多其他的事,比如檢測VSYNC被延遲多久執行,掉了多少幀,處理Touch事件(一般是MOVE),處理動畫,以及UI;
- 當doFrame在處理Choreographer.CALLBACK_TRAVERSAL的回調時(mTraversalRunnable),才是真正的開始處理View重繪;
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}
回到ViewRootImpl調用doTraversal進行View樹遍歷;
8、doTraversal
// 這里是真正執行了,
void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
<!--移除同步柵欄,只有重繪才設置了柵欄,說明重繪的優先級還是挺高的,所有的同步消息必須讓步-->
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
performTraversals();
}
}
- doTraversal會先將柵欄移除,然后處理performTraversals,進行測量、布局、繪制,提交當前幀給SurfaceFlinger進行圖層合成顯示;
- 以上多個boolean變量保證了每16ms最多執行一次UI重繪;
9、UI局部重繪
View重繪刷新,并不會導致所有View都進行一次measure、layout、draw,只是這個待刷新View鏈路需要調整,剩余的View可能不需要浪費精力再來一遍;
View.java
public RenderNode updateDisplayListIfDirty() {
final RenderNode renderNode = mRenderNode;
...
if ((mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == 0
|| !renderNode.isValid()
|| (mRecreateDisplayList)) {
<!--失效了,需要重繪-->
} else {
<!--依舊有效,無需重繪-->
mPrivateFlags |= PFLAG_DRAWN | PFLAG_DRAWING_CACHE_VALID;
mPrivateFlags &= ~PFLAG_DIRTY_MASK;
}
return renderNode;
}
繪制總結
- android最高60FPS,是VSYNC及決定的,每16ms最多一幀;
- VSYNC要客戶端主動申請,才會有;
- 有VSYNC到來才會刷新;
- UI沒更改,不會請求VSYNC也就不會刷新;
原文鏈接:https://juejin.cn/post/7174617975168139319
相關推薦
- 2022-06-30 React-hooks中的useEffect使用步驟_React
- 2022-08-02 Android開發自定義雙向SeekBar拖動條控件_Android
- 2022-12-08 Matlab實現獲取文件夾下所有指定后綴的文件_C 語言
- 2022-10-15 python?FastApi實現數據表遷移流程詳解_python
- 2022-09-08 python輾轉相除法求最大公約數和最小公倍數的實現_python
- 2022-07-31 C語言算法積累加tag的循環隊列_C 語言
- 2023-02-01 Bat腳本-timeout?命令(延時執行)_DOS/BAT
- 2022-12-01 Django中ModelForm組件的簡單配置與使用教程_python
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支