Android屏幕刷新機制

60Hz刷新頻率由來

  • 12fps:由於人類眼睛的特殊生理結構,如果所看畫面之幀率高於每秒約10~12幀的時候,就會認爲是連貫的
  • 24fps:有聲電影的拍攝及播放幀率均爲美秒24幀,對一般人而言已經算可接受
  • 30fps:早期的高動態電子遊戲,幀率少於美秒30幀的話就會顯得不連貫,這是因爲沒有動態模糊使流暢度降低
  • 60fps:在與手機交互的過程中,如觸摸和反饋 60幀以下是能感覺出來的,60幀以上不能察覺變化
  • 當幀率低於60fps時感覺畫面有卡頓遲滯現象

Android系統每隔16ms發出VSYNC信號(1000ms/60=16.66ms),觸發對UI進行渲染,如果每次渲染都成功,這樣就能夠達到流暢的畫面所需要的60fps,爲了能夠實現60fps,這意味着計算渲染的大多數操作都必須在16ms內完成。

屏幕刷新機制大致流程介紹

首先應用程序向系統服務申請一塊buffer(緩存),系統服務返回buffer,應用拿到buffer之後就可以進行繪製,繪製完之後將buffer提交給系統服務,系統服務將buffer寫到屏幕的一塊緩存區,屏幕會以一定的幀率刷新,每次刷新的時候,就會從緩存區將圖像數據讀取顯示出來。如果緩存區沒有新的數據,就一直用舊的數據,這樣屏幕看起來就沒有變
在這裏插入圖片描述
屏幕的圖像緩存不止一個,假如只有一個緩存,如果屏幕這邊正在讀緩存,而系統服務又在寫緩存,這有可能導致屏幕顯示不正常,如一半顯示第一幀圖像的畫面,另一半顯示第二幀圖像的畫面。如何避免這種問題的發生呢?可以弄多個緩存,屏幕從一塊緩存讀取數據顯示,系統服務向另一塊緩存寫入數據。如果要顯示下一幀圖像,將兩個緩存換一下即可,即屏幕從緩存2讀取顯示,系統服務向緩存1寫入數據。
在這裏插入圖片描述
vsync(垂直同步機制)是固定頻率的脈衝信號,屏幕根據這個信號週期性的刷新,屏幕每次收到這個信號,就從屏幕緩存區讀取一幀的圖像數據進行顯示,而繪製是由應用端(任何時候都有可能)發起的,如果屏幕收到vsync信號,但是這一幀的還沒有繪製完,就會顯示上一幀的數據,這並不是因爲繪製這一幀的時間過長(超過了信號發送週期),只是信號快來的時候纔開始繪製,如果頻繁的出現的這種情況,用戶就會感知屏幕的卡頓,即使繪製時間優化的再好也無濟於事,因爲這是底層刷新機制的缺陷。
在這裏插入圖片描述

當然系統提供瞭解決方案,如果繪製和vsync信號同步就好了,每次收到vsync信號時,一方面屏幕獲取圖像數據刷新界面,另一方面應用開始繪製準備下一幀圖像數據。如果優化的好,每一幀圖像繪製控制在16ms以內,就可以非常流暢了。那麼問題來了,應用層view的重繪一般調用requestLayout觸發,這個函數隨時都能調用,如何控制只在vsync信號來時觸發重繪呢?有一個關鍵類Choreography(舞蹈指導,編舞),它最大的作用就是你往裏面發送一個消息,這個消息最快也要等到下一個vsync信號來的時候觸發。比如說繪製可能隨時發起,封裝一個Runnable丟給Choreography,下一個vsync信號來的時候,開始處理消息,然後真正的開始界面的重繪了。相當於UI繪製的節奏完全由Choreography來控制。
在這裏插入圖片描述

Choreography原理分析

就從比較熟系的requestLayout方法開始吧,進行UI操作時,通過checkThread方法進行線程檢查,UI操作是否在UI線程,然後調用scheduleTraversals方法:

@Override
    public void requestLayout() {
        if (!mHandlingLayoutInLayoutRequest) {
            checkThread();
            mLayoutRequested = true;
            scheduleTraversals();
        }
    }

