Android 检测应用中的UI卡顿

在做app性能优化的时候,大家都希望能够写出丝滑的UI界面,目前已经有两种比较典型方式来检测了:

  1. 利用UI线程Looper打印日志
  2. 利用Choreographer

一,利用loop()中打印的日志

(1)原理

大家都知道在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
            if (msg == null) {
                return;
            }

            final Printer logging = me.mLogging;
            if (logging != null) {
                logging.println(">>>>> Dispatching to " 
                    + msg.target + " " +msg.callback 
                    + ": " + msg.what);
            }

            final long traceTag = me.mTraceTag;
            if (traceTag != 0 && Trace.isTagEnabled(traceTag)) {
                Trace.traceBegin(traceTag,
                    msg.target.getTraceName(msg));
            }
            try {
                msg.target.dispatchMessage(msg);
            } finally {
                if (traceTag != 0) {
                    Trace.traceEnd(traceTag);
                }
            }
            if (logging != null) {
                logging.println("<<<<< Finished to " 
                + msg.target + " " + msg.callback);
            }
            //...
        }
    }

上面可知很多时候,我们只要有办法检测:

msg.target.dispatchMessage(msg);

此行代码的执行时间,就能够检测到部分UI线程是否有耗时操作了。可以看到在执行此代码前后,如果设置了logging,会分别打印出>>>>> Dispatching to和<<<<< Finished to这样的log。

我们可以通过计算两次log之间的时间差值,大致代码如下:

public class BlockDetectByLoopPrinter {

    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,当我在匹配到>>>>> Dispatching时,我会在1000ms毫秒后执行一个任务(打印出UI线程的堆栈信息,会在非UI线程中进行);正常情况下,肯定是低于1000ms执行完成的,所以当我匹配到<<<<< Finished,会移除该任务。

大概代码如下:

public class LogMonitor {
    private final static String TAG = LogMonitor.class.getSimpleName();
    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();
                .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() {
        Class handlerClass = null;
        try {
            handlerClass = Class.forName("android.os.Handler");
                .getDeclaredMethod("hasCallbacks", Runnable.class);
            method.setAccessible(true);
            return (boolean) method.invoke(mIoHandler,mLogRunnable);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
        return false;
    }

    public void startMonitor() {
        mIoHandler.postDelayed(mLogRunnable, TIME_BLOCK);
    }

    public void removeMonitor() {
        mIoHandler.removeCallbacks(mLogRunnable);
    }
}

我们利用了HandlerThread这个类,同样利用了Looper机制,只不过在非UI线程中,如果执行耗时达到我们设置的阈值,则会执行mLogRunnable,打印出UI线程当前的堆栈信息;如果你阈值时间之内完成,则会remove掉该runnable。

(2)测试

用法很简单,在Application的onCreate中调用:

  BlockDetectByLoopPrinter.start();

即可。

然后我们在Activity里面,点击一个按钮,让睡眠2s,测试下:

  findViewById(R.id.nextBtn).setOnClickListener(
          new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

运行点击时,会打印出log:

03-12 21:47:11.102 16933-17068/com.example.nickyang.demo1 E/LogMonitor:    
        java.lang.Thread.sleep(Native Method)
        java.lang.Thread.sleep(Thread.java:1031)
        java.lang.Thread.sleep(Thread.java:985)
        com.example.nickyang.demo1.MainActivity$2
                            .onClick(MainActivity.java:60)
        android.view.View.performClick(View.java:4848)
        android.view.View$PerformClick.run(View.java:20300)
        android.os.Handler.handleCallback(Handler.java:815)
        android.os.Handler.dispatchMessage(Handler.java:104)
        android.os.Looper.loop(Looper.java:194)
        android.app.ActivityThread.main(ActivityThread.java:5682)
        java.lang.reflect.Method.invoke(Native Method)
        java.lang.reflect.Method.invoke(Method.java:372)
        com.android.internal.os.ZygoteInit$MethodAndArgsCaller
                                    .run(ZygoteInit.java:963)
        com.android.internal.os.ZygoteInit.main(ZygoteInit.java:758)

会打印出耗时相关代码的信息,然后可以通过该log定位到耗时的地方。

二,利用Choreographer

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);
          }
       });
    }
}

第一次的时候开始检测,如果大于阈值则输出相关堆栈信息,否则则移除。使用方式和上面的一致。

三,利用Looper机制

先看一段代码:

    new Handler(Looper.getMainLooper())
    .post(new Runnable() {
        @Override
        public void run() {}
    }

该代码在UI线程中的MessageQueue中插入一个Message,最终会在loop()方法中取出并执行。

假设,我在run方法中,拿到MessageQueue,自己执行原本的Looper.loop()方法逻辑,那么后续的UI线程的Message就会将直接让我们处理,这样我们就可以做一些事情:

public class BlockDetectByLooper {
    private static final String FIELD_mQueue = "mQueue";
    private static final String METHOD_next = "next";
    private static final String METHOD_recycleUnchecked = "recycleUnchecked";

    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);
                        Binder.clearCallingIdentity();
                        Method recycleUnchecked = Message.class.
                        getDeclaredMethod(METHOD_recycleUnchecked);
                        recycleUnchecked.setAccessible(true);
                        recycleUnchecked.invoke(msg);
                        LogMonitor.getInstance().removeMonitor();
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }

            }
        });
    }
}

其实很简单,将Looper.loop里面本身的代码直接copy来了这里。当这个消息被处理后,后续的消息都将会在这里进行处理。

但是,这种方式中间有变量和方法需要反射来调用,不过不影响查看msg.getTarget().dispatchMessage(msg)执行时间,所以就不要在线上使用这种方式了。

更多精彩Android技术可以关注我们的微信公众号,扫一扫下方的二维码或搜索关注公众号:


Android老鸟
这里写图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值