一、为什么卡顿监控如此重要
一个APP从0-1的阶段是疯狂堆功用的阶段,或许不会考虑功能问题和代码规范问题,可是一旦这个APP做到老练之后咱们就需要来考虑这个APP的功能问题了,启动是否卡顿,滑动是否流通,这些都是对于用户体会至关重要的东西,这也是一个APP是否老练的重要规范和指标。
二、咱们能做什么
提到卡顿和流通就不得不提一个指标便是FPS,一般卡顿问题是主线程执行了太多的耗时操作,例如渲染,布局,输入等,阻塞了主线程的音讯循环导致单个音讯处理进程,音讯揉捏过多那么掉帧就会很显着,用户也就看到你的APP卡的不可,降低用户信任。解决卡顿问题很重要,可是更重要的是咱们能够监控到卡顿,FPS便是这样的指标,下面来说下咱们怎样来自己核算FPS。
三、监控原理
咱们人眼所能看到的接连画面,其实能够简略理解为是一张一张的图片形成的,如果按照一秒60张,算下来16ms便是一张新的图片,这个进程是很快速的,可是如果咱们应用卡顿发生了,那么或许在100ms,200ms,500ms等就展现了一张图片,那么用户就会显着看到卡顿了,那咱们就能够知道一个核算公式:
// 帧率
帧率 = (单位图片次数) / (一张图片的耗时)
咱们能够定义一个作为帧率的概念,用FPS来标识,也便是在一秒内能展现多少张图片的意思,单位时刻能够当作1s,一张图片的的耗时能够跟机器硬件相关起来,例如之前的手机是16.6ms来展现一帧的(现在由于有高刷的状况 ,这个值会更低)。
从这个公式咱们就知道了,单位时刻是不变的,一张图片的耗时是跟设备有关系的,设备越好,值越低,会让帧率越高,帧率越高咱们的体会就会更好,越流通。
那咱们扩展下这个公式就能够这样展现:
// 帧率
帧率 = (从第一张到最后一张图片总次数) / (从第一张到最后一张的总时刻)
// 变成代码跟清晰
fps = (sumFrames - lastSumFrames)/ (frameCostTimes - lastFrameCostTimes)
这样它的基本原理,核算规矩咱们就都清楚了,咱们接下来就找到合适的地方按照此规矩核算即可,
伪代码类似于这样:
// 首要定义四个变量,也便是代表上述公式核算的首要值
long sumFrames = 0L;
long lastSumFrames = 0L;
long frameCostTimes = 0L;
long lastFrameCostTimes = 0L;
fun calcFps() {
frameCostTimes = xxx //这个应该怎样核算才是核心
sumFrames += 1
if (duration > 200) {// 不需要每次都核算,隔200间隔再核算
val fps = (sumFrames - lastSumFrames) / (frameCostTime - lastFrameCostTime)
lastSumFrames = sumFrames
}
}
四、代码实战
咱们要找到Android
代码中帧率变化的地方,参考了Android
官方文档,和Matrix
中监控的完成,有以下集中方法能够来做:
- 运用
Choreographer
来做,这个是Android
系统供给的和硬件Vsync
信号(也便是16.6ms的硬件来源)接收和处理的地方 - 7.0以上的设备,官方直接供给了API,能够用
addOnFrameMetricsAvailableListener
来做 - 参考
Matrix
中的方法,由于所有的绘制音讯,接触音讯,都是跟主线程的looper
有关系,那么能够监听主线程的音讯来看每个音讯的耗时然后核算帧率
4.1 Looper方法
结合Matrix
源码,是7.0以上的版别运用官方的API
,7.0以下的版别是经过主线程的音讯循环,那么咱们能够参照这种方法来讲一下它的具体完成,至于Choreographer
的方法,音讯循环也涉及到就在说了。
咱们能够看下Matrix
中的代码是如何来做的:
private synchronized void resetPrinter() {
...
// 设置了logging
looper.setMessageLogging(printer = new LooperPrinter(originPrinter));
...
}
咱们再看下LooperPrinter干了什么,看下Looper的代码就更清晰了
public void println(String x) {
if (null != origin) {
origin.println(x);
if (origin == this) {
throw new RuntimeException(TAG + " origin == this");
}
}
if (!isHasChecked) {
isValid = x.charAt(0) == '>' || x.charAt(0) == '<';
isHasChecked = true;
if (!isValid) {
MatrixLog.e(TAG, "[println] Printer is inValid! x:%s", x);
}
}
if (isValid) {
// 它会依据Looper里边的音讯,来判别是音讯处理前和后,这样就能够来做音讯的耗时核算了
dispatch(x.charAt(0) == '>', x);
}
}
// 音讯处理前
logging.println(">>>>> Dispatching to " + msg.target + " " +
msg.callback + ": " + msg.what);
......
// 音讯处理后
logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
那咱们接着看UIThreadMonitor.java的代码就知道前后是怎样处理的了
// 处理音讯前
private void dispatchBegin() {
token = dispatchTimeMs[0] = System.nanoTime();
dispatchTimeMs[2] = SystemClock.currentThreadTimeMillis();
if (config.isAppMethodBeatEnable()) {
AppMethodBeat.i(AppMethodBeat.METHOD_ID_DISPATCH);
}
synchronized (observers) {
for (LooperObserver observer : observers) {
if (!observer.isDispatchBegin()) {
observer.dispatchBegin(dispatchTimeMs[0], dispatchTimeMs[2], token);
}
}
}
if (config.isDevEnv()) {
MatrixLog.d(TAG, "[dispatchBegin#run] inner cost:%sns", System.nanoTime() - token);
}
}
// 处理音讯后
private void dispatchEnd() {
long traceBegin = 0;
if (config.isDevEnv()) {
traceBegin = System.nanoTime();
}
if (config.isFPSEnable() && !useFrameMetrics) {
// 开端时刻
long startNs = token;
// Vsync信号接收的时刻
long intendedFrameTimeNs = startNs;
if (isVsyncFrame) {
doFrameEnd(token);
intendedFrameTimeNs = getIntendedFrameTimeNs(startNs);
}
long endNs = System.nanoTime();
synchronized (observers) {
for (LooperObserver observer : observers) {
if (observer.isDispatchBegin()) {
// 最重要的代码是在这里
observer.doFrame(AppActiveMatrixDelegate.INSTANCE.getVisibleScene(), startNs, endNs, isVsyncFrame, intendedFrameTimeNs, queueCost[CALLBACK_INPUT], queueCost[CALLBACK_ANIMATION], queueCost[CALLBACK_TRAVERSAL]);
}
}
}
}
if (config.isEvilMethodTraceEnable() || config.isDevEnv()) {
dispatchTimeMs[3] = SystemClock.currentThreadTimeMillis();
dispatchTimeMs[1] = System.nanoTime();
}
AppMethodBeat.o(AppMethodBeat.METHOD_ID_DISPATCH);
synchronized (observers) {
for (LooperObserver observer : observers) {
if (observer.isDispatchBegin()) {
observer.dispatchEnd(dispatchTimeMs[0], dispatchTimeMs[2], dispatchTimeMs[1], dispatchTimeMs[3], token, isVsyncFrame);
}
}
}
this.isVsyncFrame = false;
if (config.isDevEnv()) {
MatrixLog.d(TAG, "[dispatchEnd#run] inner cost:%sns", System.nanoTime() - traceBegin);
}
}
然后它是在FrameTracer来统一处理的
private void notifyListener(final String focusedActivity, final long startNs, final long endNs, final boolean isVsyncFrame,
final long intendedFrameTimeNs, final long inputCostNs, final long animationCostNs, final long traversalCostNs) {
long traceBegin = System.currentTimeMillis();
try {
// intendedFrameTimeNs是接收Vsync信号接收的时刻,endNs是实际的帧时刻
final long jitter = endNs - intendedFrameTimeNs;
// 核算丢帧
final int dropFrame = (int) (jitter / frameIntervalNs);
....
synchronized (listeners) {
for (final IDoFrameListener listener : listeners) {
if (config.isDevEnv()) {
listener.time = SystemClock.uptimeMillis();
}
if (null != listener.getExecutor()) {
if (listener.getIntervalFrameReplay() > 0) {
listener.collect(focusedActivity, startNs, endNs, dropFrame, isVsyncFrame,
intendedFrameTimeNs, inputCostNs, animationCostNs, traversalCostNs);
} else {
listener.getExecutor().execute(new Runnable() {
@Override
public void run() {
// 真正处理核算帧率的地方;
listener.doFrameAsync(focusedActivity, startNs, endNs, dropFrame, isVsyncFrame,
intendedFrameTimeNs, inputCostNs, animationCostNs, traversalCostNs);
}
});
}
} else {
listener.doFrameSync(focusedActivity, startNs, endNs, dropFrame, isVsyncFrame,
intendedFrameTimeNs, inputCostNs, animationCostNs, traversalCostNs);
}
if (config.isDevEnv()) {
listener.time = SystemClock.uptimeMillis() - listener.time;
MatrixLog.d(TAG, "[notifyListener] cost:%sms listener:%s", listener.time, listener);
}
}
}
} finally {
long cost = System.currentTimeMillis() - traceBegin;
if (config.isDebug() && cost > frameIntervalNs) {
MatrixLog.w(TAG, "[notifyListener] warm! maybe do heavy work in doFrameSync! size:%s cost:%sms", listeners.size(), cost);
}
}
}
咱们再来看下FrameDecoration类,它是怎样来核算FPS的
@Override
public void doFrameAsync(String focusedActivity, long startNs, long endNs, int dropFrame, boolean isVsyncFrame, long intendedFrameTimeNs, long inputCostNs, long animationCostNs, long traversalCostNs) {
super.doFrameAsync(focusedActivity, startNs, endNs, dropFrame, isVsyncFrame, intendedFrameTimeNs, inputCostNs, animationCostNs, traversalCostNs);
sumFrameCost += (dropFrame + 1) * frameIntervalMs;
sumFrames += 1;
float duration = sumFrameCost - lastCost[0];
....
long collectFrame = sumFrames - lastFrames[0];
if (duration >= 200) {
// 这便是fps核算的地方
final float fps = Math.min(maxFps, 1000.f * collectFrame / duration);
updateView(view, fps, belongColor,
dropLevel[FrameTracer.DropStatus.DROPPED_NORMAL.index],
dropLevel[FrameTracer.DropStatus.DROPPED_MIDDLE.index],
dropLevel[FrameTracer.DropStatus.DROPPED_HIGH.index],
dropLevel[FrameTracer.DropStatus.DROPPED_FROZEN.index],
sumDropLevel[FrameTracer.DropStatus.DROPPED_NORMAL.index],
sumDropLevel[FrameTracer.DropStatus.DROPPED_MIDDLE.index],
sumDropLevel[FrameTracer.DropStatus.DROPPED_HIGH.index],
sumDropLevel[FrameTracer.DropStatus.DROPPED_FROZEN.index]);
belongColor = bestColor;
lastCost[0] = sumFrameCost;
lastFrames[0] = sumFrames;
mainHandler.removeCallbacks(updateDefaultRunnable);
mainHandler.postDelayed(updateDefaultRunnable, 250);
}
}
这样咱们就知道了它是怎样核算FPS
的了
final float fps = Math.min(maxFps, 1000.f * collectFrame / duration);
// 和咱们上述自己的公式比照一下
fun calcFps() {
frameCostTimes = xxx //这个应该怎样核算才是核心
sumFrames += 1
if (duration > 200) {// 不需要每次都核算,隔200间隔再核算
val fps = (sumFrames - lastSumFrames) / (frameCostTime - lastFrameCostTime)
lastSumFrames = sumFrames
}
}
和咱们自己的公式比照下就知道了,咱们就能够知道了,它是在音讯循环监听每一次音讯的的耗时,经过这个来核算的累计帧和耗时,还有一个问题Looper
为啥能够监控到FPS
呢,它和vsync
也不要紧啊,咱们看下UIThreadMonitor.java
这个类就知道了,具体代码是:
private long getIntendedFrameTimeNs(long defaultValue) {
try {
return ReflectUtils.reflectObject(vsyncReceiver, "mTimestampNanos", defaultValue);
} catch (Exception e) {
e.printStackTrace();
MatrixLog.e(TAG, e.toString());
}
return defaultValue;
}
它是反射获取了Choreographer
中的mTimestampNanos
字段,它便是帧预期完毕的时刻,由于所有的绘制,动画,手势等都是Choreographer
也都是在主线程里监听恳求Vsync
然后再主线程里处理的,所以咱们能够反射这个值来作为帧完毕的时刻,这样就能够知道FPS
是怎样核算的了。
4.2 addOnFrameMetricsAvailableListener 方法
这个方法就更简略了,直接看官方API
试用下就能够了,只支持7.0以上的版别,示例代码:
val frameIntervalNanos = 1 / getWindowManager().getDefaultDisplay().getRefreshRate() * 1000000000
window.addOnFrameMetricsAvailableListener({ window, frameMetrics, dropCountSinceLastInvocation ->
val frameMetricsCopy = FrameMetrics(frameMetrics)
val vsyncTime = frameMetricsCopy.getMetric(FrameMetrics.VSYNC_TIMESTAMP)
val intentedVsyncTime = frameMetricsCopy.getMetric(FrameMetrics.INTENDED_VSYNC_TIMESTAMP)
val jitter = vsyncTime - intentedVsyncTime
val dropFrame: Int = (jitter / frameIntervalNanos).toInt()
// 不丢帧时正常帧也要算进去所以要+1
sumFrameCost += ((dropFrame + 1) * frameIntervalNanos / 1000000)
sumFrames += 1
val duration = sumFrameCost - lastDuration
val collectFrame = sumFrames - lastFrames
if (duration >= 200) {
val fps = 1000.0f * collectFrame / duration
Log.i("hellokai", ">>>>>>>fps->$fps")
lastFrames = sumFrame
lastDuration = sumFrameCost
}
}, Handler(Looper.getMainLooper()))
运用起来很简略,首要的核心核算方法也是上面的公式。
五、总结
这样咱们就知道如何自己来核算FPS
了,其实核心代码都是相同的,首要是要找到Vsync
所给的帧回调的时刻来做差值,才干核算出FPS
是多少。简略总结下,示例代码为:
import android.app.Activity
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.view.Choreographer
import android.view.FrameMetrics
import java.lang.reflect.Field
class FpsTrace {
private var start = 0L
var fps = 0f
private set
private var sumFrameCost = 0L
private var sumFrame = 0L
private var lastDuration = 0L
private var lastFrames = 0L
private var refreshRate: Float = 0f
private var frameIntervalNanos: Float = 0f
fun setUp(activity: Activity) {
refreshRate = activity.windowManager.defaultDisplay.refreshRate
frameIntervalNanos = 1 / refreshRate * 1000000000
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
activity.window.addOnFrameMetricsAvailableListener({ window, frameMetrics, dropCountSinceLastInvocation ->
val frameMetricsCopy = FrameMetrics(frameMetrics)
val vsyncTime = frameMetricsCopy.getMetric(FrameMetrics.VSYNC_TIMESTAMP)
val intentedVsyncTime = frameMetricsCopy.getMetric(FrameMetrics.INTENDED_VSYNC_TIMESTAMP)
val jitter = vsyncTime - intentedVsyncTime
val dropFrame = (jitter / frameIntervalNanos).toLong()
calcFps(dropFrame)
}, Handler(Looper.getMainLooper()))
} else {
Handler(Looper.getMainLooper()).apply {
looper.setMessageLogging {
if (it.toString().startsWith(">>>>>")) {
startMethod(it)
}
if (it.toString().startsWith("<<<<<")) {
endMethod(it)
}
}
}
}
}
private fun startMethod(it: String) {
start = System.nanoTime()
}
private fun endMethod(it: String) {
val intentStart = getIntendedFrameTimeNs(start)
val jitter = System.nanoTime() - intentStart
val dropFrame = (jitter / frameIntervalNanos).toLong()
calcFps(dropFrame)
}
private fun calcFps(dropFrame: Long) {
sumFrameCost += ((dropFrame + 1) * frameIntervalNanos / 1000000).toLong()
sumFrame += 1
val duration = sumFrameCost - lastDuration
val collectFrame = sumFrame - lastFrames
if (duration >= 200) {
fps = Math.min(refreshRate, 1000.0f * collectFrame / duration)
Log.i("hellokai", ">>>>>>>fps->$fps,")
lastFrames = sumFrame
lastDuration = sumFrameCost
}
}
private fun getIntendedFrameTimeNs(defaultValue: Long): Long {
val choreographer = Choreographer.getInstance()
val getDeclaredField = Class::class.java.getDeclaredMethod("getDeclaredField", String::class.java)
val field = getDeclaredField.invoke(choreographer.javaClass, "mDisplayEventReceiver") as Field
field.isAccessible = true
val vsyncReceiver = field.get(choreographer)
return ReflectUtils.reflectObject(vsyncReceiver, "mTimestampNanos", defaultValue)
}
}