在scheduleTraversals方法裏面,主要做了兩件事,一件事是向線程的消息隊列中加入了syncBarrier,另外就是往mChoreographer的mCallbackQueue數組插入了一個callback(需要執行的相關操作,這裏主要是UI繪製),syncBarrier是一個屏障,將它插入到消息隊列後,這個屏障後面的普通消息就不能處理了,等到屏障撤除之後才能處理。但是這個屏障對異步消息是沒有影響的。主要是有些類型的消息非常緊急,需要馬上處理。如果普通消息太多,容易耽誤事(影響緊急消息的執行),所以插入了一個屏障,優先處理異步消息。請求同步Vsync信號,就是一個異步消息,就是請求系統服務SurfaceFlinger在下一次Vsync信號過來時,立即通知我們,我們就可以立即執行mChoreographer的callback數組裏面對應callback的相關操作,即UI繪製了,這裏先簡單提一下,後面會具體分析。

@UnsupportedAppUsage
    void scheduleTraversals() {
        if (!mTraversalScheduled) {// 註釋1
        	// 註釋2
            mTraversalScheduled = true;
            // 註釋3
            // 插入同步屏障syncBarrier到消息隊列,擋住普通的同步消息,優先執行異步消息
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
            // 註釋4
            
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
            if (!mUnbufferedInputDispatch) {
                scheduleConsumeBatchedInput();
            }
            ...
        }
    }

Choreography和ViewRootImpl一起創建的,是通過ThreadLocal存儲的,即在不同的線程調用getInstance得到的是不同的Choreography對象。

public static Choreographer getInstance() {
		// ThreadLocal<Choreography>
        return sThreadInstance.get();
    }

Choreographer要執行的操作就是mTraversalRunnable即TraversalRunnable的run方法,即doTraversal

final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
final class TraversalRunnable implements Runnable {
    @Override
    public void run() {
        doTraversal();
    }
}

順着代碼調用邏輯進入doTraversal方法,在scheduleTraversals方法中mTraversalScheduled賦值爲true,在doTraversal註釋1處賦值爲false,即使多次調用requestLayout方法,也不會多次執行,因爲只有下一次vsync信號過來的時候,執行doTraveersal方法時纔會置爲false,即在一個vsync信號週期內,只會觸發一次界面重繪。

void doTraversal() {
    if (mTraversalScheduled) {
        // 註釋1
        mTraversalScheduled = false;
        // 註釋2
        // 移除同步消息屏障
        mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
		...
		// 開始繪製流程
        performTraversals();

        ...
        }
    }
}

到目前爲止,我們已經知道Choreography控制UI繪製和Vsync同步的基本原理了,但是同步屏障擋住同步消息,優先執行異步消息,關於異步消息的相關邏輯貌似目前還沒有涉及到,接着看下去,進入Choreography的postCallback方法:

@TestApi
public void postCallback(int callbackType, Runnable action, Object token) {
    postCallbackDelayed(callbackType, action, token, 0);
}

順着調用邏輯進入postCallbackDelayed方法

@TestApi
public void postCallbackDelayed(int callbackType,
        Runnable action, Object token, long delayMillis) {
    if (action == null) {
        throw new IllegalArgumentException("action must not be null");
    }
    if (callbackType < 0 || callbackType > CALLBACK_LAST) {
        throw new IllegalArgumentException("callbackType is invalid");
    }
	// 註釋1
    postCallbackDelayedInternal(callbackType, action, token, delayMillis);
}

我們需要關注的是註釋1處的postCallbackDelayedInternal方法:

private void postCallbackDelayedInternal(int callbackType,
        Object action, Object token, long delayMillis) {
    
    ...
    synchronized (mLock) {
        // 註釋1
        // 獲取當前時間
        final long now = SystemClock.uptimeMillis();
        // 註釋2
        // 超時時間或者理解爲觸發時間
        final long dueTime = now + delayMillis;
        // 註釋3
        // 將執行動作放在mCallbackQueue數組中
        mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);

        if (dueTime <= now) {// 註釋4//如果已經到觸發時間就註冊請求垂直同步信號
            scheduleFrameLocked(now);
        } else {
            // 註釋5
            // 如果還沒有到觸發時間,使用handler在發送一個延時的異步消息。
            // 這個延時消息會在到觸發時間的時候執行
            Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
            msg.arg1 = callbackType;
            msg.setAsynchronous(true);
            mHandler.sendMessageAtTime(msg, dueTime);
        }
    }
}

