区块链技术博客
www.b2bchain.cn

面试官:如何监测应用的 FPS ?求职学习资料

D0b2wT.gif

本文介绍了面试官:如何监测应用的 FPS ?求职学习资料,有助于帮助完成毕业设计以及求职,是一篇很好的资料。

对技术面试,学习经验等有一些体会,在此分享。

Android 面试进阶指南目录

计算机网络

  1. http 速查

Android

  1. 面试官:任务栈?返回栈?启动模式?傻傻分不清楚?
  2. 面试官:唠唠 Activity 的生命周期
  3. 面试官: 说一说 Context
  4. 面试官:为什么不能使用 Application Context 显示 Dialog?
  5. 面试官:OutOfMemoryError 可以被 try catch 吗 ?
  6. 面试官:为什么 Activity.finish() 之后 10s 才 onDestroy ?
  7. 面试官:如何监测应用的 FPS ?

目录

  • 什么是 FPS?
  • 从 View.invalidate() 说起
  • 承上启下的 “编舞者”
  • 如何监测应用的 FPS?
  • 最后

什么是 FPS ?

即使你不知道 FPS,但你一定听说过这么一句话,在 Android 中,每一帧的绘制时间不要超过 16.67ms。那么,这个 16.67ms 是怎么来的呢?就是由 FPS 决定的。

FPS,Frame Per Second,每秒显示的帧数,也叫 帧率。Android 设备的 FPS 一般是 60,也即每秒要刷新 60 帧,所以留给每一帧的绘制时间最多只有 1000/60 = 16.67ms 。一旦某一帧的绘制时间超过了限制,就会发生 掉帧,用户在连续两帧会看到同样的画面。

监测 FPS 在一定程度上可以反应应用的卡顿情况,原理也很简单,但前提是你对屏幕刷新机制和绘制流程很熟悉。所以我不会直接进入主题,让我们先从 View.invalidate() 说起。

从 View.invalidate() 说起

要探究屏幕刷新机制和 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()

> 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 吗?也不是,它也是有父亲的。

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);         ......     } }
  1. mTraversalScheduled 是个布尔值,防止重复调用,在一次 vsync 信号期间多次调用是没有意义的
  2. 利用 Handler 的同步屏障机制,优先处理异步消息
  3. Choreographer 登场

到这里,鼎鼎大名的 编舞者 —— Choreographer [ˌkɔːriˈɑːɡrəfər] 就该出场了(为了避免面试中出现不会读单词的尴尬,掌握一下发音还是必须的)。

通过 mChoreographer 发送了一个任务 mTraversalRunnable ,最终会在某个时刻被执行。在看源码之前,先抛出来几个问题:

  1. mChoreographer 是在什么时候初始化的?
  2. mTraversalRunnable 是个什么鬼?
  3. mChoreographer 是如何发送任务以及任务是如何被调度执行的?

围绕这三个问题,我们再回到源码中。

先来看第一个问题,这就得回到上一节介绍过的 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();     } }

这里出现了几个新面孔,FrameHandlerFrameDisplayEventReceiverCallbackQueue,这里暂且不表,先混个脸熟,后面会一一说到。

介绍完 Choreographer 是如何初始化的,再回到 Choreographer 发送任务那块。

mChoreographer.postCallback(                 Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);

我们看看 mTraversalRunnable 是什么东西。

“`java

ViewRootImpl.java

final TraversalRunnable mTraversalRunnable = new TraversalRunnable();

final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();

Android 面试进阶指南目录

计算机网络

  1. http 速查

Android

  1. 面试官:任务栈?返回栈?启动模式?傻傻分不清楚?
  2. 面试官:唠唠 Activity 的生命周期
  3. 面试官: 说一说 Context
  4. 面试官:为什么不能使用 Application Context 显示 Dialog?
  5. 面试官:OutOfMemoryError 可以被 try catch 吗 ?
  6. 面试官:为什么 Activity.finish() 之后 10s 才 onDestroy ?
  7. 面试官:如何监测应用的 FPS ?

目录

  • 什么是 FPS?
  • 从 View.invalidate() 说起
  • 承上启下的 “编舞者”
  • 如何监测应用的 FPS?
  • 最后

什么是 FPS ?

即使你不知道 FPS,但你一定听说过这么一句话,在 Android 中,每一帧的绘制时间不要超过 16.67ms。那么,这个 16.67ms 是怎么来的呢?就是由 FPS 决定的。

FPS,Frame Per Second,每秒显示的帧数,也叫 帧率。Android 设备的 FPS 一般是 60,也即每秒要刷新 60 帧,所以留给每一帧的绘制时间最多只有 1000/60 = 16.67ms 。一旦某一帧的绘制时间超过了限制,就会发生 掉帧,用户在连续两帧会看到同样的画面。

监测 FPS 在一定程度上可以反应应用的卡顿情况,原理也很简单,但前提是你对屏幕刷新机制和绘制流程很熟悉。所以我不会直接进入主题,让我们先从 View.invalidate() 说起。

从 View.invalidate() 说起

要探究屏幕刷新机制和 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()

> 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 吗?也不是,它也是有父亲的。

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);         ......     } }
  1. mTraversalScheduled 是个布尔值,防止重复调用,在一次 vsync 信号期间多次调用是没有意义的
  2. 利用 Handler 的同步屏障机制,优先处理异步消息
  3. Choreographer 登场

到这里,鼎鼎大名的 编舞者 —— Choreographer [ˌkɔːriˈɑːɡrəfər] 就该出场了(为了避免面试中出现不会读单词的尴尬,掌握一下发音还是必须的)。

通过 mChoreographer 发送了一个任务 mTraversalRunnable ,最终会在某个时刻被执行。在看源码之前,先抛出来几个问题:

  1. mChoreographer 是在什么时候初始化的?
  2. mTraversalRunnable 是个什么鬼?
  3. mChoreographer 是如何发送任务以及任务是如何被调度执行的?

围绕这三个问题,我们再回到源码中。

先来看第一个问题,这就得回到上一节介绍过的 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();     } }

这里出现了几个新面孔,FrameHandlerFrameDisplayEventReceiverCallbackQueue,这里暂且不表,先混个脸熟,后面会一一说到。

介绍完 Choreographer 是如何初始化的,再回到 Choreographer 发送任务那块。

mChoreographer.postCallback(                 Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);

我们看看 mTraversalRunnable 是什么东西。

“`java

