当我们讨论流畅度的时候,我们究竟在说什么?

前言:那些年我们用过的显示性能指标

相对其他 Android 性能指标(如内存、CPU、功耗等)而言,显示性能(包括但不仅限于我们常说的“流畅度”)的概念本来就相对复杂。让我们更蛋疼的是,业界对显示测试评估方式也是丰富多样,这无疑更加重了我们对其理解的复杂程度。

笔者简单搜集了一些业界中提及的显示性能指标,大家可以来品评一下:

指标名称:FPS

相关资料Android性能测试之fps获取

指标名称:Aggregate frame stats(N 多个指标)

相关资料?Testing Display Performance

指标名称:Jankiness count、Max accumulated frames、Frame rate

相关资料JankTestBase.java

指标名称:SM、Skipped frames

相关资料Android应用性能评测调优

面对如此之多的显示性能指标,想必大家也会跟笔者一样,心中难免疑惑丛生。其实,我们只需要依次弄清楚以下三个哲学问题,所有的问题也许就会迎刃而解:

  • 你是谁——这些指标具体反映了什么问题
  • 你从哪儿来——这些指标数值是怎么得到的
  • 你要到哪儿去——这些指标如何落地来指导优化

因此,本文将尝试依次从上诉三个问题来逐步分析和探讨各个显示性能指标。

Step 1:你是谁——这些指标具体反映了什么问题

总所周知,脱离了具体的应用背景,所有的指标都是没有意义的。所以,为了彻底弄清楚各个显示性能指标的具体身份,我们势必得从 Android 的图像渲染流程说起。

具体展开之前,首先需要说明的是,为了降低复杂程度和本章篇幅,在这个环节之中,我们只讨论图像渲染流程中的各个具体环节所对应的指标有哪些。而指标的具体定义,由第二章《你从哪儿来——这些指标数值是怎么得到的》进行讨论。

Android 图像渲染流程

下图是笔者结合各类资料(主要是是源码及官方文档),在根据自己的理解梳理出的几种常见场景下的图像渲染流程:

PS 1:笔者个人技术水平有限,若存在理解有误的地方还望指正。

PS 2:本文主要讨论的 Android 源码为 Android 6.0

备注:基于 OpenGL 的应用可以使用 Choreographer 中的 VSYNC 信号来进行图像渲染工作的安排。

上面这幅图涉及的概念较多,要完全吃透估计得费不少时间。不过好在我们只是想弄明白显示性能各种指标的含义,所以我们只需要理清下面两大关系即可:

  • SurfaceFlinger、HWComposer与Surface的关系

    • Surface:可以理解为Android系统中的一个基本显示单元。只要使用Android任意一种API绘图,绘制的结果都将反映在Surface上。
    • SurfaceFlinger:服务运行在System进程中,用来统一管理系统的帧缓冲区设备,其主要作用是将系统中的大部分Surface进行合成。SurfaceFlinger主要使用GPU进行Surface的合成,合成的结果将形成一个FrameBuffer。
    • HWComposer:即Hardware Composer HAL,其作用是将SurfaceFlinger通过GPU合成的结果与其他Surface一起最终形成BufferQueue中的一个Buffer。此外,HWComposer可以协助SurfaceFlinger进行Surface的合成,但是否进行协助是由HWComposer决定的。
    • 值得注意的是,有的Surface不由WindowManager管理,将直接作为HWComposer的输入之一与SurfaceFlinger的输出做最后的合成。
  • Choreographer、SurfaceFlinger、HWComposer与VSYNC的关系
    • VSYNC:Vertical Synchronization的缩写,它的作用是使GPU的渲染频率与显示器的刷新频率(一般为固定值)同步从而避免出现画面撕裂的现象。
    • HWComposer:VSYNC信号主要由HWComposer通过硬件触发。
    • Choreographer:当收到VSYNC信号时,Choreographer将按优先级高低依次去调用使用者通过postCallback提前设置的回调函数,它们分别是:优先级最高的CALLBACK_INPUT、优先级次高的CALLBACK_ANIMATION以及优先级最低的CALLBACK_TRAVERSAL。
    • SurfaceFlinger:Surface的合成操作也时基于VSYNC信号进行的。