註釋3處,Choreography裏面有一個mCallbackQueue數組,裏面的每一個元素都是一個callback的單鏈表,添加callback一方面要根據callback的類型callbackType插到對應的單鏈表,另一方面要根據callback執行的時間順序排序,越是馬上要執行的callback,越是插入到鏈表的前面,然後等待被調用。如果還沒有到觸發時間就走註釋5處邏輯,發送延時異步消息請求同步vsync信號,如果到了觸發時間,進入註釋4處的scheduleFrameLocked方法:

private void scheduleFrameLocked(long now) {
    ...

    // 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.
    // 如果在Choreography的UI線程中,就直接調用立即安排垂直同步,否則就發送一個消息到UI線程
    // 儘快安排請求一個垂直同步
    if (isRunningOnLooperThreadLocked()) {
    	scheduleVsyncLocked();
    } else {
        Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_VSYNC);
        // 異步消息
        msg.setAsynchronous(true);
        // 插入到消息隊列的頭部,可見消息之緊急因爲要告訴SurfaceFlinger,
        // vsync信號過來時第一時間通知
        mHandler.sendMessageAtFrontOfQueue(msg);
    }
    ...     
}

這個方法的作用很明確,就是請求同步Vsync信號,就是說vsync信號過來的時候,讓系統服務SurfaceFlinger第一時間通知我們,我們去執行繪製的相關操作。如果不在Choreography的UI線程,就發送異步消息讓UI線程請求同步Vsync信號,再看註釋5處之後的邏輯

private final class FrameHandler extends Handler {
        public FrameHandler(Looper looper) {
            super(looper);
        }

        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case MSG_DO_FRAME:
                    doFrame(System.nanoTime(), 0);
                    break;
                case MSG_DO_SCHEDULE_VSYNC:
                    doScheduleVsync();
                    break;
                case MSG_DO_SCHEDULE_CALLBACK:
                //postCallbackDelayedInternal()方法中當未到期的時候發送過來的
                    doScheduleCallback(msg.arg1);
                    break;
            }
        }
    }

以上代碼我們可以看出這個,FramHandler拿到 whate屬性值爲MSG_DO_SCHEDULE_CALLBACK的時候會去執行 doScheduleCallback(msg.arg1)方法

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

這個方法中先是做了一些判斷,mFrameSceduled爲false 並且hasDueCallbacksLocked()這個方法的返回值爲true,看方法名就能猜出這個callback是否到了觸發時間,下面我們再分析這個。最終如果滿足條件的情況下它會調用 scheduleFrameLocked()這個方法,也就是說註釋4處到了觸發時間,註釋5處還沒有到觸發時間,就發送一個延遲異步消息,到了觸發時間,最終都是調用scheduleVsyncLocked方法,該後續調用邏輯如下

@UnsupportedAppUsage
private void scheduleVsyncLocked() {
    mDisplayEventReceiver.scheduleVsync();
}
@UnsupportedAppUsage
public 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 {
        // native方法
        // 請求同步vsync信號
        nativeScheduleVsync(mReceiverPtr);
    }
}

前面分析了請求同步vsync信號的過程,當下一個vsync信號發生的時候,SurfaceFlinger就會通知我們,就會回調該類的onVsync函數,參數timestampNanos就是vsync的時間戳,該函數裏面會發送一個消息到Choreography的工作線程裏面去了,這裏並不是要切換工作線程,因爲onVsync本身就在Choreography的工作線程。這個消息帶了時間戳的,表示消息觸發的時間,有了這個時間戳,就可以按照時間戳的順序來處理消息。到時間了,就會去執行run方法,即執行doFrame方法

 private final class FrameDisplayEventReceiver extends DisplayEventReceiver
            implements Runnable {
        ...
        @Override
        public void onVsync(long timestampNanos, int builtInDisplayId, int frame) {
            mTimestampNanos = timestampNanos;
            mFrame = frame;
            Message msg = Message.obtain(mHandler, this);
            msg.setAsynchronous(true);
            mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
        }
        @Override
        public void run() {
            mHavePendingVsync = false;
            doFrame(mTimestampNanos, mFrame);
        }
    }

