在做 app 性能优化的时候,大家都希望能够写出丝滑的 UI 界面,以前写过一篇博客,主要是基于 Google 当时发布的性能优化典范,主要提供一些 UI 优化性能示例:
实际上,由于各种机型的配置不同、代码迭代历史悠久,代码中可能会存在很多在 UI 线程耗时的操作,所以我们希望有一套简单检测机制,帮助我们定位耗时发生的位置。
本篇博客主要描述如何检测应用在 UI 线程的卡顿,目前已经有两种比较典型方式来检测了:
两种方式都有一些开源项目,例如:
其实编写本篇文章,主要是因为发现一个还比较有意思的方案,该方法的灵感来源于一篇给我微信投稿的文章:
该项目主要用于捕获 UI 线程的 crash,当我看完该项目原理的时候,也可以用来作为检测卡段方案,可能还可以做一些别的事情。
所以,本文出现了 3 种检测 UI 卡顿的方案,3 种方案原理都比较简单,接下来将逐个介绍。
大家都知道在 Android UI 线程中有个 Looper,在其 loop 方法中会不断取出 Message,调用其绑定的 Handler 在 UI 线程进行执行。
大致代码如下:
- public static void loop() {
- final Looper me = myLooper();
- final MessageQueue queue = me.mQueue;
- // ...
- for (;;) {
- Message msg = queue.next(); // might block
- // 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);
- }
- // focus
- msg.target.dispatchMessage(msg);
- if (logging != null) {
- logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
- }
- // ...
- }
- msg.recycleUnchecked();
- }
- }
所以很多时候,我们只要有办法检测:
msg.target.dispatchMessage(msg);
此行代码的执行时间,就能够检测到部分 UI 线程是否有耗时操作了。可以看到在执行此代码前后,如果设置了 logging,会分别打印出
和
- >>>>> Dispatching to
这样的 log。
- <<<<< Finished to
我们可以通过计算两次 log 之间的时间差值,大致代码如下:
- public class BlockDetectByPrinter {
- 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 x) {
- if (x.startsWith(START)) {
- LogMonitor.getInstance().startMonitor();
- }
- if (x.startsWith(END)) {
- LogMonitor.getInstance().removeMonitor();
- }
- }
- });
- }
- }
假设我们的阈值是 1000ms,当我在匹配到
时,我会在 1000ms 毫秒后执行一个任务(打印出 UI 线程的堆栈信息,会在非 UI 线程中进行);正常情况下,肯定是低于 1000ms 执行完成的,所以当我匹配到
- >>>>> Dispatching
,会移除该任务。
- <<<<< Finished
大概代码如下:
- public class LogMonitor {
- private static LogMonitor sInstance = new LogMonitor();
- private HandlerThread mLogThread = new HandlerThread("log");
- private Handler mIoHandler;
- private static final long TIME_BLOCK = 1000L;
- private LogMonitor() {
- mLogThread.start();
- mIoHandler = new Handler(mLogThread.getLooper());
- }
- private static Runnable mLogRunnable = new Runnable() {
- @Override
- public void run() {
- StringBuilder sb = new StringBuilder();
- StackTraceElement[] stackTrace = Looper.getMainLooper().getThread().getStackTrace();
- for (StackTraceElement s : stackTrace) {
- sb.append(s.toString() + "\n");
- }
- Log.e("TAG", sb.toString());
- }
- };
- public static LogMonitor getInstance() {
- return sInstance;
- }
- public boolean isMonitor() {
- return mIoHandler.hasCallbacks(mLogRunnable);
- }
- public void startMonitor() {
- mIoHandler.postDelayed(mLogRunnable, TIME_BLOCK);
- }
- public void removeMonitor() {
- mIoHandler.removeCallbacks(mLogRunnable);
- }
- }
我们利用了 HandlerThread 这个类,同样利用了 Looper 机制,只不过在非 UI 线程中,如果执行耗时达到我们设置的阈值,则会执行
,打印出 UI 线程当前的堆栈信息;如果你阈值时间之内完成,则会 remove 掉该 runnable。
- mLogRunnable
用法很简单,在 Application 的 onCreate 中调用:
- BlockDetectByPrinter.start();
即可。
然后我们在 Activity 里面,点击一个按钮,让睡眠 2s,测试下:
- findViewById(R.id.id_btn02)
- .setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- try {
- Thread.sleep(2000);
- } catch (InterruptedException e) {
- }
- }
- });
运行点击时,会打印出 log:
- 02-21 00:26:26.408 2999-3014/com.zhy.testlp E/TAG:
- java.lang.VMThread.sleep(Native Method)
- java.lang.Thread.sleep(Thread.java:1013)
- java.lang.Thread.sleep(Thread.java:995)
- com.zhy.testlp.MainActivity$2.onClick(MainActivity.java:70)
- android.view.View.performClick(View.java:4438)
- android.view.View$PerformClick.run(View.java:18422)
- android.os.Handler.handleCallback(Handler.java:733)
- android.os.Handler.dispatchMessage(Handler.java:95)
会打印出耗时相关代码的信息,然后可以通过该 log 定位到耗时的地方。
Android 系统每隔 16ms 发出 VSYNC 信号,触发对 UI 进行渲染。SDK 中包含了一个相关类,以及相关回调。理论上来说两次回调的时间周期应该在 16ms,如果超过了 16ms 我们则认为发生了卡顿,我们主要就是利用两次回调间的时间周期来判断:
大致代码如下:
- public class BlockDetectByChoreographer {
- public static void start() {
- Choreographer.getInstance()
- .postFrameCallback(new Choreographer.FrameCallback() {
- @Override
- public void doFrame(long l) {
- if (LogMonitor.getInstance().isMonitor()) {
- LogMonitor.getInstance().removeMonitor();
- }
- LogMonitor.getInstance().startMonitor();
- Choreographer.getInstance().postFrameCallback(this);
- }
- });
- }
- }
第一次的时候开始检测,如果大于阈值则输出相关堆栈信息,否则则移除。
使用方式和上述一致。
先看一段代码:
- new Handler(Looper.getMainLooper())
- .post(new Runnable() {
- @Override
- public void run() {}
- }
该代码在 UI 线程中的 MessageQueue 中插入一个 Message,最终会在 loop() 方法中取出并执行。
假设,我在 run 方法中,拿到 MessageQueue,自己执行原本的
方法逻辑,那么后续的 UI 线程的 Message 就会将直接让我们处理,这样我们就可以做一些事情:
- Looper.loop()
- public class BlockDetectByLooper {
- private static final String FIELD_mQueue = "mQueue";
- private static final String METHOD_next = "next";
- public static void start() {
- new Handler(Looper.getMainLooper()).post(new Runnable() {
- @Override
- public void run() {
- try {
- Looper mainLooper = Looper.getMainLooper();
- final Looper me = mainLooper;
- final MessageQueue queue;
- Field fieldQueue = me.getClass().getDeclaredField(FIELD_mQueue);
- fieldQueue.setAccessible(true);
- queue = (MessageQueue) fieldQueue.get(me);
- Method methodNext = queue.getClass().getDeclaredMethod(METHOD_next);
- methodNext.setAccessible(true);
- Binder.clearCallingIdentity();
- for (; ; ) {
- Message msg = (Message) methodNext.invoke(queue);
- if (msg == null) {
- return;
- }
- LogMonitor.getInstance().startMonitor();
- msg.getTarget().dispatchMessage(msg);
- msg.recycle();
- LogMonitor.getInstance().removeMonitor();
- }
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
- });
- }
- }
其实很简单,将 Looper.loop 里面本身的代码直接 copy 来了这里。当这个消息被处理后,后续的消息都将会在这里进行处理。
中间有变量和方法需要反射来调用,不过不影响查看
执行时间,但是就不要在线上使用这种方式了。
- msg.getTarget().dispatchMessage(msg);
不过该方式和以上两个方案对比,并无优势,不过这个思路挺有意思的。
使用方式和上述一致。
最后,可以考虑将卡顿日志输出到文件,慢慢分析;可以结合上述原理以及自己需求开发做一个合适的方案,也可以参考已有开源方案。
我的微信公众号:hongyangAndroid
(可以给我留言你想学习的文章,支持投稿)
来源: http://www.tuicool.com/articles/ruEbMbA