怎样检测函数执行是否卡顿 (字节跳动)
这道题想考察什么?
对Android程序执行的理解
考察的知识点
TraceView/Android Studio Profiler的应用,Handler机制,UI刷新Choreographer等
考生如何回答
卡顿意味着我们的App发生了掉帧,被使用者所感知。 而导致App卡顿的原因很多:UI绘制慢、内存使用不当(内存抖动)等等情况都会导致程序出现卡顿,而这些卡顿又分为:可重现与不可重现。
可重现的卡顿
有一部分的卡顿是可本地复现的,对于这种容易重现的场景,一般我们在开发及体验测试阶段容易注意得到,而定位卡顿的根源,我们常用的方法是通过 TraceView、Systrace 工具来抓取卡顿过程中函数的执行情况(堆栈,耗时,调用次数等)。 通过 TraceView 的可视化界面,我们可以具体知道某个过程中的调用栈信息及各个函数的执行次数与耗时,能比较直观的找到严重耗时的函数,帮助我们快速解决卡顿问题。
目前Traceview 已弃用。如果使用 Android Studio 3.2 或更高版本,则应改为使用 CPU Profiler
不可重现的卡顿
但往往大部分卡顿是很难及时发现的,不可重现的卡顿,经常出现在线上用户的真实使用过程中,这种卡顿往往跟机器性能,手机环境,甚至是操作偏好等因素息息相关。一般也是从用户反馈中得到,通常表述为“你们APP好卡”,我们很难在这种描述中,直接洞察到卡顿的根源,甚至有些连卡顿的场景都不知道,很难准确重现,所以这种卡顿容易让人摸不着头脑。 当然作为开发者,我更希望用户反馈的是,“某某函数耗时666ms,请解决它。” 那么面对这种卡顿,我们怎么办呢?
解决方案
造成卡顿的直接原因通常是,主线程执行繁重的UI绘制、大量的计算或IO等耗时操作。
业界有几种常见解决方案,都可以从一定程度上,帮助开发者快速定位到卡顿的堆栈,如 BlockCanary、ArgusAPM、LogMonitor 。这些方案的主要思想是,监控主线程执行耗时,当超过阈值时,dump出当前主线程的执行堆栈,通过堆栈分析找到卡顿原因。
从监控主线程的实现原理上,主要分为两种:
1、依赖主线程 Looper,监控每次 dispatchMessage 的执行耗时。(BlockCanary)
2、依赖 Choreographer 模块,监控相邻两次 Vsync 事件通知的时间差。(ArgusAPM、LogMonitor)
第一种方案,看下 Looper#loop 代码片段:
public static void loop() {
...
for (;;) {
...
// This must be in a local variable, in case a UI event sets the logger
Printer logging = me.mLogging;
if (logging != null) {
logging.println(">>>>> Dispatching to " + msg.target + " " +
msg.callback + ": " + msg.what);
}
msg.target.dispatchMessage(msg);
if (logging != null) {
logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
}
...
}
}
主线程所有执行的任务都在 dispatchMessage
方法中派发执行完成,我们通过 setMessageLogging
的方式给主线程的 Looper 设置一个 Printer ,因为 dispatchMessage
执行前后都会打印对应信息,在执行前利用另外一条线程,通过 Thread#getStackTrace
接口,以轮询的方式获取主线程执行堆栈信息并记录起来,同时统计每次 dispatchMessage
方法执行耗时,当超出阈值时,将该次获取的堆栈进行分析上报,从而来捕捉卡顿信息,否则丢弃此次记录的堆栈信息。
第二种方案,利用系统 Choreographer 模块,向该模块注册一个 FrameCallback 监听对象,同时通过另外一条线程循环记录主线程堆栈信息,并在每次 Vsync 事件 doFrame 通知回来时,循环注册该监听对象,间接统计两次 Vsync 事件的时间间隔,当超出阈值时,取出记录的堆栈进行分析上报。
简单代码实现如下:
Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() {
@Override
public void doFrame(long frameTimeNanos) {
if(frameTimeNanos - mLastFrameNanos > 100) {
...
}
mLastFrameNanos = frameTimeNanos;
Choreographer.getInstance().postFrameCallback(this);
}
});
这两种方案,可以较方便的捕捉到卡顿的堆栈,但其最大的不足在于,无法获取到各个函数的执行耗时,对于稍微复杂一点的堆栈,很难找出可能耗时的函数,也就很难找到卡顿的原因。另外,通过其他线程循环获取主线程的堆栈,如果稍微处理不及时,很容易导致获取的堆栈有所偏移,不够准确,加上没有耗时信息,卡顿也就不好定位。
所以怎么更准确地捕捉卡顿堆栈,又能计算出各个函数执行耗时的方案。 而要计算函数的执行耗时,最关键的点在于如何对执行过程中的函数进行打点监控。
1、字节码插桩,修改字节码,在编译期修改所有 class 文件中的函数字节码,对所有函数前后进行打点插桩。
2、使用JVMTI监听函数进入与退出,JVMTI全称是Java Virtual Machine Tool Interface,Java虚拟机工具接口,在Android 8.0及以上可用,可以让我们监控与控制虚拟机的某种行为。
还有一种方式:https://zhuanli.tianyancha.com/502d748aa97a4733a33ff77215cf9b68
在应用启动时,默认打开 Trace 功能(Debug.startMethodTracing),应用内所有函数在执行前后将会经过alvik 上 dvmMethodTraceAdd 函数 或 art 上 Trace::LogMethodTraceEvent 函数, 通过hack手段代理该函数,在每个执行方法前后进行打点记录。
最后
我整理了一套Android面试题合集,除了以上面试题,还包含【Java 基础、集合、多线程、虚拟机、反射、泛型、并发编程、Android四大组件、异步任务和消息机制、UI绘制、性能调优、SDN、第三方框架、设计模式、Kotlin、计算机网络、系统启动流程、Dart、Flutter、算法和数据结构、NDK、H.264、H.265.音频编解码、FFmpeg、OpenMax、OpenCV、OpenGL ES】