屏幕刷新機制小結

  1. Android刷新機制
  2. SurfaceView理解

1、Android屏幕刷新機制canvas

  1. 首先須要瞭解一些基本概念bash

    • 在一個顯示系統裏,通常包括CPU、GPU、Display三部分,CPU負責計算數據,把計算號的數據交給CPU,GPU會對圖形數據進行渲染,渲染後放到buffer裏存起來,而後Display(可稱爲屏幕或者顯示器)負責把buffer裏的數據呈現到屏幕上。顯示過程,簡單來講就是CPU/GPU準備好數據,存入buffer,Display每隔一段時間去buffer裏面取數據,而後顯示出來。Display每次讀取的頻率是固定的,好比16ms一次,可是CPU/GPU寫數據是徹底無規律的。
    • CPU計算數據指的是View樹的繪製過程,也就是Activity對應視圖樹從根佈局DecorView開始遍歷View,分別執行測量、佈局、繪製三個操做過程。咱們常說的16.6ms刷新一次屏幕其實就是底層以固定的頻率將buffer中的屏幕數據顯示出來。
  2. 在以前的幾篇文章裏,例如 Window和WindowManager相關知識點(六) 以及 Android View相關知識點以及原理(四) 內對DecorView和setContentView以及Window的聯繫能夠知道在onResume的時候纔會建立ViewRootImpl將DecorView和Window關聯起來,而且和任意View調用invalidate等刷新同樣都會走ViewRootImpl中的 scheduleTraversals()方法,而後調用Choreographer的postCallBack方法數據結構

    void scheduleTraversals() {
            if (!mTraversalScheduled) {
            	//編號1
                mTraversalScheduled = true;
                //編號2
                mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
                //編號3
                mChoreographer.postCallback(
                        Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
                if (!mUnbufferedInputDispatch) {
                    scheduleConsumeBatchedInput();
                }
                notifyRendererOfFramePending();
                pokeDrawLockIfNeeded();
            }
        }
    複製代碼
  3. Choreographer的postCallBack中傳了一個mTraversalRunnable,你們能夠看下面代碼,run方法內調用了doTraversal方法多線程

    final class TraversalRunnable implements Runnable {
            @Override
            public void run() {
                doTraversal();
            }
        }
        
       void doTraversal() {
          if (mTraversalScheduled) {
          	  //編號4
              mTraversalScheduled = false;
              //編號5
              mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
              if (mProfile) { 
                  Debug.startMethodTracing("ViewAncestor");
              }
              //這裏開始對view樹進行測量、佈局、繪製
              performTraversals();
              if (mProfile) {
                  Debug.stopMethodTracing();
                  mProfile = false;
              }
          }
      }
    複製代碼
  4. 這裏會產生一個疑問,調用了scheduleTraversals方法後,代碼裏只是將Runnable做爲參數傳遞到了Choreographer的postCallback方法中,要想調用doTraversal方法,那必需要有某處執行這個Runnable。爲了解清楚執行邏輯,請看Choreographer的postCallback的代碼異步

    public void postCallback(int callbackType, Runnable action, Object token) {
            postCallbackDelayed(callbackType, action, token, 0);
        }
        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");
         }
         postCallbackDelayedInternal(callbackType, action, token, delayMillis);
     }
     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;
            //編號6
            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);
            }
        }
      }
    複製代碼

    它最終會調用postCallbackDelayedInternal方法,而傳遞進來的delayMillis是0,因此dueTime = now,因此調用的是scheduleFrameLocked方法,接下來看它的調用代碼ide

    //方法
     private void scheduleFrameLocked(long now) {
            if (!mFrameScheduled) {
                mFrameScheduled = true;
                if (USE_VSYNC) {
                    if (DEBUG_FRAMES) {
                        Log.d(TAG, "Scheduling next frame on vsync.");
                    }
                    //是否在主線程
                    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);
                }
            }
        }
        //方法
        private void scheduleVsyncLocked() {
            mDisplayEventReceiver.scheduleVsync();
        }
        //方法 DisplayEventReceiver
        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 {
              nativeScheduleVsync(mReceiverPtr);
          }
      }
      //方法 FrameDisplayEventReceiver類
      private static native void nativeScheduleVsync(long receiverPtr);
    複製代碼

    能夠發現最終調用的是native方法,到這裏就無法跟下去了,換種思路,既然以前把Runnable放到了CallbackQueue中,見第4點的編號6註釋,那麼調用時機必定對應於從CallbackQueue取出Runnable,也就是CallbackQueue的extractDueCallbacksLocked方法,通過查找,能夠得出是doCallbacks方法內調用了該方法( CallbackQueue 是Choreographer的內部類 ) ,而調用doCallbacks方法的是doFrame方法。(這裏能夠看到一個callbackType對應一個Runnable隊列,mCallbackQueue[callbackType])oop

    void doCallbacks(int callbackType, long frameTimeNanos) {
            CallbackRecord callbacks;
            synchronized (mLock) {
                // We use "now" to determine when callbacks become due because it's possible // for earlier processing phases in a frame to post callbacks that should run // in a following phase, such as an input event that causes an animation to start. final long now = System.nanoTime(); callbacks = mCallbackQueues[callbackType].extractDueCallbacksLocked( now / TimeUtils.NANOS_PER_MS); ..... } } void doFrame(long frameTimeNanos, int frame) { final long startNanos; synchronized (mLock) { ...... try { Trace.traceBegin(Trace.TRACE_TAG_VIEW, "Choreographer#doFrame"); AnimationUtils.lockAnimationClock(frameTimeNanos / TimeUtils.NANOS_PER_MS); mFrameInfo.markInputHandlingStart(); doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos); ...... 複製代碼

    那麼doFrame方法是哪裏調用的呢?從以前得出了是調用FrameDisplayEventReceiver的native方法後就跟不下去了,那麼進入這個類中能夠發現它的 run 方法偏偏調用了doFrame方法,而官方對它其中的onVsync方法也有註釋 Called when a vertical sync pulse is received ,說明這是底層回調用的。佈局

    @Override
            public void onVsync(long timestampNanos, int builtInDisplayId, int frame) {
                if (builtInDisplayId != SurfaceControl.BUILT_IN_DISPLAY_ID_MAIN) {
                    Log.d(TAG, "Received vsync from secondary display, but we don't support "
                            + "this case yet. Choreographer needs a way to explicitly request "
                            + "vsync for a specific display to ensure it doesn't lose track "
                            + "of its scheduled vsync.");
                    scheduleVsync();
                    return;
                }
                // Post the vsync event to the Handler.
                long now = System.nanoTime();
                if (timestampNanos > now) {
                    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;
                //編號7
                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);
            }
    複製代碼

    從Handler的處理消息機制來看,咱們知道通常若是傳遞了Runnable,會執行它的run方法,不然看你有沒有指定Callback,有的話會執行你的Callback,沒有則會調用handleMessage,因此 編號7 處,將this傳遞給了Message,後續將執行本身的run方法,繼而執行doFrame方法,而後就會執行doCallbacks方法,而後會執行Runnable對象的run方法,run方法內又會執行doTraversals方法,這就開始刷新view咯post

  5. 小結一下,FrameDisplayEventReceiver繼承DisplayEventReceiver接受底層的VSync信號開始處理UI過程,VSync信號由SurfaceFlinger實現並定時發送。FrameDisplayEventReceiver收到信號後,調用onVsync方法組織消息發送到主線程處理。這個消息的內容主要就是run方法裏面的doFrame了。FrameDsiplayEventReceiver之因此能收到信號,回調onVsync方法,能夠理解爲APP層在調用native方法nativeScheduleVsync時向底層註冊了一個屏幕刷新信號監聽事件,要否則底層怎麼知道APP須要刷新數據呢?而且APP對底層是隱藏的,底層壓根不知道APP的存在。優化

    梳理一下,APP經過native方法向底層註冊了下一幀的屏幕刷新界面,而後在每16.6ms的幀信號到來時,它就會回調onVsync刷新屏幕了

  6. 那麼問題來了,是否是在16.6ms內或者是屏幕刷新前,我能夠無限註冊該監聽事件,也就是一幀內是否是會註冊不少重複監聽

    • 首先看段代碼

      void scheduleTraversals() {
              if (!mTraversalScheduled) {
                  mTraversalScheduled = true;
                  //編號 8 
                  mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
                  mChoreographer.postCallback(
                          Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
               ......
          }
              void doTraversal() {
              if (mTraversalScheduled) {
                  mTraversalScheduled = false;
                  //編號 9 
                  mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
      		......
          }
      複製代碼
    • 首先看scheduleTraversals方法,它在向底層註冊監聽前,有一個mTraversalScheduled變量,該變量默認false,屢次調用scheduleTraversals方法,只要mTraversalScheduled爲false纔會重複註冊,而從doTraversal方法中能夠看出只有接收到一幀信號是該變量纔會重置爲false。那爲何這麼設計呢?查看performTraversals方法就能夠知道,界面刷新調用了這個方法,方法內能夠層層遍歷View樹的,須要刷新的View都會遍歷到並刷新的,因此也就沒有必要重複註冊吧

  7. 接下來細心的人可能會問,在onVsync方法內的 編號7 處代碼,這裏會將刷新消息封裝到Message裏面放到主線程的MessageQueue中,而我們的主線程也是一直在處理MessageQueue裏面的消息的,同一時間只能處理一個Message,若是其它消息耗時,致使刷新消息在16.6ms內還沒處理,該怎麼辦

    • 這種狀況是避免不了的,你們只能儘可能不要在這其中處理過多耗時操做
    • Android系統對其有一個優化,就是 編號 8 和 編號 9 處代碼,能夠先理解爲設置和移除屏障消息的標識。首先是經過 編號 8 處代碼會往隊列裏發送一個同步屏障,主Looper經過next不斷取出隊頭的Message執行,若是隊頭是一個同步屏障消息時,將會遍歷整個隊列,尋找設置了異步標識的消息,找到該消息就取出該消息來執行,不然就讓next方法陷入阻塞階段。因此在屏障消息後面的全部同步消息都將攔截,直到經過doTraversal方法將屏障消息移除隊列。這樣就能保證刷新消息先執行,可是在以前取出的Message仍是會依舊執行,因此說若是Message內內容耗時的話,仍是會影響刷新消息的執行的。
  8. 小結

    • 界面上任意一個View的刷新請求都會調用ViewRootImpl的scheduleTraversals()方法,這個方法內會過濾一幀內的重複調用,保證同一幀內只會進行一遍View樹的遍歷刷新
    • 當調用scheduleTraversals()方法後,會向主線程的消息隊列發送一個同步屏障,攔截這個時刻以後的全部同步消息,直到底層回調onVsync後產生的具備異步標識的刷新界面消息被執行
    • 而後會把刷新界面的操做,也就是doTraversals方法放到Runnable裏,經過Choreographer的postCallback方法以時間戳的形式放到隊列裏面,若是當前是主線程,將直接調用native方法,若是不是主線程,將會以最高優先級,將Message放到主線程,保證儘可能第一個執行該native方法
    • native的這個方法是用來向底層註冊監聽下一個屏幕刷新信號,當下一個屏幕刷新信號發出時,底層就會回調Choreographer的onVsync方法
    • onVsync方法調用後,因爲是具備異步標識的方法,因此不會被同步屏障攔截,就能儘可能保證第一時間取出並刷新界面,同時移除同步屏障
    • 最後就是執行View的遍歷刷新了
    • 若是界面一直保持沒變的話,也就是沒有註冊監聽事件,可是底層的仍是會以每16.6ms固定頻率來切換每一幀的畫面,只是最後這些畫面都是相同的而已。
    • 底層是以固定的頻率來切換屏幕的畫面的,即便CPU計算完了數據,也就是說測量,佈局,繪製等等都算完了,它也不會當即顯示,而是要等到信號來的時候。

2、SurfaceView

  1. 首先列舉最多見的和View的區別
    • View的繪圖效率低,主要用於動畫變化較少的程序,必須在主線程更新
    • SurfaceView繪圖效率高,用於界面更新頻繁的程序,通常在子線程更新
    • SurfaceView擁有獨立的Surface(繪圖表面),即它不與其宿主窗口共享同一個Surface
    • SurfaceView使用雙緩衝機制,播放視頻時畫面更流暢
    • 每一個窗口在SurfaceFlinger服務中都對應有一個Layer,用它來描述它的繪製表面。對於那些具備SurfaceView的窗口來講,每個SurfaceView在SurfaceFlinger服務中還對應於一個獨立的Layer或者LayerBuffer,用來單獨描述它的繪圖表面,以區別於它的宿主窗口的繪圖表面。因此SurfaceView的UI就能夠在一個獨立的線程中進行繪製,能夠不佔用主線程資源,它產生緣由也是爲了應對耗時的操做,例如Camera X。
  2. 部分概念
    • Canvas是Java層構建的數據結構,是給View用的畫布,ViewGroup會將Canvas拆分給子View,在onDraw方法裏將圖形數據繪製在它得到的Canvas上
    • Surface是Native層構建的數據結構,是給SurfaceFlinger用的畫布,它是直接被用來繪製到屏幕上的數據結構
    • 開發者通常所用到的View都是在Canvas進行繪製,而後最頂層的View的Canvas的數據信息會轉換到一個Surface上,SurfaceFlinger會將各個應用窗口的Surface進行合成,而後繪製到屏幕上。
  3. 雙緩衝機制
    • SurfaceView在更新視圖時用到了兩張Canvas,一張frontCanvas和一張backCanvas,每次實際顯示的是frontCanvas,backCanvas存儲的是上一次更改的視圖。當你在播放這一幀的時候,它已經提早幫你加載好後面一幀了,因此播放起來流暢。
    • 當使用lockCanvas()獲取畫布時,獲得的實際是backCanvas而不是正在顯示的frontCanvas,以後咱們再在backCanvas上繪製新的視圖,再經過unlockCanvasAndPost(canvas)此視圖,而後上傳的這張Canvas將替換原來的frontCanvas做爲新的frontCanvas,原來的frontCanvas將切換到後臺做爲backCanvas。至關於多線程交替解析和渲染每一幀視頻數據
  4. SurfaceView是Zorder排序的,默認在宿主Window的後面,SurfaceView經過在Window上面」挖洞「(設置透明區域進行顯示)
  5. SurfaceView是一個有本身的Surface的View,它的渲染能夠放到單獨線程而不是主線程中,其缺點是不能作變形和動畫
  6. SurfaceTexture能夠用做非直接輸出的內容流,這樣就提供二次處理的機會,與SurfaceView直接輸出相比,這樣會有若干幀的延遲,內存消耗也會大一些
  7. TextureView是在View hierachy中作繪製,所以它通常是在主線程作的
  8. 具體介紹能夠參考 SurfaceTexture,TextureView, SurfaceView 和 GLSurfaceView 區別知多少
相關文章
相關標籤/搜索