ViewRootImpl.java

final TraversalRunnable mTraversalRunnable = new TraversalRunnable();

final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();

Android 面试进阶指南目录

计算机网络

  1. http 速查

Android

  1. 面试官:任务栈?返回栈?启动模式?傻傻分不清楚?
  2. 面试官:唠唠 Activity 的生命周期
  3. 面试官: 说一说 Context
  4. 面试官:为什么不能使用 Application Context 显示 Dialog?
  5. 面试官:OutOfMemoryError 可以被 try catch 吗 ?
  6. 面试官:为什么 Activity.finish() 之后 10s 才 onDestroy ?
  7. 面试官:如何监测应用的 FPS ?

目录

  • 什么是 FPS?
  • 从 View.invalidate() 说起
  • 承上启下的 “编舞者”
  • 如何监测应用的 FPS?
  • 最后

什么是 FPS ?

即使你不知道 FPS,但你一定听说过这么一句话,在 Android 中,每一帧的绘制时间不要超过 16.67ms。那么,这个 16.67ms 是怎么来的呢?就是由 FPS 决定的。

FPS,Frame Per Second,每秒显示的帧数,也叫 帧率。Android 设备的 FPS 一般是 60,也即每秒要刷新 60 帧,所以留给每一帧的绘制时间最多只有 1000/60 = 16.67ms 。一旦某一帧的绘制时间超过了限制,就会发生 掉帧,用户在连续两帧会看到同样的画面。

监测 FPS 在一定程度上可以反应应用的卡顿情况,原理也很简单,但前提是你对屏幕刷新机制和绘制流程很熟悉。所以我不会直接进入主题,让我们先从 View.invalidate() 说起。

从 View.invalidate() 说起

要探究屏幕刷新机制和 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()

> 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 吗?也不是,它也是有父亲的。

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);         ......     } }
  1. mTraversalScheduled 是个布尔值,防止重复调用,在一次 vsync 信号期间多次调用是没有意义的
  2. 利用 Handler 的同步屏障机制,优先处理异步消息
  3. Choreographer 登场

到这里,鼎鼎大名的 编舞者 —— Choreographer [ˌkɔːriˈɑːɡrəfər] 就该出场了(为了避免面试中出现不会读单词的尴尬,掌握一下发音还是必须的)。

通过 mChoreographer 发送了一个任务 mTraversalRunnable ,最终会在某个时刻被执行。在看源码之前,先抛出来几个问题:

  1. mChoreographer 是在什么时候初始化的?
  2. mTraversalRunnable 是个什么鬼?
  3. mChoreographer 是如何发送任务以及任务是如何被调度执行的?

围绕这三个问题,我们再回到源码中。

先来看第一个问题,这就得回到上一节介绍过的 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();     } }

这里出现了几个新面孔,FrameHandlerFrameDisplayEventReceiverCallbackQueue,这里暂且不表,先混个脸熟,后面会一一说到。

介绍完 Choreographer 是如何初始化的,再回到 Choreographer 发送任务那块。

mChoreographer.postCallback(                 Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);

我们看看 mTraversalRunnable 是什么东西。

“`java

ViewRootImpl.java

final TraversalRunnable mTraversalRunnable = new TraversalRunnable();

final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();

部分转自互联网,侵权删除联系

赞(0) 打赏
部分文章转自网络,侵权联系删除b2bchain区块链学习技术社区 » 面试官:如何监测应用的 FPS ?求职学习资料
分享到: 更多 (0)
D0b2wT.gif

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

b2b链

联系我们联系我们