简单来说,Android 图像渲染流程主要由以下特征:

  1. 我们可以简单把 Android 图像渲染架构分为应用(Surface)、系统(SurfaceFlinger)、硬件(Screen)三个层级,其中绘制在应用层,合成及提交上屏在系统层,显示在硬件层;
  2. 无论应用(Surface)、系统(SurfaceFlinger)、硬件(Screen)都是当且仅当绘制内容发生改变,才会对绘制内容进行处理;
  3. 系统中的 SurfaceFlinger 以及绝大部分 Surface 都是按照 VSYNC 信号的节奏来安排自己的任务;
  4. 目前,绝大部分 Surface 都属于 Hardware Rendering。

各个指标在 Android 图像渲染流程所代表的意义

大致梳理了 Android 的图像渲染流程之后,我们需要做的一件事情,就是看看上面提到的指标,都对应了渲染流程的哪些阶段,这样对于我们了解各个指标所反映的具体物理意义及其优势劣势都有极大帮助。再次强调,在这个环节之中,我们的讨论仅限于只讨论指标所对应的渲染流程的具体阶段,各指标的具体定义由第二章具体展开。

系统层级(SurfaceFlinger)的显示性能指标

  • 基础数据:SurfaceFlinger 合成次数
  • 指标意义:
    • 系统合成帧率:FPS
  • 特别说明:
    • SurfaceFlinger 仅在显示区域内的 Surface 有提交内容更新时才会进行合成(上屏),因此,系统合成帧率低并不一定意味着图像显示性能差,有可能是因为当前并没有任何的内容更新所导致。
    • 若显示区域内的某个待测 Surface 持续进行更新时, SurfaceFlinger的合成(上屏)的频率可以在某种程度上反映该 Surface 的显示性能,但从理论上分析该指标并不一定准确。这是因为,若显示区域内尚存在其他 Surface,它们也会影响 SurfaceFlinger 的合成(上屏)的行为,从而干扰结果。
    • 若某个 Surface 的合成不在 SurfaceFlinger 中进行(如 Camera Preview),则该 Surface 的显示性能无法用这类指标进行衡量。

应用层级(Surface)的显示性能指标

  • 基础数据:绘制过程中每一帧的关键时间点(如开始绘制时间、结束绘制时间等)
  • 指标意义:
    • 应用绘制帧率:Frame rate
    • 应用绘制轮询频率:SM
    • 应用绘制超时(跳帧)的次数:Aggregate frame stats、Jankiness count、Skipped frames
    • 应用绘制超时(跳帧)的幅度:Aggregate frame stats、Max accumulated frames、Skipped frames
  • 特别说明:
    • 与 SurfaceFlinger 类似, Surface也仅在有内容更新时才会进行绘制,因此,绘制频率低并不一定意味着图像显示性能差,有可能是因为当前并没有任何的内容更新所导致。
    • 如 SM、Skipped frames 这类指标,由于其基础数据取自 Choreographer,若 某些 Surface 的绘制不依赖于 Choreographer ,则这些指标无法衡量该 Surface 的显示性能。
    • 如 Aggregate frame stats、Jankiness count、Max accumulated frames、Frame rate 这类指标, 由于其基础数据仅在硬件绘制(Hardware Rendering)过程中进行统计,属于 HWUI 的功能,所以非硬件绘制的 Surface 自然无法使用这类指标进行衡量。

小结

评价显示性能的各个指标,可以按其在图像渲染流程中的作用,分为以下两类:

  1. 系统层级的指标仅有 FPS 一根独苗,它的限制是 Surface 的和合成需要在 SurfaceFlinger 中进行;
  2. 应用层级的指标较多,它们之中又可以分为两类:

    1) SM、Skipped frames 需要 Surface 依赖 Choreographer 进行绘制,才能正常工作;

    2) Aggregate frame stats、Jankiness count、Max accumulated frames、Frame rate 属于 HWUI 的功能, 需要 Surface 的绘制由 HWUI 进行才能进行分析。

Step 2:你从哪儿来——这些指标数值是怎么得到的

第一章的内容仅仅是站在整个图像绘制流程的高度来简单分析各个指标的,本章将进一步分析各个指标的基础数据来源以及具体计算方式。

基础数据:系统层级(SurfaceFlinger)的合成(上屏)的次数

前面说到,在 Android 系统中,SurfaceFlinger 扮演了系统中所有 Surface 的管理者的角色,当应用程序所对应的 Surface 更新之后,绝大多数的 Surface 都将在 SurfaceFlinger 之中完成了合并的工作之后,最终才会在 Screen 上显示出来。

当然, SurfaceFlinger 的执行也是由 VSYNC 信号驱动的,这也决定了每秒钟合成次数的上限就是 60 次。当 SurfaceFlinger 接收到 Surface 更新通知的时候,将会由 SurfaceFlinger::handleMessageRefresh 函数进行处理,其中包含重建可见区域、初始化、合成等步骤。这里,我们主要关注 SurfaceFlinger::doComposition() 这个方法。

