利用Choreographer
两种方式都有一些开源项目,例如: 其实编写本篇文章,主要是因为发现一个还比较有意思的方案,该方法的灵感来源于一篇给我微信投稿的文章: 该项目主要用于捕获UI线程的crash,当我看完该项目原理的时候,也可以用来作为检测卡段方案,可能还可以做一些别的事情。 所以,本文出现了3种检测UI卡顿的方案,3种方案原理都比较简单,接下来将逐个介绍。 二、利用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 // 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 和<<<<< Finished to 这样的log。 我们可以通过计算两次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,当我在匹配到>>>>> Dispatching 时,我会在1000ms毫秒后执行一个任务(打印出UI线程的堆栈信息,会在非UI线程中进行);正常情况下,肯定是低于1000ms执行完成的,所以当我匹配到<<<<< 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线程中,如果执行耗时达到我们设置的阈值,则会执行mLogRunnable ,打印出UI线程当前的堆栈信息;如果你阈值时间之内完成,则会remove掉该runnable。 (2)测试用法很简单,在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定位到耗时的地方。 三、 利用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); } }); } }
|