Example 1.
When introducing the online ANR fetching tool, we found many problems caused by IdleHandler. The stack is shown below
If you think about why idleHandler causes this problem, you might think it’s because a single message takes too long to execute. Then take a look at the example. The code of the project itself is relatively complex, and the simplified code is as follows:
- Utility class
/** * Add task to IdleHandler ** @param runnable runnable public static void run(runnable runnable) {IUiRunnable uiRunnable = new IUiRunnable(runnable); Looper.getMainLooper().getQueue().addIdleHandler(uiRunnable); }Copy the code
- Using utility classes
public class MainActivity extends AppCompatActivity { public static final String TAG = "idleHandler"; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); } @Override protected void onResume() { super.onResume(); Log.e(TAG, "onResume: start"); Delay MSG new Handler().postdelayed (new Runnable() {@override public void run() {log.e (TAG, TAG, delay MSG new Handler().postdelayed (new Runnable() {@override public void run() {log.e (TAG, TAG, TAG) "delay: msg"); startService(new Intent(MainActivity.this, MyService.class)); }}, 3000); Uimanager.run (() -> test(1)); UIManager.run(() -> test(2)); UIManager.run(() -> test(3)); } private void test(final int I) {try {log. e(TAG, "queueIdle:test start "+ I); private void test(final int I) {try {log. e(TAG, "queueIdle:test start" + I); Thread.sleep(3000); Log.e(TAG, "queueIdle:test end " + i); } catch (InterruptedException e) { e.printStackTrace(); }}}Copy the code
Let’s guess the output order of the following code. If you look back at the code, you’d think it would look like this, right?
If you think the output above is ok, read on
Here I cut out the real log:
what? ? Why did delay3000MS fail? Instead, Idle messages are executed first.
Why is that? Do not panic encountered this problem we must pass the IdleHandler mechanism source code, to find the answer.
2. Source code analysis
//MessageQueue.java Message next() { // Return here if the message loop has already quit and been disposed. // This can happen if the application tries to restart a looper after quit // which is not supported. final long ptr = mPtr; if (ptr == 0) { return null; } int pendingIdleHandlerCount = -1; // -1 only during first iteration int nextPollTimeoutMillis = 0; for (;;) { if (nextPollTimeoutMillis ! = 0) { Binder.flushPendingCommands(); } nativePollOnce(ptr, nextPollTimeoutMillis); . // If first time idle, then get the number of idlers to run. // Idle handles only run if the queue is empty or if the first message // in the Queue (possibly a barrier) is due to be handled in the future. //1 execution time if (pendingIdleHandlerCount < 0 && (mMessages == null || now < mMessages.when)) { pendingIdleHandlerCount = mIdleHandlers.size(); } if (pendingIdleHandlerCount <= 0) { // No idle handlers to run. Loop and wait some more. mBlocked = true; continue; } if (mPendingIdleHandlers == null) { mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)]; } //2 copy handlers = mIdleHandlers. ToArray (handlers); } // Run the idle handlers. // We only ever reach this code block during the first iteration. i < pendingIdleHandlerCount; i++) { final IdleHandler idler = mPendingIdleHandlers[i]; mPendingIdleHandlers[i] = null; // release the reference to the handler boolean keep = false; try { keep = idler.queueIdle(); } catch (Throwable t) { Log.wtf(TAG, "IdleHandler threw exception", t); } //4 Whether to remove the current idleHandler if (! keep) { synchronized (this) { mIdleHandlers.remove(idler); } } } // Reset the idle handler count to 0 so we do not run them again. pendingIdleHandlerCount = 0; // While calling an idle handler, a new message could have been delivered // so go back and look again for a pending message without waiting. nextPollTimeoutMillis = 0; Public void addIdleHandler(@nonNULL idleHandler handler) {if (handler == null) {throw new NullPointerException("Can't add a null IdleHandler"); } synchronized (this) { mIdleHandlers.add(handler); }}Copy the code
Given that the next() method is long and covered extensively, I won’t go into details here.
-
At comment 1, this is IdleHandler execution time, that is, the main thread has no message or execution time (idle time).
-
MPendingIdleHandlers copy the contents of the mIdleHandlers, and the tasks we add by calling addIdleHandler() are stored in the mIdleHandlers.
-
If addIdleHandler() is called many times in a short period of time, it means that these idle MSG will be taken out and executed one by one. If idle MSG is very time-consuming, the postDelay task execution time of the previous main thread will be very unreliable. Similarly, if touch events occur during the period, it is highly likely that ANR will occur due to untimely processing. Check to see if there are any such problems in your projects.
-
Note 4, which controls whether the IdleHandler is called again or once, is determined by the return value of the queueIdle() method, which we should take advantage of.
The above tool method is well intended. It provides interfaces to each business side to delay loading non-urgent tasks at a certain time of need, thus reducing the lag. However, it is not so smooth when used, and even leads to ANR.
3. Safe usage
IdleHandler to introduce
- Here’s a bit more about IdleHandler. We all know that when doing startup performance optimizations, you want to minimize the number of startup main tasks. For some tasks that are not necessary and must be completed in the main thread during startup, we can load them after the application is started. It is with this in mind that Google officially provides IdleHandler to tell us when threads are idle.
Use IdleHandler to design the utility class
Using IdleHandler directly is definitely not in line with the blogger’s coding style of this “big project”. Simple encapsulation is necessary to facilitate us to do some function customization. Here is the tool class in the project.
- The correct way to start the process of non-urgent main thread tasks into uiTasks, and then one by one to perform, remember that a single message should not take too long.
private static List<Runnable> uiTasks = new ArrayList<>(); public static UIPoolManager addTask(Runnable runnable) { tasks.add(runnable); return this; } public static UIManager runUiTasks() { NullHelper.requireNonNull(uiTasks); IUiTask iUiTask = new IUiTask() { @Override public boolean queueIdle() { if (! uiTasks.isEmpty()) { Runnable task = uiTasks.get(0); task.run(); uiTasks.remove(task); } // Execute one task at a time to avoid occupying the main thread for too long. uiTasks.isEmpty(); }}; Looper.myQueue().addIdleHandler(iUiTask); return this; }Copy the code
Try again using the method above.
@Override protected void onResume() { super.onResume(); Log.e(TAG, "onResume: start"); //1 new Handler().postDelayed(new Runnable() { @Override public void run() { Log.e(TAG, "delay: msg"); startService(new Intent(MainActivity.this, MyService.class)); }}, 3000); UIManager.addTask(() -> test(1)); UIManager.addTask(() -> test(2)); UIManager.addTask(() -> test(3)); UIManager.runUiTasks(); }Copy the code
Get the desired timing result:
Refer to the link
The official guide
IdleHandler for Android lazy loading solution