void SurfaceFlinger::handleMessageRefresh() {
  ...
  if (CC_UNLIKELY(mDropMissedFrames && frameMissed)) {
    // Latch buffers, but don‘t send anything to HWC, then signal another
    // wakeup for the next vsync
    preComposition();
    repaintEverything();
  } else {
    preComposition();
    rebuildLayerStacks();
    setUpHWComposer();
    doDebugFlashRegions();
    doComposition(); //重点关注对象
    postComposition();
  }
  ...
}

在 doComposition 中,完成 Surface 的合成之后,都会调用 DisplayDevice::flip(),它会使用变量 mPageFlipCount 统计我们进行合成的次数,这个变量就是我们统计 FPS 的核心原始数据。mPageFlipCount 记录了 SurfaceFlinger 一共进行了多少次合成,也可以简单理解为,SurfaceFlinger 向屏幕提交了多少帧的数据。

void SurfaceFlinger::doComposition() {
  ATRACE_CALL();
  const bool repaintEverything = android_atomic_and(0, &mRepaintEverything);
  for (size_t dpy=0 ; dpy<mDisplays.size() ; dpy++) {
    const sp<DisplayDevice>& hw(mDisplays[dpy]);
    if (hw->isDisplayOn()) {
      // transform the dirty region into this screen‘s coordinate space
      const Region dirtyRegion(hw->getDirtyRegion(repaintEverything));
      // repaint the framebuffer (if needed)
      doDisplayComposition(hw, dirtyRegion);
      hw->dirtyRegion.clear();
      hw->flip(hw->swapRegion);//重点关注对象
      hw->swapRegion.clear();
    }
    // inform the h/w that we‘re done compositing
    hw->compositionComplete();
  }
  postFramebuffer();
}
void DisplayDevice::flip(const Region& dirty) const {
  mFlinger->getRenderEngine().checkErrors();
  if (kEGLAndroidSwapRectangle) {
    if (mFlags & SWAP_RECTANGLE) {
      const Region newDirty(dirty.intersect(bounds()));
      const Rect b(newDirty.getBounds());
      eglSetSwapRectangleANDROID(mDisplay, mSurface,
      b.left, b.top, b.width(), b.height());
    }
  }
  mPageFlipCount++;
}

不仅如此, Android 还为我们获取这个基础数据提供了比较方便的方法。通过执行 adb 命令:service call SurfaceFlinger 1013,我们就可以得出当前的 mPageFlipCount。

C:\Users\xiaosongluo>adb shell
shell@cancro:/ $ su
su
root@cancro:/ # service call SurfaceFlinger 1013
service call SurfaceFlinger 1013
Result: Parcel(00aea4f4    ‘....‘)

这条命令将使得 SurfaceFlinger 接收到 1013 的指令,从而将 mPageFlipCount 打印出来,具体过程见下述源码。

status_t SurfaceFlinger::onTransact( uint32_t code, const Parcel& data, Parcel* reply, uint32_t flags) {
  ...
  case 1013: {
    Mutex::Autolock _l(mStateLock);
    sp<const DisplayDevice> hw(getDefaultDisplayDevice());
    reply->writeInt32(hw->getPageFlipCount());
    return NO_ERROR;
  }
  ...
}
uint32_t DisplayDevice::getPageFlipCount() const {
 return mPageFlipCount;
}

FPS 的计算方法

根据 FPS 的定义,我们不难逆推得出 FPS 的计算方法:

在 t1 时刻获取 mPageFlipCount 的数值 v1,在在 t2时刻获取 mPageFlipCount 的数值 v2,FPS 的计算公式:

FPS = (v2 - v1) / (t2 - t1);

需要注意的是:mPageFlipCount 的原始数据是 16 进制的,一般而言计算之前需要先进行进制转换。

基础数据:应用层级(Surface)的绘制过程中每一帧的关键时间点(FrameInfo)

请大家先注意 FrameInfo 是由 Android 6.0(具体来讲是 Android M Preview) 引入到 HWUI 模块中的统计功能。 因此,目前来讲绝大多数系统上的大多数应用都暂时无法获取这一基础数据。不过 This Is The Future。

