Android 检测应用中的UI卡顿

    xiaoxiao2021-03-25  98

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

    利用UI线程Looper打印日志利用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老鸟

    转载请注明原文地址: https://ju.6miu.com/read-40666.html

    最新回复(0)