热文导读| 点击标题阅读
互联网寒冬下,程序员如何突围提升自己?
Flutter 与 React Native 谁主沉浮?
新东方旗下公司被曝禁止员工午休,CEO公开道歉被前员工打脸!
作者:sunhapper
链接:https://www.jianshu.com/p/c647d090e9c8
流畅度应该是终端用户感知最明显的性能指标了,提升流畅度是提升用户体验性价比最高的方式之一,我们先来看看在系统层面上Google为了优化流畅度做了哪些努力
垂直同步是一个游戏中很常见的概念,它的出现是为了解决如下图的画面撕裂的问题
究其原因是屏幕的刷新并不是瞬时完成的,而GPU产生一帧新画面的速度和屏幕刷新速度不同步,当GPU速度又大于显示器的刷新速度,在显示器从上到下扫描显示的过程中图像缓冲就被更新了,显示器并不知道这个变化还是继续扫描,就产生了画面撕裂
android 4.1的黄油计划引入垂直同步之后,只有在接收到Vsync信号,系统才会让CPU/GPU开始下一帧的渲染工作,即每个屏幕刷新周期之间最多只会产生一帧画面,以此避免画面撕裂
一旦收到VSync信号,立刻就开始执行下一帧的绘制工作。这样也可以大大降低Jank出现的概率。只需要保证渲染一帧画面的时间在1/60s(16ms)就行了
先看看双缓冲的模型
两个缓存区分别为 Back Buffer 和 Frame Buffer。GPU 向 Back Buffer 中写数据,屏幕从 Frame Buffer 中读数据。VSync 信号负责调度从 Back Buffer 到 Frame Buffer 的复制操作,可认为该复制操作在瞬间完成(只是交换了内存地址)。
如果所有渲染操作都在16ms之内完成,双重缓冲可以很好的工作,但是渲染耗时超过16ms呢
第一个B画面的渲染超过了16ms,因为此时B画面占据了Back Buffer,所以当接收到下一帧的VSync信号时系统没有开始渲染工作,导致jank的发生
而三重缓冲增加了一个Back Buffer
在接收到VSync信号,B画面还在渲染,因为CPU已经空闲了,而且有另一块缓冲区,所以同时开始了下一帧的渲染工作
三重缓冲可以更充分的利用CPU/GPU提升画面显示的流畅度
硬件加速就是依赖GPU实现图形绘制加速。
可以看出GPU的ALU(算术逻辑单元)比CPU多的多,而图形处理和栅格化操作实际就是大量的数学计算,所以用GPU去渲染图形比CPU快的多
android 3.0引入了硬件加速,android4.0以后默认开启了硬件加速
当启动硬件加速后,Android 使用 “DisplayList” 组件进行绘制而非直接使用 CPU 绘制每一帧。DisplayList 是一系列绘制操作的记录,抽象为 RenderNode 类。
这样间接的进行绘制操作的优点很多:
RenderThread是Android 5.0引入的功能。
渲染工作的真正执行者是 GPU,而 GPU是不知道什么是动画的:执行动画的唯一途径便是将每一帧的不同绘制操作分发给 GPU,但该逻辑本身不能在 GPU 上执行。
而如果在 UI 线程执行该操作,任意的重操作都将阻塞新的绘制指令及时分发,动画就很容易出现延迟和卡顿。
添加一个RenderThread专门用来处理渲染的相关操作,UI线程只管计算生成一个DisplayList,剩下的渲染相关的事情就交给RenderThread,这样减轻了UI线程的负担,也提升了动画的流畅度
渲染场景 | 纯软件绘制 | 硬件加速 | 加速效果分析 |
---|---|---|---|
页面初始化 | 绘制所有View | 创建所有DisplayList | GPU分担了复杂计算任务 |
在一个复杂页面调用背景透明TextView的setText(),且调用后其尺寸位置不变 | 重绘脏区所有View | TextView及每一级父View重建DisplayList | 重叠的兄弟节点不需CPU重绘,GPU会自行处理 |
TextView逐帧播放Alpha / Translation / Scale动画 | 每帧都要重绘脏区所有View | 除第一帧同场景2,之后每帧只更新TextView对应RenderNode的属性 | 刷新一帧性能极大提高,动画流畅度提高 |
修改TextView透明度 | 重绘脏区所有View | 直接调用RenderNode.setAlpha()更新 | 加速前需全页面遍历,并重绘很多View;加速后只触发DecorView.updateDisplayListIfDirty,不再往下遍历,CPU执行时间可忽略不计 |
系统层面对于UI流畅度的优化措施
google为了画面的流畅度可谓是用心良苦,作为一个有追求的开发者,我们当然也要朝着如丝般顺滑努力,首先先从数据收集开始
这个就是传说中的玄学曲线了,可以通过它可视化的直观掌握当前界面是否流畅
绿线是16ms的分界线,每一个竖条代表渲染一帧的耗时,要保证流畅理论上需要每一条都在绿线之下
有几个关键点需要注意一下
优点:
缺点:
android 6.0以上设备使用adb shell dumpsys gfxinfo
可以获取到Aggregate frame stats
Stats since: 752958278148nsTotal frames rendered: 82189Janky frames: 35335 (42.99%)90th percentile: 34ms95th percentile: 42ms99th percentile: 69msNumber Missed Vsync: 4706 //垂直同步失败Number High input latency: 142 //因为处理输入耗时Number Slow UI thread: 17270 //UI线程任务过重造成的超时Number Slow bitmap uploads: 1542 //加载bitmap导致的超时Number Slow draw: 23342 //绘制太慢导致的超时
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 //UI线程任务过重造成的超时
Number Slow bitmap uploads: 1542 //加载bitmap导致的超时
Number Slow draw: 23342 //绘制太慢导致的超时
使用adb shell dumpsys gfxinfo
可以重置数据,结束进程不会重置Aggregate frame stats
使用adb shell dumpsys gfxinfo
可以获取上120帧的详细耗时,这个数据跟GPU呈现模式的条形图是对应的
数据的单位是纳秒,通过统计计算可以获得每一帧的各阶段耗时
具体每一列数据代表什么可以看下面的链接
Framestats data format
优点:
缺点:
从 7.0(API 24)开始,安卓 SDK 新增 OnFrameMetricsAvailableListener 接口用于提供帧绘制各阶段的耗时,数据源与 GPU Profile 相同。
public void startFrameMetrics(View view) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { final String activityName = getClass().getSimpleName(); listener = new Window.OnFrameMetricsAvailableListener() { private int allFrames = 0; private int jankyFrames = 0; @Override public void onFrameMetricsAvailable(Window window, FrameMetrics frameMetrics, int dropCountSinceLastInvocation) { FrameMetrics frameMetricsCopy = new FrameMetrics(frameMetrics); allFrames++; float totalDurationMs = (float) (0.000001 * frameMetricsCopy.getMetric( FrameMetrics.TOTAL_DURATION)); if (totalDurationMs > 17) { jankyFrames++; String msg = String.format("Janky frame detected on %s with total duration: %.2fms\n", activityName, totalDurationMs); float layoutMeasureDurationMs = (float) (0.000001 * frameMetricsCopy.getMetric( FrameMetrics.LAYOUT_MEASURE_DURATION)); float drawDurationMs = (float) (0.000001 * frameMetricsCopy.getMetric( FrameMetrics.DRAW_DURATION)); float gpuCommandMs = (float) (0.000001 * frameMetricsCopy.getMetric( FrameMetrics.COMMAND_ISSUE_DURATION)); float othersMs = totalDurationMs - layoutMeasureDurationMs - drawDurationMs - gpuCommandMs; float jankyPercent = (float) jankyFrames / allFrames * 100; msg += String.format("Layout/measure: %.2fms, draw:%.2fms, gpuCommand:%.2fms others:%.2fms\n", layoutMeasureDurationMs, drawDurationMs, gpuCommandMs, othersMs); msg += "Janky frames: " + jankyFrames + "/" + allFrames + "(" + jankyPercent + "%)" + dropCountSinceLastInvocation; Log.e("FrameMetrics", msg); } }}; getWindow().addOnFrameMetricsAvailableListener(listener, new Handler()); } else { Log.w("FrameMetrics", "FrameMetrics can work only with Android SDK 24 (Nougat) and higher"); } }
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
final String activityName = getClass().getSimpleName();
listener = new Window.OnFrameMetricsAvailableListener() {
private int allFrames = 0;
private int jankyFrames = 0;
@Override
public void onFrameMetricsAvailable(Window window, FrameMetrics frameMetrics,
int dropCountSinceLastInvocation) {
FrameMetrics frameMetricsCopy = new FrameMetrics(frameMetrics);
allFrames++;
float totalDurationMs = (float) (0.000001 * frameMetricsCopy.getMetric(
FrameMetrics.TOTAL_DURATION));
if (totalDurationMs > 17) {
jankyFrames++;
String msg = String.format("Janky frame detected on %s with total duration: %.2fms\n",
activityName, totalDurationMs);
float layoutMeasureDurationMs = (float) (0.000001 * frameMetricsCopy.getMetric(
FrameMetrics.LAYOUT_MEASURE_DURATION));
float drawDurationMs = (float) (0.000001 * frameMetricsCopy.getMetric(
FrameMetrics.DRAW_DURATION));
float gpuCommandMs = (float) (0.000001 * frameMetricsCopy.getMetric(
FrameMetrics.COMMAND_ISSUE_DURATION));
float othersMs = totalDurationMs - layoutMeasureDurationMs - drawDurationMs - gpuCommandMs;
float jankyPercent = (float) jankyFrames / allFrames * 100;
msg += String.format("Layout/measure: %.2fms, draw:%.2fms, gpuCommand:%.2fms others:%.2fms\n",
layoutMeasureDurationMs, drawDurationMs, gpuCommandMs, othersMs);
msg += "Janky frames: " + jankyFrames + "/" + allFrames + "(" + jankyPercent + "%)"
+ dropCountSinceLastInvocation;
Log.e("FrameMetrics", msg);
}
}
};
getWindow().addOnFrameMetricsAvailableListener(listener, new Handler());
} else {
Log.w("FrameMetrics", "FrameMetrics can work only with Android SDK 24 (Nougat) and higher");
}
}
FrameMetrics中包含了渲染一帧各个阶段的耗时数据
优点:
缺点:
这种检测流畅度的方法起源于FaceBook的一次关于UI流畅度的技术分享The Road to 60FPS
Choreographer是一个接收VSync信号并分发的组件,Choreographer 收到通知依次处理 Input、Animation、Draw,这三个过程都是通过 FrameCallback 回调的方式完成的。
通过Choreographer.FrameCallback
可以获取到VSync信号开始被处理的时间戳,减去上一个时间戳,可以近似为上一帧的渲染耗时(只计算了UI线程的耗时,计算不到渲染线程和GPU耗时)
public void postFrameCallback(View view) { lastTime = System.nanoTime(); Choreographer.getInstance().postFrameCallback(this); } @Override public void doFrame(long frameTimeNanos) { //每个FrameCallback都只会回调一次,所以需要在回调中注册下一帧VSync信号的回调 Choreographer.getInstance().postFrameCallback(this); long jitterNanos = frameTimeNanos - lastTime; if (jitterNanos > FRAME_INTERVAL_NANOS) { Log.i(TAG, "doFrame: lastTime:" + lastTime + " frameTimeNanos:" + frameTimeNanos + " frame:" + jitterNanos); lastTime = frameTimeNanos; }
lastTime = System.nanoTime();
Choreographer.getInstance().postFrameCallback(this);
}
@Override
public void doFrame(long frameTimeNanos) {
//每个FrameCallback都只会回调一次,所以需要在回调中注册下一帧VSync信号的回调
Choreographer.getInstance().postFrameCallback(this);
long jitterNanos = frameTimeNanos - lastTime;
if (jitterNanos > FRAME_INTERVAL_NANOS) {
Log.i(TAG,
"doFrame: lastTime:" + lastTime + " frameTimeNanos:" + frameTimeNanos
+ " frame:" + jitterNanos);
lastTime = frameTimeNanos;
}
Choreographer.FrameCallback
和之前的gfxinfo的数据有一些不同
优点:
缺点:
这个是网上比较多介绍的获取数据计算fps的方式,但是自己试验获取不到有效数据,所以计算fps只能通过gfxinfo中获取的数据了
流畅度相关数据的收集
通过上面的介绍,我们收集到了两种与流畅度相关的核心数据
最直观的数据就是每一帧的渲染耗时了,当一帧耗时大于1/60s,即使只超过1ms,这一帧依然会错过一个VSync,到下一个VSync信号产生时才能显示到屏幕上。看上去这个值是和流畅度紧密相关的,但是会不会有什么问题呢?
先来看一个极端情况
这里每一帧渲染都是超过16ms的,但是因为三重缓冲和RenderThread的存在,这里是有60fps的,只不过显示出现了1/30s的延迟,并不会让用户察觉有什么异样。
所以单纯用单位时间渲染超时的帧数量来衡量流畅度其实并不太合理,甚至于google做的那些底层优化就是为了让我们的应用在渲染超过16ms时依然有良好的流畅度表现。
FPS这是最常用的衡量画面流畅度的指标。
不过在Android系统中用FPS用fps来衡量流畅度却有些缺陷和不便
首先先说明下什么是丢帧,理论上屏幕的刷新率是60hz,加入垂直同步机制之后,每秒渲染的画面上限就是60,因为某一个VSync信号产生时因为UI线程卡顿或者图像缓冲全部被占据等情况导致这一个VSync没有被响应,这种情况就是丢帧。而在画面没有更新的情况下没有新的帧需要渲染,这种情况并不是丢帧。
注意一下丢帧与渲染超时的区别
因为丢帧的计算实际是依赖于对VSync信号的响应,自然得用到Choreographer.FrameCallback
public void postFrameCallback(View view) { lastTime = System.nanoTime(); Choreographer.getInstance().postFrameCallback(this); disposable = Flowable.interval(200, TimeUnit.MILLISECONDS) .map(new Function() {
@Override public Integer apply(Long aLong) { Log.i(TAG, "apply: " + aLong); int sm = (frameCount - lastFrameCount) * 1000 / 200; lastFrameCount = frameCount; return sm; } }) .subscribe(new Consumer() { @Override public void accept(Integer sm) throws Exception {StringBuilder builder = new StringBuilder();builder.append("Smoothness :").append(sm);for (int i = 0; i < skipCount.length; i++) { if (i == 0) { builder.append(" normal: "); } else { builder.append(" skip ").append(i).append(" frames: "); } builder.append(skipCount[i]); } Log.i(TAG, builder.toString()); } }, new Consumer() { @Override public void accept(Throwable throwable) throws Exception { } }); } @Override public void doFrame(long frameTimeNanos) { Choreographer.getInstance().postFrameCallback(this); long jitterNanos = frameTimeNanos - lastTime; frameCount++; //index表示两个VSync信号之间被忽略的信号数量,即丢帧 //FRAME_INTERVAL_NANOS取了17ms,因为取1/60s转换成纳秒进行比较的话数值太接近了,稍有误差都会比较大的影响结果 int index = (int) (jitterNanos / FRAME_INTERVAL_NANOS); if (index > 7) { index = 7; } skipCount[index]++; lastTime = frameTimeNanos; }@Override
public Integer apply(Long aLong) {
Log.i(TAG, "apply: " + aLong);
int sm = (frameCount - lastFrameCount) * 1000 / 200;
lastFrameCount = frameCount;
return sm;
}
})
.subscribe(new Consumer() {
@Override
public void accept(Integer sm) throws Exception {
StringBuilder builder = new StringBuilder();
builder.append("Smoothness :").append(sm);
for (int i = 0; i < skipCount.length; i++) {
if (i == 0) {
builder.append(" normal: ");
} else {
builder.append(" skip ").append(i).append(" frames: ");
}
builder.append(skipCount[i]);
}
Log.i(TAG, builder.toString());
}
}, new Consumer() {
@Override
public void accept(Throwable throwable) throws Exception {
}
});
}
@Override
public void doFrame(long frameTimeNanos) {
Choreographer.getInstance().postFrameCallback(this);
long jitterNanos = frameTimeNanos - lastTime;
frameCount++;
//index表示两个VSync信号之间被忽略的信号数量,即丢帧
//FRAME_INTERVAL_NANOS取了17ms,因为取1/60s转换成纳秒进行比较的话数值太接近了,稍有误差都会比较大的影响结果
int index = (int) (jitterNanos / FRAME_INTERVAL_NANOS);
if (index > 7) {
index = 7;
}
skipCount[index]++;
lastTime = frameTimeNanos;
}
这里不仅计算了每秒响应的VSync信号数量作为流畅度(实时数据),还记录了不同连续丢帧发生的次数(可以制定多个维度的数据上报)
综合兼容性,数据与实际场景的契合程度还有计算复杂度,使用Choreographer统计VSync信号是更为合理的选择
更多学习和讨论,欢迎加入我们的知识星球,这里有1000+小伙伴,让你的学习不寂寞~·
看完本文有收获?请转发分享给更多人
我们的知识星球第三期开期了,已达到1100人了,能连续做三期已很不容易了,有很多老用户续期,目前续期率达到50%,说明了大家对我们的知识星球还是很认可的,欢迎大家加入尽早我们的知识星球,更多星球信息参见:
欢迎加入Java和Android架构社群
如何进阶成为Java的Android版和架构师?
说两件事
微信扫描或者点击上方二维码领取的Android \ Python的\ AI \的Java等高级进阶资源
更多学习资料点击下面的“阅读原文 ”获取
谢谢老板,点个好看↓