我们再来仔细瞧瞧 Google 给出的显示性能测试的十全大补丸 《Testing Display Performance : Aggregate frame stats》 。其中,特别值得关注的是 adb shell dumpsys gfxinfo framestats 这一条命令。通过这条命令,我们获取每一帧绘制过程中每个关键节点的耗时情况,从而仔细的分析潜在的性能问题。

>adb shell dumpsys gfxinfo <PACKAGE_NAME> framestats
0,27965466202353,27965466202353,27965449758000,27965461202353,27965467153286,27965471442505,27965471925682,27965474025318,27965474588547,27965474860786,27965475078599,27965479796151,27965480589068,0,27965482993342,27965482993342,27965465835000,27965477993342,27965483807401,27965486875630,27965487288443,27965489520682,27965490184380,27965490568703,27965491408078,27965496119641,27965496619641,0,27965499784331,27965499784331,27965481404000,27965494784331,27965500785318,27965503736099,27965504201151,27965506776568,27965507298443,27965507515005,27965508405474,27965513495318,27965514061984,0,27965516575320,27965516575320,27965497155000,27965511575320,27965517697349,27965521276151,27965521734797,27965524350474,27965524884536,27965525160578,27965526020891,27965531371203,27965532114484,

不得不说,按照 Google 给出的这种测试方法进行测试得到的显示性能数据是非常全面的。

这些基础数据都是记录在 FrameInfo 之中,由 CanvasContext 在doFrame()时进行记录。相关的主要源码如下:

//源码:FrameInfo.cpp
#include "FrameInfo.h"
#include <cstring>

namespace android {
  namespace uirenderer {
    const std::string FrameInfoNames[] = {
      "Flags",
      "IntendedVsync",
      "Vsync",
      "OldestInputEvent",
      "NewestInputEvent",
      "HandleInputStart",
      "AnimationStart",
      "PerformTraversalsStart",
      "DrawStart",
      "SyncQueued",
      "SyncStart",
      "IssueDrawCommandsStart",
      "SwapBuffers",
      "FrameCompleted",
    };

    void FrameInfo::importUiThreadInfo(int64_t* info) {
      memcpy(mFrameInfo, info, UI_THREAD_FRAME_INFO_SIZE * sizeof(int64_t));
    }
  } /* namespace uirenderer */
} /* namespace android */
//源码:CanvasContext.cpp
// Called by choreographer to do an RT-driven animation
void CanvasContext::doFrame() {
  if (CC_UNLIKELY(!mCanvas || mEglSurface == EGL_NO_SURFACE)) {
    return;
  }
  ATRACE_CALL();
  nsecs_t vsync = mRenderThread.timeLord().computeFrameTimeNanos();
  int64_t frameInfo[UI_THREAD_FRAME_INFO_SIZE];
  UiFrameInfoBuilder(frameInfo).addFlag(FrameInfoFlags::RTAnimation).setVsync(vsync, vsync);
  TreeInfo info(TreeInfo::MODE_RT_ONLY, mRenderThread.renderState());
  prepareTree(info, frameInfo, systemTime(CLOCK_MONOTONIC));
  if (info.out.canDrawThisFrame) {
    draw();
  }
}

void CanvasContext::prepareTree(TreeInfo& info, int64_t* uiFrameInfo, int64_t syncQueued) {
  ...
  mCurrentFrameInfo->importUiThreadInfo(uiFrameInfo);
  ...
}

Aggregate frame stats 指标的计算方法

首先需要说明的是 Aggregate frame stats 不是一个指标,而是一系列指标集合。我们来看一个具体的 Aggregate frame stats 的例子:

Stats since: 752958278148ns

Total frames rendered: 82189

Janky frames: 35335 (42.99%)

90th percentile: 34ms

95th percentile: 42ms

99th percentile: 69ms

Number Missed Vsync: 4706

Number High input latency: 142

Number Slow UI thread: 17270

Number Slow bitmap uploads: 1542

Number Slow draw: 23342

以上统计信息的实现可以详见源码:GfxMonitorImpl.java

在 Android M 以上的系统上,上述信息的获取十分方便(事实上也只有这些系统能够获取这些信息)。仅需要执行以下命令即可:

adb shell dumpsys gfxinfo <PACKAGE_NAME>

Jankiness count、Max accumulated frames、Frame rate 指标的计算方法

首先需要说明的是:Jankiness count、Max accumulated frames、Frame rate 与 Aggregate frame stats的基础数据并不一致,它们的基础属于来源于 gfxinfo(Profile data in ms)。

只是在 Android M 中 gfxinfo(Profile data in ms) 的基础数值来源于 FrameInfo,详见源码:FrameInfoVisualizer。但在更早的系统之上, gfxinfo(Profile data in ms) 的数值也可以获取。

