Android 面試進階指南目錄php
計算機網絡java
- http 速查 [1]
Android面試
- 嘮嘮任務棧,返回棧和啓動模式 [2]
- 嘮嘮 Activity 的生命週期 [3]
- 扒一扒 Context [4]
- 爲何不能使用 Application Context 顯示 Dialog? [5]
- OOM 能夠被 try catch 嗎? [6]
- Activity.finish() 以後十秒纔回調 onDestroy ? [7]
- 如何監測應用的 FPS ? [8]
即便你不知道 FPS,但你必定據說過這麼一句話,在 Android 中,每一幀的繪製時間不要超過 16.67ms。那麼,這個 16.67ms 是怎麼來的呢?就是由 FPS 決定的。數組
FPS,Frame Per Second,每秒顯示的幀數,也叫 幀率。Android 設備的 FPS 通常是 60,也即每秒要刷新 60 幀,因此留給每一幀的繪製時間最多隻有 1000/60 = 16.67ms 。一旦某一幀的繪製時間超過了限制,就會發生 掉幀,用戶在連續兩幀會看到一樣的畫面。網絡
監測 FPS 在必定程度上能夠反應應用的卡頓狀況,原理也很簡單,但前提是你對屏幕刷新機制和繪製流程很熟悉。因此我不會直接進入主題,讓咱們先從 View.invalidate() 提及。app
要探究屏幕刷新機制和 View 繪製流程,View.invalidate() 無疑是個好選擇,它會發起一次繪製流程。異步
> View.java public void invalidate() { invalidate(true); } public void invalidate(boolean invalidateCache) { invalidateInternal(0, 0, mRight - mLeft, mBottom - mTop, invalidateCache, true); } void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache, boolean fullInvalidate) { ...... 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); // 調用 ViewGroup.invalidateChild() p.invalidateChild(this, damage); } ...... }
這裏調用到 ViewGroup.invalidateChild() 。socket
> ViewGroup.java public final void invalidateChild(View child, final Rect dirty) { final AttachInfo attachInfo = mAttachInfo; ...... ViewParent parent = this; if (attachInfo != null) { ...... do { View view = null; if (parent instanceof View) { view = (View) parent; } ...... parent = parent.invalidateChildInParent(location, dirty); ...... } while (parent != null); } }
這裏有一個遞歸,不停的調用父 View 的 invalidateChildInParent() 方法,直到最頂層父 View 爲止。這很好理解,僅靠 View 自己是沒法繪製本身的,必須依賴最頂層的父 View 才能夠測量,佈局,繪製整個 View 樹。可是最頂層的父 View 是誰呢?是 setContentView() 傳入的佈局文件嗎?不是,它解析以後被塞進了 DecorView 中。是 DecorView 嗎?也不是,它也是有父親的。ide
DecorView 的 parent 是誰呢?這就得來到 ActivityThread.handleResume() 方法中。函數
> ActivityThread.java public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward, String reason) { ...... // 1. 回調 onResume() final ActivityClientRecord r = performResumeActivity(token, finalStateRequest, reason); ...... View decor = r.window.getDecorView(); decor.setVisibility(View.INVISIBLE); ViewManager wm = a.getWindowManager(); // 2. 添加 decorView 到 WindowManager wm.addView(decor, l); ...... }
第二步中實際調用的是 WindowManagerImpl.addView() 方法,WindowManagerImpl 中又調用了 WindowManagerGlobal.addView() 方法。
> WindowManagerGlobal.java // 參數 view 就是 DecorView public void addView(View view, ViewGroup.LayoutParams params, Display display, Window parentWindow) { ...... ViewRootImpl root; // 1. 初始化 ViewRootImpl root = new ViewRootImpl(view.getContext(), display); mViews.add(view); mRoots.add(root); // 2. 重點在這 root.setView(view, wparams, panelParentView); ...... }
跟進 ViewRootImpl.setView() 方法。
> ViewRootImpl.java // 參數 view 就是 DecorView public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) { synchronized (this) { if (mView == null) { mView = view; // 1. 發起首次繪製 requestLayout(); // 2. Binder 調用 Session.addToDisplay(),將 window 添加到屏幕 res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes, getHostVisibility(), mDisplay.getDisplayId(), mWinFrame, mAttachInfo.mContentInsets, mAttachInfo.mStableInsets, mAttachInfo.mOutsets, mAttachInfo.mDisplayCutout, mInputChannel); // 3. 重點在這,注意 view 是 DecorView,this 是 ViewRootImpl 自己 view.assignParent(this); } } }
跟進 View.assignParent() 方法。
> View.java // 參數 parent 是 ViewRootImpl void assignParent(ViewParent parent) { if (mParent == null) { mParent = parent; } else if (parent == null) { mParent = null; } else { throw new RuntimeException("view " + this + " being added, but" + " it already has a parent"); } }
還記得咱們跟了這麼久在幹嗎嗎?爲了探究 View 的刷新流程,咱們跟着 View.invalidate() 方法一路追到 ViewGroup.invalidateChild() ,其中遞歸調用 parent 的 invalidateChildInParent() 方法。因此咱們在 給 DecorView 找爸爸 。如今很清晰了,DecorView 的爸爸就是 ViewRootImpl ,因此最終調用的就是 ViewRootImpl.invalidateChildInParent() 方法。
> ViewRootImpl.java public ViewParent invalidateChildInParent(int[] location, Rect dirty) { // 1. 線程檢查 checkThread(); if (dirty == null) { // 2. 調用 scheduleTraversals() invalidate(); return null; } else if (dirty.isEmpty() && !mIsAnimating) { return null; } ...... // 3. 調用 scheduleTraversals() invalidateRectOnScreen(dirty); return null; }
不管是註釋 2 處的 invalite() 仍是註釋 3 處的 invalidateRectOnScreen() ,最終都會調用到 scheduleTraversals() 方法。
scheduleTraversals() 在 View 繪製流程中是個極其重要的方法,我不得不單獨開一節來聊聊它。
上一節中,咱們從 View.invalidate() 方法開始追蹤,一直跟到 ViewRootImpl.scheduleTraversals() 方法。
> ViewRootImpl.java void scheduleTraversals() { // 1. 防止重複調用 if (!mTraversalScheduled) { mTraversalScheduled = true; // 2. 發送同步屏障,保證優先處理異步消息 mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier(); // 3. 最終會執行 mTraversalRunnable 這個任務 mChoreographer.postCallback( Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null); ...... } }
到這裏,鼎鼎大名的 編舞者 —— Choreographer [ˌkɔːriˈɑːɡrəfər] 就該出場了(爲了不面試中出現不會讀單詞的尷尬,掌握一下發音仍是必須的)。
經過 mChoreographer 發送了一個任務 mTraversalRunnable ,最終會在某個時刻被執行。在看源碼以前,先拋出來幾個問題:
圍繞這三個問題,咱們再回到源碼中。
先來看第一個問題,這就得回到上一節介紹過的 WindowManagerGlobal.addView() 方法。
> WindowManagerGlobal.java // 參數 view 就是 DecorView public void addView(View view, ViewGroup.LayoutParams params, Display display, Window parentWindow) { ...... ViewRootImpl root; // 1. 初始化 ViewRootImpl root = new ViewRootImpl(view.getContext(), display); mViews.add(view); mRoots.add(root); root.setView(view, wparams, panelParentView); ...... }
註釋 1 處 新建了 ViewRootImpl 對象,跟進 ViewRootImpl 的構造函數。
> ViewRootImpl.java public ViewRootImpl(Context context, Display display) { mContext = context; // 1. IWindowSession 代理對象,與 WMS 進行 Binder 通訊 mWindowSession = WindowManagerGlobal.getWindowSession(); ...... mThread = Thread.currentThread(); ...... // IWindow Binder 對象 mWindow = new W(this); ...... // 2. 初始化 mAttachInfo mAttachInfo = new View.AttachInfo(mWindowSession, mWindow, display, this, mHandler, this, context); ...... // 3. 初始化 Choreographer,經過 Threadlocal 存儲 mChoreographer = Choreographer.getInstance(); ...... }
在 ViewRootImpl 的構造函數中,註釋 3 處初始化了 mChoreographer,調用的是 Choreographer.getInstance() 方法。
> Choreographer.java public static Choreographer getInstance() { return sThreadInstance.get(); }
sThreadInstance 是一個 ThreadLocal<Choreographer> 對象。
> Choreographer.java private static final ThreadLocal<Choreographer> sThreadInstance = new ThreadLocal<Choreographer>() { @Override protected Choreographer initialValue() { Looper looper = Looper.myLooper(); if (looper == null) { throw new IllegalStateException("The current thread must have a looper!"); } // 新建 Choreographer 對象 Choreographer choreographer = new Choreographer(looper, VSYNC_SOURCE_APP); if (looper == Looper.getMainLooper()) { mMainInstance = choreographer; } return choreographer; } };
因此 mChoreographer 保存在 ThreadLocal 中的線程私有對象。它的構造函數中須要傳入當前線程(這裏就是主線程)的 Looper 對象。
這裏再插一個題外話,主線程 Looper 是在何時建立的? 回顧一下應用進程的建立流程:
調用 Process.start() 建立應用進程
ZygoteProcess 負責和 Zygote 進程創建 socket 鏈接,並將建立進程須要的參數發送給 Zygote 的 socket 服務端
Zygote 服務端接收到參數以後調用 ZygoteConnection.processOneCommand() 處理參數,並 fork 進程
最後經過 findStaticMain() 找到 ActivityThread 類的 main() 方法並執行,子進程就啓動了
ActivityThread 並非一個線程,但它是運行在主線程上的,主線程 Looper 就是在它的 main() 方法中執行的。
> ActivityThread.java public static void main(String[] args) { ...... // 建立主線程 Looper Looper.prepareMainLooper(); ...... // 建立 ActivityThread ,並 attach(false) ActivityThread thread = new ActivityThread(); thread.attach(false, startSeq); ...... // 開啓主線程消息循環 Looper.loop(); }
Looper 也是存儲在 ThreadLocal 中的。
再回到 Choreographer,咱們來看一下它的構造函數。
> Choreographer.java private Choreographer(Looper looper, int vsyncSource) { mLooper = looper; // 處理事件 mHandler = new FrameHandler(looper); // USE_VSYNC 在 Android 4.1 以後默認爲 true, // FrameDisplayEventReceiver 是個 vsync 事件接收器 mDisplayEventReceiver = USE_VSYNC ? new FrameDisplayEventReceiver(looper, vsyncSource) : null; mLastFrameTimeNanos = Long.MIN_VALUE; // 一幀的時間,60pfs 的話就是 16.7ms mFrameIntervalNanos = (long)(1000000000 / getRefreshRate()); // 回調隊列 mCallbackQueues = new CallbackQueue[CALLBACK_LAST + 1]; for (int i = 0; i <= CALLBACK_LAST; i++) { mCallbackQueues[i] = new CallbackQueue(); } }
這裏出現了幾個新面孔,FrameHandler、FrameDisplayEventReceiver、CallbackQueue,這裏暫且不表,先混個臉熟,後面會一一說到。
介紹完 Choreographer 是如何初始化的,再回到 Choreographer 發送任務那塊。
mChoreographer.postCallback( Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
咱們看看 mTraversalRunnable 是什麼東西。
> ViewRootImpl.java final TraversalRunnable mTraversalRunnable = new TraversalRunnable(); final class TraversalRunnable implements Runnable { @Override public void run() { doTraversal(); } }
沒什麼特別的,它就是一個 Runnable 對象,run() 方法中會執行 doTraversal() 方法。
> ViewRootImpl.java void doTraversal() { if (mTraversalScheduled) { // 1. mTraversalScheduled 置爲 false mTraversalScheduled = false; // 2. 移除同步屏障 mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier); // 3. 開始佈局,測量,繪製流程 performTraversals(); ...... }
再對比一下最開始發起繪製的 scheduleTraversals() 方法:
> ViewRootImpl.java void scheduleTraversals() { // 1. mTraversalScheduled 置爲 true,防止重複調用 if (!mTraversalScheduled) { mTraversalScheduled = true; // 2. 發送同步屏障,保證優先處理異步消息 mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier(); // 3. 最終會執行 mTraversalRunnable 這個任務 mChoreographer.postCallback( Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null); ...... } }
仔細分別看一下上面兩個方法的註釋 一、二、 3,仍是很清晰的。mTraversalRunnable 被執行後最終會調用 performTraversals() 方法,來完成整個 View 的測量,佈局和繪製流程。
分析到這裏,就差最後一步了,mTraversalRunnable 是如何被調度執行的? 咱們再回到 Choreographer.postCallback() 方法。
> Choreographer.java 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) { ...... postCallbackDelayedInternal(callbackType, action, token, delayMillis); } // 傳入的參數依次是 Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null,0 private void postCallbackDelayedInternal(int callbackType, Object action, Object token, long delayMillis) { ...... synchronized (mLock) { final long now = SystemClock.uptimeMillis(); final long dueTime = now + delayMillis; // 1. 將 mTraversalRunnable 塞入隊列 mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token); if (dueTime <= now) { // 當即執行 // 2. 因爲 delayMillis 是 0,因此會執行到這裏 scheduleFrameLocked(now); } else { // 延遲執行 Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action); msg.arg1 = callbackType; msg.setAsynchronous(true); mHandler.sendMessageAtTime(msg, dueTime); } } }
首先根據 callbackType(這裏是 CALLBACK_TRAVERSAL) 將稍後要執行的 mTraversalRunnable 放入相應隊列中,其中的具體邏輯就不看了。
而後因爲 delayMillis 是 0,因此 dueTime 和 now 是相等的,因此直接執行 scheduleFrameLocked(now) 方法。若是 delayMillis 不爲 0 的話,會經過 FrameHandler 發送一個延時消息,最後執行的仍然是 scheduleFrameLocked(now) 方法。
> Choreographer.java private void scheduleFrameLocked(long now) { if (!mFrameScheduled) { mFrameScheduled = true; if (USE_VSYNC) { // Android 4.1 以後 USE_VSYNCUSE_VSYNC 默認爲 true // 若是是當前線程,直接申請 vsync,不然經過 handler 通訊 if (isRunningOnLooperThreadLocked()) { scheduleVsyncLocked(); } else { // 發送異步消息 Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_VSYNC); msg.setAsynchronous(true); mHandler.sendMessageAtFrontOfQueue(msg); } } else { // 未開啓 vsync,4.1 以後默認開啓 final long nextFrameTime = Math.max( mLastFrameTimeNanos / TimeUtils.NANOS_PER_MS + sFrameDelay, now); Message msg = mHandler.obtainMessage(MSG_DO_FRAME); msg.setAsynchronous(true); mHandler.sendMessageAtTime(msg, nextFrameTime); } } }
開頭到如今,已經好幾回提到了 VSYNC,官方也有一個視頻介紹 Android Performance Patterns: Understanding VSYNC[14] ,你們能夠看一看。簡而言之,VSYNC 是爲了解決屏幕刷新率和 GPU 幀率不一致致使的 「屏幕撕裂」 問題。VSYNC 在 PC 端是好久以來就存在的技術,但在 4.1 以後,Google 纔將其引入到 Android 顯示系統中,以解決飽受詬病的 UI 顯示不流暢問題。
再說的簡單點,能夠把 VSYNC 當作一個由硬件發出的定時信號,經過 Choreographer 監聽這個信號。每當信號來臨時,統一開始繪製工做。這就是 scheduleVsyncLocked() 方法的工做內容。
> Choreographer.java private void scheduleVsyncLocked() { mDisplayEventReceiver.scheduleVsync(); }
mDisplayEventReceiver 是 FrameDisplayEventReceiver 對象,但它並無 scheduleVsync() 方法,而是直接調用的父類方法。FrameDisplayEventReceiver 的父類是 DisplayEventReceiver 。
> DisplayEventReceiver.java public abstract class 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 { // 註冊監聽 vsync 信號,會回調 dispatchVsync() 方法 nativeScheduleVsync(mReceiverPtr); } } // 有 vsync 信號時,由 native 調用此方法 private void dispatchVsync(long timestampNanos, int builtInDisplayId, int frame) { // timestampNanos 是 vsync 回調的時間 onVsync(timestampNanos, builtInDisplayId, frame); } public void onVsync(long timestampNanos, int builtInDisplayId, int frame) { } }
在 scheduleVsync() 方法中會經過 nativeScheduleVsync() 方法註冊下一次 vsync 信號的監聽,從方法名也能看出來,下面會進入 native 調用,水平有限,就不追進去了。
註冊監聽以後,當下次 vsync 信號來臨時,會經過 jni 回調 java 層的 dispatchVsync() 方法,其中又調用了 onVsync() 方法。父類 DisplayEventReceiver 的 onVsync() 方法是個空實現,咱們再回到子類 FrameDisplayEventReceiver ,它是 Choreographer 的內部類。
> Choreographer.java private final class FrameDisplayEventReceiver extends DisplayEventReceiver implements Runnable { private long mTimestampNanos; private int mFrame; // vsync 信號監聽回調 @Override public void onVsync(long timestampNanos, int builtInDisplayId, int frame) { ...... long now = System.nanoTime(); // // timestampNanos 是 vsync 回調的時間,不能比 now 大 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; } ...... mTimestampNanos = timestampNanos; mFrame = frame; // 這裏傳入的是 this,會回調自己的 run() 方法 Message msg = Message.obtain(mHandler, this); // 這是一個異步消息,保證優先執行 msg.setAsynchronous(true); mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS); } @Override public void run() { doFrame(mTimestampNanos, mFrame); } }
在 onVsync() 回調中,向主線程發送了一個異步消息,注意 sendMessageAtTime() 方法參數中的時間是 timestampNanos / TimeUtils。timestampNanos 是 vsync 信號的時間戳,單位是納秒,因此這裏作一個除法轉換爲毫秒。代碼執行到這裏的時候 vsync 信號已經發生,因此 timestampNanos 是比當前時間小的。這樣這個消息塞進 MessageQueue 的時候就能夠直接塞到前面了。另外 callback 是 this,因此當消息被執行時,調用的是本身的 run() 方法,run() 方法中調用的是 doFrame() 方法。
> Choreographer.java void doFrame(long frameTimeNanos, int frame) { final long startNanos; synchronized (mLock) { if (!mFrameScheduled) { return; // no work to do } ...... long intendedFrameTimeNanos = frameTimeNanos; startNanos = System.nanoTime(); // 計算超時時間 // frameTimeNanos 是 vsync 信號回調的時間,startNanos 是當前時間戳 // 相減獲得主線程的耗時時間 final long jitterNanos = startNanos - frameTimeNanos; // mFrameIntervalNanos 是一幀的時間 if (jitterNanos >= mFrameIntervalNanos) { final long skippedFrames = jitterNanos / mFrameIntervalNanos; // 掉幀超過 30 幀,打印 log if (skippedFrames >= SKIPPED_FRAME_WARNING_LIMIT) { Log.i(TAG, "Skipped " + skippedFrames + " frames! " + "The application may be doing too much work on its main thread."); } final long lastFrameOffset = jitterNanos % mFrameIntervalNanos; frameTimeNanos = startNanos - lastFrameOffset; } ...... } try { AnimationUtils.lockAnimationClock(frameTimeNanos / TimeUtils.NANOS_PER_MS); // doCallBacks() 開始執行回調 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(); } ...... }
在 Choreographer.postCallback() 方法中將 mTraversalRunnable 塞進了 mCallbackQueues[] 數組中,下面的 doCallbacks() 方法就要把它取出來執行了。
> Choreographer.java void doCallbacks(int callbackType, long frameTimeNanos) { CallbackRecord callbacks; synchronized (mLock) { final long now = System.nanoTime(); // 根據 callbackType 找到對應的 CallbackRecord 對象 callbacks = mCallbackQueues[callbackType].extractDueCallbacksLocked( now / TimeUtils.NANOS_PER_MS); if (callbacks == null) { return; } mCallbacksRunning = true; ...... } try { for (CallbackRecord c = callbacks; c != null; c = c.next) { // 執行 callBack c.run(frameTimeNanos); } } finally { ...... } }
根據 callbackType 找到對應的 mCallbackQueues ,而後執行,具體流程就不深刻分析了。callbackType 共有四個類型,分別是 CALLBACK_INPUT、CALLBACK_ANIMATION、CALLBACK_TRAVERSAL、CALLBACK_COMMIT 。
> Choreographer.CallbackRecord public void run(long frameTimeNanos) { if (token == FRAME_CALLBACK_TOKEN) { ((FrameCallback)action).doFrame(frameTimeNanos); } else { ((Runnable)action).run(); } }
到此爲止,mTraversalRunnable 得以被執行,View.invalidate() 的整個流程就走通了。總結一下:
從 View.invalidate() 開始,最後會遞歸調用 parent.invalidateChildInParent() 方法。這裏最頂層的 parent 是 ViewRootImpl 。ViewRootImpl 是 DecorView 的 parent,這個賦值調用鏈是這樣的 ActivityThread.handleResumeActivity -> WindowManagerImpl.addView() -> WindowManagerGlobal.addView() -> ViewRootImpl.setView() -> View.assignParent() 。
ViewRootImpl.invalidateChildInParent() 最終調用到 scheduleTraversals() 方法,其中創建同步屏障以後,經過 Choreographer.postCallback() 方法提交了任務 mTraversalRunnable,這個任務就是負責 View 的測量,佈局,繪製。
Choreographer.postCallback() 方法經過 DisplayEventReceiver.nativeScheduleVsync() 方法向系統底層註冊了下一次 vsync 信號的監聽。當下一次 vsync 來臨時,系統會回調其 dispatchVsync() 方法,最終回調 FrameDisplayEventReceiver.onVsync() 方法。
FrameDisplayEventReceiver.onVsync() 方法中取出以前提交的 mTraversalRunnable 並執行。這樣就完成了整個繪製流程。
監測當前應用的 FPS 很簡單。每次 vsync 信號回調中,都會執行四種類型的 mCallbackQueues 隊列中的回調任務。而 Choreographer 又對外提供了提交回調任務的方法,這個方法就是 Choreographer.getInstance().postFrameCallback() 。簡單跟進去看一下。
> Choreographer.java public void postFrameCallback(FrameCallback callback) { postFrameCallbackDelayed(callback, 0); } public void postFrameCallbackDelayed(FrameCallback callback, long delayMillis) { ...... // 這裏的類型是 CALLBACK_ANIMATION postCallbackDelayedInternal(CALLBACK_ANIMATION, callback, FRAME_CALLBACK_TOKEN, delayMillis); }
和 View.invalite() 流程中調用的 Choreographer.postCallback() 基本一致,僅僅只是 callback 類型不一致,這裏是 CALLBACK_ANIMATION 。
我直接給出實現代碼 :FpsMonitor.kt[15]
object FpsMonitor { private const val FPS_INTERVAL_TIME = 1000L private var count = 0 private var isFpsOpen = false private val fpsRunnable by lazy { FpsRunnable() } private val mainHandler by lazy { Handler(Looper.getMainLooper()) } private val listeners = arrayListOf<(Int) -> Unit>() fun startMonitor(listener: (Int) -> Unit) { // 防止重複開啓 if (!isFpsOpen) { isFpsOpen = true listeners.add(listener) mainHandler.postDelayed(fpsRunnable, FPS_INTERVAL_TIME) Choreographer.getInstance().postFrameCallback(fpsRunnable) } } fun stopMonitor() { count = 0 mainHandler.removeCallbacks(fpsRunnable) Choreographer.getInstance().removeFrameCallback(fpsRunnable) isFpsOpen = false } class FpsRunnable : Choreographer.FrameCallback, Runnable { override fun doFrame(frameTimeNanos: Long) { count++ Choreographer.getInstance().postFrameCallback(this) } override fun run() { listeners.forEach { it.invoke(count) } count = 0 mainHandler.postDelayed(this, FPS_INTERVAL_TIME) } } }
大體邏輯是這樣的 :
來張 GIF 感覺一下,源碼已經上傳到個人開源項目 Mamba[16] 。
目前也有不少開源項目實現了 FPS 監測或者流暢度檢測的功能。
滴滴開源的 DoraemonKit[17] 的作法和上面介紹的是一致的(沒錯,我就是仿照它寫的,/goutou),能夠看一下 PerformanceDataManager.startMonitorFrameInfo() 方法的實現。
騰訊開源的 Matrix[18] 雖然也是在 Choreographer 上動手腳,但作的更加完全,它能夠監聽到 CALLBACK_INPUT、CALLBACK_ANIMATION、CALLBACK_TRAVERSAL 三種事件各自精確的耗時,重點關注 LooperMonitor 和 UIThreadMonitor 這兩個類。具體原理這裏就再也不分析了,能夠給你們推薦一篇文章, Matrix-TraceCanary的設計和原理分析手冊[19] 。
這一期的文章到這就結束了。以 如何監測應用的 FPS 爲引子,重點講述了屏幕刷新機制和 Choreographer 的工做流程,中間也摻雜了一些其餘內容。後續的文章也將延續此風格,儘可能涵括更多的知識點,敬請期待。