UI卡顿检测的两种方法
前言:我们都知道android开发负责的就是移动端用户与界面的交互,是用户和后端的桥梁,一个美观,流畅的界面大大提高用户的操作体验。但在一些情况下,炫酷的界面布局,复杂的动画或者自定义控件的绘制会造成一定的UI卡顿,这与我们设计的原则是相悖的。那么,造成UI卡顿的原因无非也就那几种,重要的是怎么检测是哪个地方造成了界面卡顿。
造成卡顿有可能发生在XML文件中,也可能是我们代码中的逻辑太复杂造成的,那么我们分两种不同的方法来分别对这两种情况进行具体检测
1.首先,我们看看是否是我们的界面是否过度绘制
手机开发者选项打开,我们看看里面有个显示边界布局的选项,打开它
我们可以看到,我们的手机界面布满一层层的色层次
打开我们的app
可以了解到,我们的布局绘制了越多层,红色就越深,越不合理,
像下面布局就是楼主为了实现一个自定义的特殊布局,嵌套了太多层布局,导致出现UI卡顿,可以看到我们的界面
了解了这些,我们就可以看到我们的布局是否合理,定位哪里可能造成了UI卡顿
2.卡顿如果是发生在代码部分,那么怎么定位
首先我们来了解一下dispatchMessage这个方法
1. dispatchMessage方法在哪
dispatchMessage()是在Looper.loop()里调用,源码如下:
- public static void loop() {
- final Looper me = myLooper();
- if (me == null) {
- throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
- }
- final MessageQueue queue = me.mQueue;
- // Make sure the identity of this thread is that of the local process,
- // and keep track of what that identity token actually is.
- Binder.clearCallingIdentity();
- final long ident = Binder.clearCallingIdentity();
- for (;;) {
- Message msg = queue.next(); // might block
- if (msg == null) {
- // No message indicates that the message queue is quitting.
- return;
- }
- // 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);
- }
- msg.target.dispatchMessage(msg);
- if (logging != null) {
- logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
- }
- // Make sure that during the course of dispatching the
- // identity of the thread wasn't corrupted.
- final long newIdent = Binder.clearCallingIdentity();
- if (ident != newIdent) {
- Log.wtf(TAG, "Thread identity changed from 0x"
- + Long.toHexString(ident) + " to 0x"
- + Long.toHexString(newIdent) + " while dispatching to "
- + msg.target.getClass().getName() + " "
- + msg.callback + " what=" + msg.what);
- }
- msg.recycleUnchecked();
- }
- }
所以说,第27行的代码就是可能发生UI卡顿的地方。注意这行代码的前后,有两个logging。也就是说在执行第27代码的前后,如果设置了logging,会分别打印出“>>>>> Dispatching to”和“<<<<< Finished to”这样的Log。这样就给我们监视两次Log之间的时间差,来判断是否发生了卡顿。
2. 设置logging
主要看一下21行的mLogging是什么,源码如下所示:
- public final class Looper {
- private Printer mLogging;
- public void setMessageLogging(@Nullable Printer printer) {
- mLogging = printer;
- }
- }
- public interface Printer {
- void println(String x);
- }
Looper的mLogging是私有的,并且提供了setMessageLogging(@Nullable Printer printer)方法,所以我们可以自己实现一个Printer,在通过setMessageLogging()方法传入即可。
- public class AppContext extends Application {
- @Override
- public void onCreate() {
- super.onCreate();
- 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();
- }
- }
- });
- }
- }
当我们设置了mLogging之后,loop()方法中就会回调logging.println,并将带有“>>>>> Dispatching to”和“<<<<< Finished to”的字符串传入,我们就可以拿到这两条信息。
如果“>>>>> Dispatching to”信号发生了,我们就假定发生了卡顿(这里我们设定1秒钟的卡顿判定阈值),并且发送一个延迟1秒钟的任务,这个任务就用于在子线程打印出造成卡顿的UI线程里的堆栈信息。而如果没有卡顿,即在1秒钟之内我们检测到了“<<<<< Finished to”信号,就会移除这个延迟1秒的任务。
3. LogMonitor的实现
- public class LogMonitor {
- private static LogMonitor sInstance = new LogMonitor();
- private HandlerThread mHandlerThread = new HandlerThread("log");
- private Handler mHandler;
- private LogMonitor() {
- mHandlerThread.start();
- mHandler = new Handler(mHandlerThread.getLooper());
- }
- private static Runnable mRunnable = 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 void startMonitor() {
- mHandler.postDelayed(mRunnable, 1000);
- }
- public void removeMonitor() {
- mHandler.removeCallbacks(mRunnable);
- }
- }
这里我们使用HandlerThread来构造一个Handler,HandlerThread继承自Thread,实际上就一个Thread,只不过它比普通的Thread多了一个Looper,对外提供自己这个Looper对象的get方法,然后创建Handler时将HandlerThread中的looper对象传入。这样我们的mHandler对象就是与HandlerThread这个非UI线程绑定的了,这样它处理耗时操作将不会阻塞UI。
总之,如果UI线程阻塞超过1秒,就会在子线程中执行mRunnable,打印出UI线程当前的堆栈信息,如果处理消息没有超过1秒,则会实时的remove掉这个mRunnable。
4. 测试
在Activity中设置一个按钮,并且设置点击后睡3秒。便可以看见打印出的Log信息。帮助我们定位到耗时的地方。