这里需要特别指出的是, gfxinfo(Profile data in ms)只保存了 Surface 最近渲染的128帧的信息,因此,Jankiness count、Max accumulated frames、Frame rate 也仅仅是针对这 128 帧数据所计算出来的结果,它们的具体含义分别是:

  • Jankiness count:根据相邻两帧绘制时间的差值,“估计”是否存在跳帧并进行跳帧次数的统计;
  • Max accumulated frames: 根据相邻两帧绘制时间的差值,“估计”这 128 帧绘制过程中可能形成的最大连续跳帧数;
  • Frame rate:计算所得平均(绘制)帧率。

如果你对具体的计算过程感兴趣,可以参考详见源码:JankTestBase

基础数据:应用层级(Surface)的绘制过程中每一帧的关键时间点(Choreographer)

先说一句有点绕口的话: Choreographer 是依据 Choreographer 绘制的 Surface 在 UI 绘制过程中最为核心的机制。

Choreographer 的工作机制简单来说就是,使用者首先通过 postCallback 在 Choreographer 中设置的自己回调函数:

  • CALLBACK_INPUT:优先级最高,和输入事件处理有关。
  • CALLBACK_ANIMATION:优先级其次,和Animation的处理有关。
  • CALLBACK_TRAVERSAL:优先级最低,和UI等控件绘制有关。

那么,当 Choreographer 接收到 VSYNC 信号时,Choreographer 会调用 doFrame 函数依次对上述借口进行回调,从而进行渲染。

那么显然,Choreographer 的执行效率(次数、频率)也就是我们需要的显示性能数据。而这些基础数据,Choreographer 自身也进行了记录:


// Set a limit to warn about skipped frames.
// Skipped frames imply jank.
private static final int SKIPPED_FRAME_WARNING_LIMIT = SystemProperties.getInt("debug.choreographer.skipwarning", 30);

void doFrame(long frameTimeNanos, int frame) {
  ...
  long intendedFrameTimeNanos = frameTimeNanos;
  startNanos = System.nanoTime();
  final long jitterNanos = startNanos - frameTimeNanos;
  if (jitterNanos >= mFrameIntervalNanos) {
    final long skippedFrames = jitterNanos / mFrameIntervalNanos;
    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;
    if (DEBUG_JANK) {
      Log.d(TAG, "Missed vsync by " + (jitterNanos * 0.000001f) + " ms " + "which is more than the frame interval of " + (mFrameIntervalNanos * 0.000001f) + " ms! " + "Skipping " + skippedFrames + " frames and setting frame " + "time to " + (lastFrameOffset * 0.000001f) + " ms in the past.");
    }
    frameTimeNanos = startNanos - lastFrameOffset;
  }
  ...
}

上述数据的获取并不是那么的直接,所以需要一定的手段。方法一共有三种,都不难:

  • Logcat 方案

缺点:该方案需要系统授权 “Adb Root” 权限,用于修改系统属性;对于丢帧信息只能统计分析,无法进行实时处理。

优点:设置完成后,可以获取系统中所有应用各自的绘制丢帧情况(丢帧发生的时间以及连续丢帧的数量)。

其实,仔细观察代码,我们就可以注意到 Choreographer 源码中本身就有输出的方案:

if (skippedFrames >= SKIPPED_FRAME_WARNING_LIMIT) {
  Log.i(TAG, "Skipped " + skippedFrames + " frames! " + "The application may be doing too much work on its main thread.");
}

唯一阻碍我们获取数值的是:skippedFrames 的数值只有大于 SKIPPED_FRAME_WARNING_LIMIT 才会输出相关的警告。而 SKIPPED_FRAME_WARNING_LIMIT 的数值可以由系统参数 debug.choreographer.skipwarning 来设定。

注意:初始条件下,系统中不存在 debug.choreographer.skipwarning 参数,因此 SKIPPED_FRAME_WARNING_LIMIT 将取默认值 30。因此,正常情况下,我们能够看见上诉 Log 出现的机会极少。

因此,如果我们修改(设定)系统属性 debug.choreographer.skipwarning 为 1,Logcat 中将打印出每一次丢帧的Log。需要说明的是,由于为 SKIPPED_FRAME_WARNING_LIMIT 赋值的代码段由 Zygote 在系统启动阶段加载,而其他应用都是在拷贝复用 Zygote 中的设定,因此设定系统属性后需要重启 Zygote 才能使得上述设定生效。

