Android UI performance optimization detects UI freezes in applications

This article has been published on my public account hongyangAndroid.
Please indicate the source for reprinting:
http://blog.csdn.net/lmj623565791/article/details/58626355
This article is from Zhang Hongyang's blog

I. Overview

When doing app performance optimization, everyone hopes to write a silky UI interface. I wrote a blog before, mainly based on the performance optimization model released by Google at that time, mainly to provide some UI optimization performance examples:

In fact, due to the different configurations of various models and the long history of code iteration, there may be many time-consuming operations on the UI thread in the code, so we hope to have a simple detection mechanism to help us locate the time-consuming location.

This blog mainly describes how to detect the stuck application in the UI thread. At present, there are two typical ways to detect:

  1. Logs printed by UI thread Looper
  2. 利用Choreographer

There are some open source projects for both ways, such as:

In fact, I wrote this article mainly because I found a more interesting solution. The inspiration for this method came from an article submitted to me on WeChat:

This project is mainly used to capture the crash of the UI thread. When I read the principle of the project, it can also be used as a detection card segment scheme, and may also be able to do some other things.

Therefore, there are 3 schemes for detecting UI stuck in this article. The principles of the 3 schemes are relatively simple, and they will be introduced one by one.

2. Use the log printed in loop()

(1) Principle

Everyone knows that there is a Looper in the Android UI thread. In its loop method, the Message will be continuously taken out, and the bound Handler will be called to execute it in the UI thread.

The approximate code is as follows:

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

So many times, we only need to have a way to detect:

msg.target.dispatchMessage(msg);

The execution time of this line of code can detect whether some UI threads have time-consuming operations. It can be seen that before and after executing this code, if logging is set, it will print out >>>>> Dispatching toand <<<<< Finished tosuch logs respectively.

We can calculate the time difference between two logs, roughly as follows:

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

    }
}

Assuming that our threshold is 1000ms, when I match >>>>> Dispatchingit, I will execute a task after 1000ms (print out the stack information of the UI thread, which will be done in the non-UI thread); under normal circumstances, it must be lower than 1000ms The execution is complete, so when I match it <<<<< Finished, the task will be removed.

The approximate code is as follows:

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

}

We use the HandlerThread class and also use the Looper mechanism, but in non-UI threads, if the execution time reaches the threshold we set, it will be executed mLogRunnableand the current stack information of the UI thread will be printed; if you are within the threshold time When completed, the runnable will be removed.

(2) Test

The usage is very simple, it is called in the onCreate of Application:

BlockDetectByPrinter.start();

That's it.

Then we click a button in the Activity, let sleep for 2s, and test:

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

When running the click, the log will be printed:

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)

It will print out the information about the time-consuming code, and then you can locate the time-consuming place through the log.

3. Use Choreographer

The Android system sends a VSYNC signal every 16ms to trigger the rendering of the UI. The SDK contains a related class and related callbacks. Theoretically speaking, the time period of the two callbacks should be 16ms. If it exceeds 16ms, we think that a freeze has occurred. We mainly use the time period between the two callbacks to judge:

The approximate code is as follows:

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

The detection starts at the first time, and if it is greater than the threshold, the relevant stack information is output, otherwise it is removed.

The usage is the same as above.

Fourth, use the Looper mechanism

First look at a piece of code:

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

This code inserts a Message into the MessageQueue in the UI thread, which will eventually be retrieved and executed in the loop() method.

Suppose, in the run method, I get the MessageQueue and execute the original Looper.loop()method logic myself, then the Message of the subsequent UI thread will be directly processed by us, so that we can do something:

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

            }
        });
    }
}

In fact, it is very simple, copy the code in Looper.loop directly here. When this message is processed, subsequent messages will be processed here.

There are variables and methods in the middle that need to be called by reflection, but it does not affect the viewing msg.getTarget().dispatchMessage(msg);execution time, but don't use this method online.

However, compared with the above two schemes, this method has no advantages, but this idea is quite interesting.

The usage is the same as above.

Finally, you can consider outputting the stuck log to a file and analyze it slowly; you can develop a suitable solution based on the above principles and your own needs, or you can refer to existing open source solutions.

refer to

My WeChat public account: hongyangAndroid
(you can leave me a message for the articles you want to learn, support submission)

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=325765350&siteId=291194637