進入doFrame方法:

 void doFrame(long frameTimeNanos, int frame) {
        final long startNanos;
        synchronized (mLock) {
            if (!mFrameScheduled) {
                return; // no work to do
            }

            //當前時間
            startNanos = System.nanoTime();
            //當前時間和垂直同步時間
            final long jitterNanos = startNanos - frameTimeNanos;
            //垂直同步時間和當前時間的差值如果大於一個週期就修正一下
            if (jitterNanos >= mFrameIntervalNanos) {
            //取插值和始終週期的餘數
               final long lastFrameOffset = jitterNanos % mFrameIntervalNanos;
               //當前時間減去上一步得到的餘數當作最新的始終信號時間
               frameTimeNanos = startNanos - lastFrameOffset;
            }
            //垂直同步時間上一次時間還小,就安排下次垂直同步,直接返回
            if (frameTimeNanos < mLastFrameTimeNanos) {
                scheduleVsyncLocked();
                return;
            }
            mFrameInfo.setVsync(intendedFrameTimeNanos, frameTimeNanos);
            mFrameScheduled = false;
            mLastFrameTimeNanos = frameTimeNanos;
        }

        try {
            Trace.traceBegin(Trace.TRACE_TAG_VIEW, "Choreographer#doFrame");
            AnimationUtils.lockAnimationClock(frameTimeNanos / TimeUtils.NANOS_PER_MS);

            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 {
            AnimationUtils.unlockAnimationClock();
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }

        if (DEBUG_FRAMES) {
            final long endNanos = System.nanoTime();
            Log.d(TAG, "Frame " + frame + ": Finished, took "
                    + (endNanos - startNanos) * 0.000001f + " ms, latency "
                    + (startNanos - frameTimeNanos) * 0.000001f + " ms.");
        }
    }

doFrame分爲兩個階段,先看第一個階段,參數frameTimeNanos表示這一幀的時間戳先計算當前時間和這個時間戳的間隔有多大,間隔越大,表示要延時了,如果延時超過一個週期(mFrameIntervalNanos), 就要計算到底延遲了幾個週期,如果延遲週期數(丟幀數,跳過的幀數)達到一個常量SKIPPED_FRAME_WARNING_LIMIT ,就會打印日誌"應用在主線程做了太多的事情(耗時操作)"導致繪製延遲,丟幀。第二階段就是處理callback了,callback有四種類型,每種類型對應一個單鏈表callbackQueue,給vsync事件分別分發到四種callback,然後執行對應的doCallbacks函數,單鏈表裏面的callback是有時間戳的,只有到了時間的的callback纔會回調,extractDueCallbacksLocked從callbackQueue裏面取出到了時間的callback,然後在循環裏面執行他們的run函數,requestLayout裏面的scheduleTraversals函數傳的callback是什麼?就是準備繪製,mTraversalRunnable的run方法其實調用的是performTraversals,真正的開始執行UI繪製流程。

總結

大致調用流程圖如下:
在這裏插入圖片描述
應用程序調用requestLayout發起重繪,通過Choreographer發送異步消息,請求同步vsync信號,即下一次vsync信號過來時,系統服務SurfaceFlinger在第一時間通知我們,觸發UI繪製。雖然可以手動多次調用,但是在一個vsync週期內,requestLayout只會執行一次。

常見的問題

1.丟幀一般是什麼原因引起的?
答:主線程有耗時操作,耽誤了view的繪製

2.Android刷新頻率60幀/秒,每隔16ms調onDraw繪製一次?
答: 60幀/秒也是vsync信號的頻率,但不一定每次vsync信號都會去繪製,先要應用端主動發起重繪,纔會向SurfaceFlinger請求接收vsync信號,這樣當vsync信號來的時候,纔會真正去繪製。

3.onDraw執行完之後屏幕會馬上刷新麼?
答: 不會馬上刷新,會等到下一次vsync信號時纔會刷新。
4.如果界面沒有重繪,還會每隔16ms刷新屏幕麼?
答:界面沒有重繪,應用就不會收的vsync信號,屏幕還是會刷新,畫面數據用的是舊的,看起來沒什麼變化而已
5.如果屏幕快要刷新的時候纔去onDraw繪製會丟幀麼?
答: 重繪不會立即執行,而是等到下一次vsync信號來時纔開始, 所以什麼時候發起重繪影響不大

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章