具体的设置方法如下:

setprop debug.choreographer.skipwarning 1
setprop ctl.restart surfaceflinger; setprop ctl.restart zygote

设定完成以后,我们可以直接通过 Logcat 中的信息得到系统中所有应用的绘制丢帧信息,包括丢帧发生的时间以及连续丢帧的数量。不过由于 Logcat 信息的滞后性,以上信息我们几乎只能进行在测试完成后进行统计分析,而无法进行实时处理。

  • Choreographer.FrameCallback 方案

缺点:该方案需要将测试代码与待测应用打包在一起,因此理论上仅能测试自己开发的应用。

优点:可以对丢帧信息进行实时处理

我们先来看看 Choreographer.FrameCallback 的定义。

Implement this interface to receive a callback when a new display frame is being rendered. The callback is invoked on the Looper thread to which the Choreographer is attached.

通过这个接口,我们可以在每一帧被渲染的时候记录下它开始渲染的时间,这样在下一帧被处理是,我们不仅可以判断上一帧在渲染过程中是否出现掉帧,而整个过程都是实时处理的,这为我们可以及时获取相关的调用栈信息来辅助定位潜在的性能缺陷有极大的帮助。

  • 代码注入方案

缺点:该方案需要通过注入程序为指定应用注入测试代码,因此需要系统为注入程序授权 “应用Root” 权限。

优点:与 Choreographer.FrameCallback 方案一致。

该方案可以简单理解为通过注入的方式来实现与 Choreographer.FrameCallback 方案一样的目的。因此,这里我们主要讨论两者在实现方式上的区别。

显而易见,我们需要注入的对象是 Choreographer ,因此理论上任何第三方应用都是可以被注入的。但是随着 Android 系统对”应用Root” 权限管理越来越严格,所以该方案可用的范围越来越小。

SM 指标的计算方法

根据定义,SM 其实类似于 FPS,它被设计为可以衡量应用平均每秒执行 doFrame() 的次数。我们可以认为它是在衡量 Surface 渲染轮询的次数。

针对 Logcat 方案,我们只需统计测试过程中目标进程一共掉了多少帧,由于对于绝大多数应用在没有丢帧的情况下会针对每一次 VSYNC 信号执行一次 doFrame(),而 VSYNC 绝大多数情况下每秒会触发 60 次,因此我们可以反向计算得出 SM 的数值:

SM = (60* totalSeconds - totalSkippedFrames) / totalSeconds;

针对 Choreographer.FrameCallback 方案 以及 代码注入方案,我们需要在代码中自己进行统计输出(可以是设计成实时的,也可以设计成测试结束后进行统计计算的)。

Skipped frames 指标的计算方法

这个指标的就是指当前应用在丢帧发生时的丢帧帧数。

针对 Logcat 方案, 该数值直接在 Logcat 中输出,并且带有时间信息。

04-18 16:31:24.957 I/Choreographer(24164): Skipped 4 frames!  The application may be doing too much work on its main thread.
04-18 16:31:25.009 I/Choreographer(24164): Skipped 2 frames!  The application may be doing too much work on its main thread.

针对 Choreographer.FrameCallback 方案 以及 代码注入方案,我们可能很方便的通过计算前后两帧开始渲染的时间差获得这一数值,同样方便。同样与 Logcat 方案 不同的是,它也是可以设计成实时计算的。

小结

通过对各个显示性能指标的分析,我们可以知道,虽然目前指标众多,但其实有本质区别的指标确很少:

  • 系统层面:

    • 合成(上屏)帧率:FPS
  • 应用层面:
    • 跳帧次数:Aggregate frame stats、Jankiness count、Skipped frames
    • 跳帧幅度:Aggregate frame stats、Max accumulated frames、Skipped frames
    • 绘制帧率:Frame rate
    • 绘制轮询频率:SM

更为重要的是,我们从上述的分析中知道了各个指标都有着自己的优势和不足,这也从根本上决定了它们各自有各自的用法。

Step 3:你要到哪儿去——这些指标如何落地来指导优化

其实指标的用法也是多种多样的,为了方便讨论,我们仅从日常监控、缺陷定位以及数据上报三个方面来讨论各个显示性能指标是如何落地的。

