最近公司接到一些用户反馈说,App卡顿了,甚至App无响应,我们称其为ANR。
先来说说ANR。
ANR
ANR,即 Application Not Responding,应用程序无响应。Android系统对于一些事件需要在一定的时间范围内完成,如果超过预定时间能未能得到有效响应或者响应时间过长,都会造成ANR。
出现ANR的场景有一下几种:
- 输入事件(包括按键分发事件)5秒内没有处理完毕。
- 执行BroadcastReceiver的onReceive()函数时10秒内没有处理完毕。
- Service的各个生命周期函数时20秒内没有处理完毕。
造成以上几种ANR场景的原因有2个:
- 主线程执行了耗时操作,比如数据库操作或网络编程。
- 其他进程(就是其他程序)占用CPU导致本进程得不到CPU时间片,比如其他进程的频繁读写操作可能会导致这个问题。
检测卡顿
App卡顿,肯定是我们在UI线程执行了什么耗时操作,所以我们要做的是将耗时的操作抓出来。很笨的办法就在UI线程执行的方法(怀疑有耗时操作)中加入打印开始和结束的时间,一般执行时间不会超过1秒。
我在网上找到一篇鸿洋大神关于检测应用中的UI卡顿的博文,发现一种很有趣的方法,在此记录一下。
其基本原理是利用Looper源码中打印Log信息,详细原理请看 Android UI性能优化 检测应用中的UI卡顿
具体代码如下:
public class LooperPrinter {
private static long lastTime;
public static void start() {
Looper.getMainLooper().setMessageLogging(new Printer() {
private static final String START = ">>>>> Dispatching";
private static final String END = "<<<<< Finished";
@Override
public void println(String content) {
if (content.startsWith(START)) {
LogMonitor.getInstance().startMonitor();
lastTime = System.currentTimeMillis();
}
if (content.startsWith(END)) {
LogMonitor.getInstance().removeMonitor();
Log.e("TAG", "run time : " + (System.currentTimeMillis() - lastTime));
}
}
});
}
}
Looper竟然还能这么用,涨知识了!!
public class LogMonitor {
private static LogMonitor INSTANCE = new LogMonitor();
private HandlerThread handlerThread = new HandlerThread("log");
private Handler handler;
private static final long TIME_BLOCK = 1000L;
private LogMonitor() {
handlerThread.start();
handler = new Handler(handlerThread.getLooper());
}
private static Runnable printRunnable = new Runnable() {
@Override
public void run() {
StringBuilder builder = new StringBuilder();
StackTraceElement[] stackTrace = Looper.getMainLooper().getThread().getStackTrace();
for (StackTraceElement element : stackTrace) {
builder.append(element.toString() + "\n");
}
Log.e("TAG", builder.toString());
}
};
public static LogMonitor getInstance() {
return INSTANCE;
}
public void startMonitor() {
handler.postDelayed(printRunnable, TIME_BLOCK);
}
public void removeMonitor() {
handler.removeCallbacks(printRunnable);
}
}
做一个倒计时的工具类,实例中是结合HandlerThread和Handler做的计时器,当打印“START”时,调用startMonitor方法,延时(1秒)执行打印堆栈信息,如果在规定时间内没有调用removeMonitor方法,证明没有在规定时间内打印“END”,也就是说在Main Looper中,当前Message执行时间超过规定时间,那么就会执行打印堆栈信息。打印的堆栈信息中会出现执行超过的方法。
解决公司的Bug
我使用以上方法检测了之后,很奇怪,竟然是Android的Resources.updateConfiguration方法导致的。简单说一下,由于公司之前有这么一个需求,要求我们的App字体不要受系统字体大小影响,然后在网上找了这么一个方法:
@Override
public Resources getResources() {
Resources res = super.getResources();
Configuration config = new Configuration();
config.setToDefaults();
res.updateConfiguration(config, res.getDisplayMetrics());
return res;
}
结果getResources方法用了1568毫秒,然后我把这段去掉之后,就没有这么多了(因为每个时间都没有超过1秒,所以我也不知道是哪个时间了)。我也有尝试去找代替方案,和老大讨论了一下,这个需求不大,就去掉了。希望知道的指点一下,谢谢!!