Android卡顿监控方案实践
[TOC]
如果您是直接查看此文,可能需要知晓BlockCanary的原理作为本次方案监控的前提哈~如果没有相关知识储备的话,您可以先参考Android卡顿方案调研
帧率卡顿
主要根据对卡顿调研的前提来作为帧率监控卡顿的依据,核心思想如下:
严重丢帧才更能带来体验上的“卡顿”,故UI流畅度的一个采集和衡量可以作为数据源观察,用以衡量优化之后的数据变化情况~
卡顿分布数据.png核心判断丢帧代码如下:
@Override
public void doFrame(String focusedActivity, long startNs, long endNs, boolean isVsyncFrame, long intendedFrameTimeNs, long inputCostNs, long animationCostNs, long traversalCostNs) {
super.doFrame(focusedActivity, startNs, endNs, isVsyncFrame, intendedFrameTimeNs, inputCostNs, animationCostNs, traversalCostNs);
//得到当前时间和此帧渲染时间差值来得到渲染该帧被耽误的时间
final long jiter = endNs - intendedFrameTimeNs;
//以被耽误的时间计算丢帧
final int dropFrame = (int) (jiter / frameIntervalNs);
droppedSum += dropFrame;
durationSum += Math.max(jiter, frameIntervalNs);
sumFrameCost += (dropFrame + 1) * frameIntervalMs;
sumFrames += 1;
float duration = sumFrameCost - lastCost[0];
long collectFrame = sumFrames - lastFrames[0];
final int curFps = (int) Math.min(InsectAppSnapshotMonitor.MAX_FRAME, 1000.f * collectFrame / duration);
if (curFps != mFPS) {
InsectLogger.d("瞬时帧率:%s, duration:%s, collectFrame:%s, dropFrame:%s", curFps, duration, collectFrame, dropFrame);
mFPS = curFps;
}
lastCost[0] = sumFrameCost;
lastFrames[0] = sumFrames;
cbOuter();
}
主线程慢函数
在卡顿调研中提到了主要的监控切入点是Handler
中去判断主线程执行一条消息耗费时间,区别点在于 卡顿线索 ==> 堆栈
的获取上面
思考
卡顿监控的核心初衷是让开发者能够快速的找到代码中需要优化的地方,所以作为开发人员本身,在下总结了下基础线索需要如下内容:
-
核心
- 精准抓取线索
-
线索信息
- 堆栈
- 行号
- 耗时
- 线程id(考虑到性能上的影响,作为
非必需字段
)
实现
类似BlockCanary采样dump主线程的方式
可以直接废弃了哈~因为性能上属实影响比较大,感兴趣的同学可以亲自测试一下dump线程所带来的成本,这里大概说下废弃的原因,主要是以下几点:
- stop the thread
- 线程停止,这里是主线程停止,影响不言而喻
- 虚拟机切换线程一次大概需要
500 ~ 2000个时钟周期
,一次指令运行大概在几个时钟周期的样子(主要取决于cpu的频率),这里假设1指令 = 5 * 时钟周期
,那么一次线程切换的损耗可以折算成运行100 ~ 400个指令
,反汇编看字节码的话,大概可以折算成25 ~ 100行代码
,这里也是性能优化的时候为什么提到要严格控制线程数量,启动优化甚至涉及到线程优先级的调参~
- 去重
- dump堆栈因为是周期采样方式,所以每次dump结果后需要做一个简单去重,字符串比对,(当然这里可以不考虑,完全可以开线程池去做这个事情)
- 查找
- 发生卡顿时,需要做一个查找(同理可以忽略不计)
- 循环队列维护
- 每次插入的都会做一个循环队列方式的维护
所以接下来就是借鉴matrix的思路,插桩来搞一搞堆栈的维护,这里才提一嘴目标:
核心
- 精准抓取线索
线索信息
- 堆栈
- 行号
- 耗时
- 线程id(考虑到性能上的影响,作为
非必需字段
)
然后转化成技术点:
- Transform + ASM
- 抓取类设计(上层Java)
就这么两点,思路就是这么清奇、任性~
抓取类设计
抓取类主要作用就是在插桩的时候调用抓取类的方法进行无脑插桩
即可~
- 思路
- 维护方法进入/退出的链表
- 自更新时间戳(耗时)
- 补充线索数据
因为无脑插桩
的前提哈,所以不能无脑调用System.currentTimeMillis()
,也是对性能上的一个考虑,毕竟是时间,不过因为本身debug的原因,就算执行其实也无伤大雅,不过如matrix的思路一样
优化瓶颈考虑,先易后难,既保证运行性能,又能命中问题,何乐而不为?
-
时间戳设计
时间更新只需要在5ms的时候更新一次即可,每次方法进入和退出的时候,补充的时间戳拿这个维护的时间戳即可~ (这里的流程是运行在子线程中哈~ 维护一个静态volatile时间戳字段即可)
故
时间维护线程.png睡他5ms
再说
代码只贴出维护时间的地方,其实就一个handlerThread,其余一无是处~
@JvmStatic
private val sUpdateTimerTask = Runnable {
try {
while (true) {
while (!isPauseUpdateTime) {
sCurrentDiffTime = SystemClock.uptimeMillis() - sDiffTime
SystemClock.sleep(UPDATE_CYCLE_MS)
}
synchronized(sUpTimeLock) {
sUpTimeLock.wait()
}
}
} catch (e: Exception) {
InsectLogger.e("计时器线程异常退出$e")
}
}
-
抓取设计
抓取类其实就是一个静态工具类而已,提供出插桩和发生卡顿时获取线索的地方即可,没有什么流程可言,无需像matrix那样设计的复杂,需要标记起始的方法,然后根据起始dump线索,写起来会比较复杂一些,同时这里也是本文方案和matrix的差异之一,matrix因为是循环大队列的原因,所以对内存颇有影响,具大佬们自己统计,影响在7MB的亚子,感兴趣的同学可以参考带佬们的设计AppMethodBeat.java
本文的设计就会比较简单了,核心只需要提供三个方法即可
MethodBeat设计.png方法进入代码设计如下:
退出的代码和下面代码如出一辙,这里就不贴出来了,重在思路
/** * 方法进入 */ @JvmStatic fun i(method: String) { if (isPauseUpdateTime) { return } var threadId: Long = -1 if (mNeedThreadId) { threadId = Thread.currentThread().id } if (mFilterThread) { //过滤主线程以外的堆栈进来 if (!isMainThread(if (threadId == -1L) Thread.currentThread().id else threadId)) { return } } if (mCurReportMethodIn == method && sMethods.isNotEmpty() && sMethods.last.isEnter) { sMethods.last.count += 1 } else { appendMethod(InsectAppMethod().apply { this.method = method this.timeStep = sCurrentDiffTime if (mNeedThreadId) { this.threadId = threadId } }, true) mCurReportMethodIn = method } }
插桩插件
说到插桩插件就不得不提一嘴之前做函数耗时插件,本插件也是在函数耗时插件上的基础上实现
-
思路
-
插件不宜更新频繁,所以尽量支持成可通过外部配置来设置插桩的代码
-
包/类/方法 白名单设计(譬如插桩类本身就不能进行插桩)
-
扩展:编译优化
-
- coding
思路非常简单,再结合原来封装好的时机,这里就把核心方法贴一下就行,值的注意的是行号也是在这里补充上的哈~
温馨提示:插件插桩方式有一点小小的bug,方法进入和退出时候插桩会混在一起插
如:需要插入两对儿代码 A1,A2和B1,B2
那么效果:
A1,B1,X,X,A2,B2
期望结果:
A1,B1,X,X,B2,A2
这里只需要把插件在方法退出的或者进入的时候逆序遍历即可哈~这里因为对功能影响不大,所以就不做调整了
override fun visitLineNumber(adviceAdapter: AdviceAdapter?, methodVisitor: MethodVisitor?, line: Int, start: Label?) {
mLineNumber = line
}
override fun onMethodEnter(adviceAdapter: AdviceAdapter?, methodVisitor: MethodVisitor?) {
if (isABS()) {
return
}
//白名单判断
mFilter = filterPkg(ownerClassName) || filterClass(ownerClassName) || filterMethod("$ownerClassName.$name")
if (mFilter) {
PLogger.log("过滤掉$ownerClassName.$name")
return
}
methodVisitor?.let { visitor ->
insertMethod(true, "$ownerClassName.$name", visitor)
}
}
override fun onMethodExit(adviceAdapter: AdviceAdapter?, methodVisitor: MethodVisitor?, opcode: Int) {
if (isABS()) {
return
}
//白名单判断
if (mFilter) {
return
}
methodVisitor?.let { visitor ->
insertMethod(false, "$ownerClassName.$name", visitor)
}
}
验证效果
废话不多,直接上效果:
结果验证.png堆栈格式:
调用层级, 调用方法, 方法调用次数, 方法耗时(方法耗时计算只有在堆栈有"()"括号字样的才能计算,毕竟需要方法进入和方法退出)~
如上可以看到最为耗时的方法为mockBlock方法,他调用了testBock2方法,这个方法就耗时了720ms,可以优先查看这个方法的运行情况:
private void testBlock2() {
int count = 0;
while (count < Integer.MAX_VALUE / 8) {
count++;
}
}
可见一波无脑递加~你不卡谁卡
综上一个自定义堆栈维护就搞定了,因为源码中含公司的敏感内容和服务器,就暂时先不开源了,抓取工具类可以原封不动的贴出来供大家参考哈~可以直接贴到项目中运行
class InsectMethodBeat {
companion object {
/**
* sMethods存储的最大的方法数量,先暂定为2000条方法(1000个方法进入/退出)
*/
@JvmStatic
val MAX_STACK_SIZE = 1000 * 2
@JvmStatic
private val sMethodListeners = LinkedHashSet<IInsectMethodListener>()
@JvmStatic
private val sTimeThread = HandlerThread("INSECT_MONITOR-Method").run {
start()
this
}
@JvmStatic
private val sTimerHandler = Handler(sTimeThread.looper)
@JvmStatic
private val sMethods = LinkedList<InsectAppMethod>()
@Volatile
@JvmStatic
private var sCurrentDiffTime = SystemClock.uptimeMillis()
@Volatile
@JvmStatic
private var sDiffTime = sCurrentDiffTime
@Volatile
@JvmStatic
private var isPauseUpdateTime = false
@JvmStatic
private val UPDATE_CYCLE_MS = 5L
/**
* 更新时间的锁对象
*/
@JvmStatic
private val sUpTimeLock = Object()
@JvmStatic
private var mCurReportMethodIn = ""
@JvmStatic
private var mCurReportMethodOut = ""
/**
* 获取主线程id
*/
@JvmStatic
private val mMainThreadId = Looper.getMainLooper().thread.id
/**
* 是否过滤主线程堆栈, 默认过滤主线程堆栈
*/
@JvmStatic
private var mFilterThread = true
/**
* 是否需要线程id
*/
@JvmStatic
private var mNeedThreadId = true
/**
* 供卡顿检测时候用,在主线程分发结束时候调用
*/
@JvmStatic
fun onDispatchEnd() {
isPauseUpdateTime = true
sMethods.clear()
}
/**
* 供卡顿检测时候用,在主线程分发开始时候调用
*/
@JvmStatic
fun onDispatchStart() {
sCurrentDiffTime = SystemClock.uptimeMillis() - sDiffTime
isPauseUpdateTime = false
synchronized(sUpTimeLock) {
sUpTimeLock.notify()
}
}
@JvmStatic
private val sUpdateTimerTask = Runnable {
try {
while (true) {
while (!isPauseUpdateTime) {
sCurrentDiffTime = SystemClock.uptimeMillis() - sDiffTime
SystemClock.sleep(UPDATE_CYCLE_MS)
}
synchronized(sUpTimeLock) {
sUpTimeLock.wait()
}
}
} catch (e: Exception) {
InsectLogger.e("计时器线程异常退出$e")
}
}
init {
enableTimer()
}
@JvmStatic
fun filterMainThread(filter: Boolean) {
mFilterThread = filter
}
@JvmStatic
private fun enableTimer() {
//启动定时器
sCurrentDiffTime = SystemClock.uptimeMillis() - sDiffTime
sTimerHandler.removeCallbacksAndMessages(null)
sTimerHandler.postDelayed(sUpdateTimerTask, UPDATE_CYCLE_MS)
}
@JvmStatic
private fun disableTimer() {
sTimerHandler.removeCallbacksAndMessages(null)
}
/**
* 方法进入
*/
@JvmStatic
fun i(method: String) {
if (isPauseUpdateTime) {
return
}
var threadId: Long = -1
if (mNeedThreadId) {
threadId = Thread.currentThread().id
}
if (mFilterThread) {
//过滤主线程以外的堆栈进来
if (!isMainThread(if (threadId == -1L) Thread.currentThread().id else threadId)) {
return
}
}
if (mCurReportMethodIn == method && sMethods.isNotEmpty() && sMethods.last.isEnter) {
sMethods.last.count += 1
} else {
appendMethod(InsectAppMethod().apply {
this.method = method
this.timeStep = sCurrentDiffTime
if (mNeedThreadId) {
this.threadId = threadId
}
}, true)
mCurReportMethodIn = method
}
}
/**
* 方法退出
*/
@JvmStatic
fun o(method: String) {
if (isPauseUpdateTime) {
return
}
var threadId: Long = -1
if (mNeedThreadId) {
threadId = Thread.currentThread().id
}
if (mFilterThread) {
//过滤主线程以外的堆栈进来
if (!isMainThread(if (threadId == -1L) Thread.currentThread().id else threadId)) {
return
}
}
if (mCurReportMethodOut == method && sMethods.isNotEmpty() && !sMethods.last.isEnter) {
sMethods.last.count += 1
} else {
appendMethod(InsectAppMethod().apply {
this.method = method
this.timeStep = sCurrentDiffTime
this.isEnter = false
if (mNeedThreadId) {
this.threadId = threadId
}
}, true)
mCurReportMethodOut = method
}
}
@JvmStatic
private fun isMainThread(id: Long): Boolean = (id == mMainThreadId)
/**
* 外部发生卡顿时获取记录的堆栈内容
*/
@JvmStatic
fun dumpStack(): LinkedList<InsectAppMethod> {
synchronized(sMethods) {
return LinkedList(sMethods)
}
}
@JvmStatic
private fun appendMethod(method: InsectAppMethod, autoClear: Boolean) {
synchronized(sMethods) {
if (autoClear) {
if (sMethods.size >= MAX_STACK_SIZE) {
InsectLogger.d("InsectMethodBeat 清除内部堆栈")
sMethods.clear()
}
}
sMethods.add(method)
}
}
/**
* 预留给外部使用,目前没啥卵用,当然如果您想监听方法的进入和退出可以使用
*/
@JvmStatic
fun addListener(listener: IInsectMethodListener) {
synchronized(sMethodListeners) {
sMethodListeners.add(listener)
}
}
/**
* 预留给外部使用,目前没啥卵用,当然如果您想监听方法的进入和退出可以使用
*/
@JvmStatic
fun removeListener(listener: IInsectMethodListener) {
synchronized(sMethodListeners) {
sMethodListeners.remove(listener)
}
}
/**
* 预留给外部使用,目前没啥卵用,当然如果您想监听方法的进入和退出可以使用
*/
@JvmStatic
fun clearListeners() {
synchronized(sMethodListeners) {
sMethodListeners.clear()
}
}
/**
* 预留给外部使用,目前没啥卵用,当然如果您想监听方法的进入和退出可以使用
*/
@JvmStatic
private fun cb(method: String, time: Long, isIn: Boolean = true) {
if (sMethodListeners.size <= 0) {
return
}
for (listener in sMethodListeners) {
if (isIn) {
listener.onMethodIn(method, time)
} else {
listener.onMethodOut(method, time)
}
}
}
/**
* 打印自定义堆栈
*/
@JvmStatic
fun printReport() {
printReport(sMethods)
}
@JvmStatic
fun printReport(report: List<InsectAppMethod>) {
val sb = StringBuilder("printblockstack:\n${report.size}")
for (method in report) {
sb.append(method.toString() + "\n")
}
sb.append("printblockstack end!!!")
DefaultMonitorFileWriter.getInstance().writeMessage(sb.toString())
InsectLogger.d("InsectMethodBeat", sb.toString())
}
}
}
网友评论