日常监控

  • FPS:数据形式最为直观(FPS 是最早的显示性能指标,而且在多个平台中都有着类似的定义),且对系统平台的要求最低(API level 1),游戏、视频等连续绘制的应用可以考虑选用,但不适用于绝大多数非连续绘制的应用;
  • SM:数据形式与 FPS 类似,可以很好的弥补 FPS 无法准确刻画非连续绘制的应用显示性能的缺陷;
  • Aggregate frame stats:除了对系统平台有较高的要求以外,其采集方式最为简单(系统自带功能);
  • Skipped frames:与 Aggregate frame stats 类似, 信息量相对较少,但可适用范围更广

特别说明:Jankiness count、Max accumulated frames、Frame rate 只统计了128帧的信息(约2~3秒),而且 Jankiness count、Max accumulated frames对于掉帧情况的计算并非是一个准确值,因此这些指标都不太适用于日常监控

缺陷定位

  • Skipped frames:基于 Choreographer.FrameCallback 方案实现的 Skipped frames 指标,可以在卡顿出现的时刻获取应用堆栈信息,可以在一定程度上进行缺陷定位

特别说明:

1. FrameInfo 相关指标无法直接进行缺陷定位,但 FrameInfo 当中包含了大量详尽的绘制基础数据,对于缺陷定位也有较大帮助;

2. 关于缺陷定位过程中连续掉帧阈值的选取,可参考维基百科中提到几个重要的帧率数值:

- 12 fps:由于人类眼睛的特殊生理结构,如果所看画面之帧率高于每秒约10-12帧的时候,就会认为是连贯的

- 24 fps:有声电影的拍摄及播放帧率均为每秒24帧,对一般人而言已算可接受

- 30 fps:早期的高动态电子游戏,帧率少于每秒30帧的话就会显得不连贯,这是因为没有动态模糊使流畅度降低

- 60 fps:在实际体验中,60帧相对于30帧有着更好的体验

以上各数据分别对应: 0 帧、1帧、2.5帧、5帧。

数据上报

  • Aggregate frame stats:除了对系统平台有较高的要求以外,其采集方式最为简单(系统自带功能)、数据也比较清晰,相信基于这类指标实现性能数据上报是特别方便的
  • Skipped frames :基于 Choreographer.FrameCallback 方案实现的 Skipped frames 指标,采集方式简单,实现基础性能数据上报、卡顿数据上报也是很方便的

小结

发现了没有 Skipped frames 的用处很大有没有? 而且通读全篇,你会发现 Aggregate frame stats、Jankiness count、Max accumulated frames 这些指标都有提供类似的功能。

友情附赠: 现有显示性能指标对比

本来写到这里本文的主要内容就应该结束了,但是如果不对比一下显示性能指标神马的,总会让人觉得缺少了一些什么。

友情提示:下述内容相对主观,建议各位读者依据项目情况自行进行选择。

指标名称 指标意义 基础数据来源 采集方式 适用系统 适用应用 用途
FPS 系统合成帧率 SurfaceFlinger adb shell 监控
Aggregate frame stats 应用跳帧次数、幅度 FrameInfo adb shell 最低23 HW Rendering 监控/上报
Jankiness count (估算)应用跳帧次数 FrameInfo(128帧) adb shell HW Rendering 定位
Max accumulated frames (估算)应用跳帧幅度 FrameInfo(128帧) adb shell HW Rendering 定位
Frame rate 应用绘制帧率 FrameInfo(128帧) adb shell HW Rendering 定位
SM 应用绘制轮询频率 Choreographer 多种方式 最低16 SW/HW Rendering 及 部分 OpenGL Rendering 监控
Skipped frames 应用跳帧次数、幅度 Choreographer 多种方式 最低 16 SW/HW Rendering 及 部分 OpenGL Rendering 监控/定位/上报
时间: 2024-10-11 21:07:27

当我们讨论流畅度的时候,我们究竟在说什么?的相关文章

Android流畅度之帧率

背景:app改版,人为感受卡顿,需要客观数据支撑观点.故,搜索各种性能指标,并理解之.(这是一篇摘要文......) 首先,明确人为感受的性能不好属于下面哪种: 1. 响应时间,界面跳转后响应时间: 2. 流畅度,界面操作时或动画展示的效果: 而流畅度的衡量指标又有几种: 1. 帧率fps(Frames Per Second,每秒钟填充图像的帧率) 2. 丢帧SF(Skipped frame) 3. 流畅度SM(SMoothness)(腾讯分享) 其中得到最广泛使用的还是帧率.以下详细说明之 回

腾讯GT的流畅度测试方案研究

