一:背景
众所周知,Android不允许在UI线程中做耗时的操作,否则有可能发生ANR的可能,默认情况下,在Android中Activity的最长执行时间是5秒,BroadcastReceiver的最长执行时间则是10秒。如果操作这个时长,则会产生ANR。而本文所要介绍的主要内容是检测UI线程中的耗时操作,从而能够定位一些老代码中的各种耗时的操作,作为性能优化的依据。
二:常见方案
- 通过UI 线程looper
- 通过Choreographer
2.1 通过UI 线程looper的打印日志
在github上也有许多开源库是基于该原理,比较有代表行的有
AndroidPerformanceMonitor
ANR-WatchDog
下面以AndroidPerformanceMonitor为例子来进行原理的介绍
AndroidPerformanceMonitor
通过如下代码可以看到,只存在一个主线程的Looper,所有通过主线程Handler发送的消息,都会发送到这里。
/**
* Initialize the current thread as a looper, marking it as an
* application's main looper. The main looper for your application
* is created by the Android environment, so you should never need
* to call this function yourself. See also: {@link #prepare()}
*/
public static void prepareMainLooper() {
prepare(false);
synchronized (Looper.class) {
if (sMainLooper != null) {
throw new IllegalStateException("The main Looper has already been prepared.");
}
sMainLooper = myLooper();
}
}
/**
* Returns the application's main looper, which lives in the main thread of the application.
*/
public static Looper getMainLooper() {
synchronized (Looper.class) {
return sMainLooper;
}
}
继续仔细观察Looper的源码可以发现,msg.target.dispatchMessage(msg); 这句用来进行消息的分发处理,在处理前后分别会打印日志,如果我们在消息处理之前计一个时,在消息处理之后计算一个值,如果这两者的阈值,大于了我们设定的门限,那么就可以认为卡顿。
public static void loop() {
final Looper me = myLooper();
...
for (;;) {
Message msg = queue.next(); // might block
if (msg == null) {
// No message indicates that the message queue is quitting.
return;
}
// 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);
}
...
}
}
但是MainLooper里面的默认打印的消息,并没有记录时间,这时,我们需要通过Looper.setMessageLogging来设置自定义的 Printer 。
public void setMessageLogging(@Nullable Printer printer) {
mLogging = printer;
}
在BlockCanary中 定义了自定义的Printer
class LooperMonitor implements Printer
其println()方法逻辑就是消息处理之后各记录时间值,计算这两者的阈值,判断是否block。
@Override
public void println(String x) {
if (mStopWhenDebugging && Debug.isDebuggerConnected()) {
return;
}
if (!mPrintingStarted) {
mStartTimestamp = System.currentTimeMillis();
mStartThreadTimestamp = SystemClock.currentThreadTimeMillis();
mPrintingStarted = true;
startDump();
} else {
final long endTime = System.currentTimeMillis();
mPrintingStarted = false;
if (isBlock(endTime)) {
notifyBlockEvent(endTime);
}
stopDump();
}
}
流程图

2.2 通过UI 线程looper循环发送消息
比较有代表性的作品
ANR-WatchDog
实现十分的简单,主要的类就是一个Thread, 通过使用主线程的Handler,不断的发送任务, 使得变量_tick的值不断的增加,然后再去做判断,_tick的值是否能得到,如果_tick的值没有变,则认为UI线程已经卡顿.
private final Runnable _ticker = new Runnable() {
@Override public void run() {
_tick = (_tick + 1) % Integer.MAX_VALUE;
}
};
@Override
public void run() {
setName("|ANR-WatchDog|");
int lastTick;
int lastIgnored = -1;
while (!isInterrupted()) {
lastTick = _tick;
_uiHandler.post(_ticker);
try {
//睡眠一段时间,确保_tick的值更新后再做判断。
Thread.sleep(_timeoutInterval);
}
catch (InterruptedException e) {
_interruptionListener.onInterrupted(e);
return ;
}
// If the main thread has not handled _ticker, it is blocked. ANR.
if (_tick == lastTick) {
if (!_ignoreDebugger && Debug.isDebuggerConnected()) {
if (_tick != lastIgnored)
Log.w("ANRWatchdog", "An ANR was detected but ignored because the debugger is connected (you can prevent this with setIgnoreDebugger(true))");
lastIgnored = _tick;
continue ;
}
ANRError error;
if (_namePrefix != null)
error = ANRError.New(_namePrefix, _logThreadsWithoutStackTrace);
else
error = ANRError.NewMainOnly();
_anrListener.onAppNotResponding(error);
return;
}
}
}
通过Choreographer
Takt
Android系统从4.1(API 16)开始加入Choreographer这个类来控制同步处理输入(Input)、动画(Animation)、绘制(Draw)三个UI操作。其实UI显示的时候每一帧要完成的事情只有这三种。

Choreographer接收显示系统的时间脉冲(垂直同步信号-VSync信号),在下一个frame渲染时控制执行这些操作。
Choreographer.getInstance().postFrameCallback(new FPSFrameCallback());
把你的回调添加到Choreographer之中,那么在下一个frame被渲染的时候就会回调你的callback.
通过判断两次回调doFrame执行的时间差,来判断是否发生ANR
以Takt为例。以下为其核心代码,主要代码都添加了注释。
@Override
public void doFrame(long frameTimeNanos) {
long currentTimeMillis = TimeUnit.NANOSECONDS.toMillis(frameTimeNanos);
if (frameStartTime > 0) {
// take the span in milliseconds
final long timeSpan = currentTimeMillis - frameStartTime;
//渲染次数+1
framesRendered++;
if (timeSpan > interval) {
//超过阈值,计算刷新频率
final double fps = framesRendered * 1000 / (double) timeSpan;
frameStartTime = currentTimeMillis;
framesRendered = 0;
for (Audience audience : listeners) {
//回调处理
audience.heartbeat(fps);
}
}
} else {
//第一次 frameStartTime=0;
frameStartTime = currentTimeMillis;
}
choreographer.postFrameCallback(this);
}
三:参考资料
BlockCanary — 轻松找出Android App界面卡顿元凶
Cockroach
Choreographer源码分析
Android应用ANR分析
网友评论