GT源码:https://github.com/TencentOpen/GT 一.流畅度模块的代码结构 流畅度插件总共就几个类,其实处理方式也比较简单粗暴,就是通过Choreographer输出的log信息获取跳帧数据.SMActivity.java为插件的入口类,你可以通过预设环境操作来实现log打印操作,然后通过SMLogService.java过滤出当前进程的丢帧值,最后由SMServiceHelper.java来进行数据处理.流畅度值为60减去1s内的跳帧数. 二.流畅度测试 1.简要流

Android流畅度测试

Android流畅度测试 测试方法一:系统自带-开发者模式 测试方法二:FPS Meter测试安卓帧数 H5页面加载速度:window.performance.timing 测试方法一:系统自带-开发者模式 实际上,为了方便开发者测试,安卓本身就内置了流畅度检测的功能.不过,这需要我们开启隐藏的开发者选项.如果你在用原生系统,那么开启开发者选项的方法很简单,进入到设置菜单“关于手机”页面,点击数次“版本号”,即可开启开发者选项.如果用的是其他ROM,方法也许有所不同,比如说魅族的Flyme开启开

提高Android应用手写流畅度(基础篇)

在使用android类的手写应用时,整体上都有这样一个印象:android的手写不流畅.不自然,和苹果应用比起来相差太远.本文结合作者亲身经历,介绍一下有效提高手写流畅度的几种方法: 1.未做任何处理的手写效果: 这是一个自定义的view,通过在onTouchEvent时间中捕获系统回调的触摸点信息,然后再onDraw方法里面刷新,可以明显地感觉到线条很生硬,并且在手写的过程中跟随感很差,反应迟钝,具体代码如下: package com.mingy.paint.view; import andr

UI的流畅度优化

Android中所有的界面绘制工作都是在UI线程中进行的,提高UI流畅度的最核心根本在于释放UI线程.即:不在主线程中做耗时的操作. 很多人都知道,耗时的操作要放到子线程中去做,比如访问网络,比如读写sd卡.像这类操作大家都会很自然的想到使用子线程来完成耗时的操作,等操作结束之后,再通过Handler通知主线程进行界面的更新.这是非常正确的方法.但是有一类方法,它必须得运行在在UI线程中,就是布局文件的加载.如果这类方法花的时间太多了,也是会对流畅度产生很大的影响.今天我们就来讲讲布局文件的优化

基于jq流畅度非常好的图片左右切换焦点图

今天给大家分享一款基于jq流畅度非常好的图片左右切换焦点图.这是一款基于jQuery实现的支持鼠标拖动切换jQuery特效下载.效果图如下: 在线预览   源码下载 实现的代码. html代码: <svg xmlns="http://www.w3.org/2000/svg" version="1.1" class="filters hidden"> <defs> <filter id="blur"

U3D开发中提升流畅度的几个方法

  很多人在用unity3D开发游戏的过程中都会遇到一个问题,那就是流畅度,有时候这的确是一个很糟糕的事情,尤其是当我们的机器配置不理想的时候.下面为大家提供几个有效的解决方法. 首先,我们如果是在PC端开发一款产品,那么我们就需要把我们产品的顶点数保持在20万-300万之间,这还是主要依赖于我们目标的GPU的. 如果我们的产品中使用了内置的着色器,那么我们最好是去选用Mobile或者是Unlit种类的着色器,因为这两个着色器可以非常完美的除移动以外的平台上良好的运行的.作为复杂着色器的简化或者

提升c++builder 代码输入流畅度的配置

提高c++builder 代码输入流畅度 1.输入指针的函数名后,识别函数参数移动光标到括弧内,此功能太慢,有明显延迟,建议关闭.关闭以后,输入函数名不会自动添加(),需要自己手动输入括弧了,不过速度快了啊.要看函数参数的定义快捷键Ctrl+Shift+Space this->Focused() this->FocusControl(); 去掉下面2个勾,此功能是IDE自带. Auto Parenthesis Code Parameters 2.输入任意两个字母,弹出相关的变量.函数等方便选择

【MIG专项测试组】如何准确评测Android应用的流畅度?

转自 腾讯Bugly 叶方正,2008年加入腾讯,就职于无线研发部[专项测试组].曾经负责多个产品的性能优化工作,积累大量的移动终端平台优化以及评测经验. 怎样获取SM值? 前文我们分析了通过测量应用的帧率FPS并不能准确评价App的流畅度(如何量化Android应用的“卡”?流畅度原理&定义篇),FPS较低并不能代表当前App在UI上界面不流畅,而1s内VSync这个Loop运行了多少次更加能说明当前App的流畅程度. 那么我们可以直接在App代码中通过Choreographer的回调Fram