First, problem performance

FirstActivity has a click event that takes you to SecondActivity, but clicking on it with a Tag prevents multiple SecondActivities, and this Tag is reset in SecondActivity#onDestroy(). In some cases, Add a log to SecondActivity#onDestroy() and see that onPause() is not called and onDestroy() is 10 seconds late.

Second, the cause of the problem

Predecessors plant trees for descendants to enjoy the shade, thanks to the analysis of ZCJ wind fly big guy:

An in-depth analysis of the Android Activity onStop and onDestroy() callback delay and delay 10s

Through this big guy’s analysis, it can be learned that: After the onResume callback for the next Activity to display, the ActivityThread registers an IdleHandler for the main thread message queue, ActivityManagerService handles Activity#onStop() and Activity#onDestroy(). This IdleHandler must never be called if the main thread is looping through messages accumulated in the Message queue. As a robust ROM, AMS sends a message delayed by 10 seconds, ensuring that the Activity can be destroyed if the normal process doesn’t work, thus indicating that onPause() is not called and onDestroy() is delayed by 10 seconds.

Third, problem verification

Thanks again to ZCJ Wind fly big guy:

public class SecondActivity extends Activity {
    private static final String TAG = "SecondActivity";
    private Handler handler = new Handler();
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        postMsg();
    }

    private void postMsg() {
        handler.post(new Runnable() {
            @Override
            public void runThread.sleep(20) {thread.sleep (20); thread.sleep (20); thread.sleep (20); } catch (InterruptedException e) { e.printStackTrace(); }}}); handler.postDelayed(newRunnable() {
            @Override
            public void run() { postMsg(); }}, 10); } @Override protected voidonDestroy() {
        super.onDestroy();
        Log.i(TAG, "onDestroy: ");
    }

    @Override
    protected void onStop() {
        super.onStop();
        Log.i(TAG, "onStop: "); }}Copy the code

In the example above, onDestroy() is called 10s late, so the main thread MessageQueue is looping through accumulated messages. So the question is, what are these cumulative messages? And who sent it?

Fourth, problem solving

1. The cumulativeMessageWhat is the

The Handler mechanism can be used. The main thread holds a Looper, and the Looper maintains a MessageQueue inside to schedule various messages. Therefore, you can try to understand what Message is through MessageQueue. Read the Looper source code and find the following code:

    /**
     * Dumps the state of the looper for debugging purposes.
     *
     * @param pw A printer to receive the contents of the dump.
     * @param prefix A prefix to prepend to each line which is printed.
     */
    public void dump(@NonNull Printer pw, @NonNull String prefix) {
        pw.println(prefix + toString());
        mQueue.dump(pw, prefix + "", null);
    }
Copy the code

MessageQueue#dump()

    void dump(Printer pw, String prefix, Handler h) {
        synchronized (this) {
            long now = SystemClock.uptimeMillis();
            int n = 0;
            for(Message msg = mMessages; msg ! = null; msg = msg.next) {if (h == null || h == msg.target) {
                    pw.println(prefix + "Message " + n + ":" + msg.toString(now));
                }
                n++;
            }
            pw.println(prefix + "(Total messages: " + n + ", polling=" + isPollingLocked()
                    + ", quitting=" + mQuitting + ")"); }}Copy the code

It looks like you can pull the list of messages in the main thread Message queue at a particular moment, try the following code:

private void tryDump() {// For example only, direct new Thread() new is not recommendedThread() {
            @Override
            public void run() {
                while (true) {
                    Looper.getMainLooper().dump(new Printer() {
                        @Override
                        public void println(String x) {
                            Log.i("SecondActivity"."println: "+ x); }},"");
                    try {
                        Thread.sleep(500);
                    } catch (Exception ignore) { }
                }
            }
        }.start();
    }
Copy the code

Get a log similar to the following:

when
Message
SlideShineImageView

Message
Looper

    public static void loop() {// *** omit some code ***for(; ;) { Message msg = queue.next(); // might blockif (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
            final Printer logging = me.mLogging;
            if(logging ! = null) { logging.println(">>>>> Dispatching to " + msg.target + "" +
                        msg.callback + ":"+ msg.what); } // *** omit some code ***if(logging ! = null) { logging.println("<<<<< Finished to " + msg.target + ""+ msg.callback); } // *** omit some code ***}}Copy the code

Therefore, if Printer is set, the Message executed by the main thread can be printed. Looper provides this method:

/**
     * @param printer A Printer object that will receive log messages, or
     * null to disable message logging.
     */
    public void setMessageLogging(@Nullable Printer printer) {
        mLogging = printer;
    }
Copy the code

Try setting and get a log similar to the following:

View
Message
Draw messages for the system
View#requestLayout()
View#invalidate()

2. Who is the culprit

Although I know the cause of the problem, but because there are many views, directly adding logs in all views is a lot of work and easy to do nothing, so I think the View itself is a tree, View#requestLayout() orView # Invalidate () will definitely end up calling back to the tree root (DecorView), so try overriding some of the methods in the ContentView of FirstActivity to find the problem View. View#requestLayout()

    public void requestLayout() {// *** omit some code ***if(mParent ! = null && ! mParent.isLayoutRequested()) { mParent.requestLayout(); }if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == this) {
            mAttachInfo.mViewRequestingLayout = null;
        }
    }
Copy the code

The final callback to ViewRootImpl#requestLayout() looks like this:

    public void requestLayout() {
        if(! mHandlingLayoutInLayoutRequest) { checkThread(); mLayoutRequested =true; scheduleTraversals(); }}Copy the code

This will trigger the View’s drawing process logic, so try using the following ViewGroup as the root layout of the FirstActivity:

public class FirstActivityRootLayout extends FrameLayout {
    private static final String TAG = "FirstActivityRootLayout";

    public FirstActivityRootLayout(Context context, AttributeSet attributeSet) {
        super(context, attributeSet);
    }

    @Override
    public void requestLayout() {
        Log.i(TAG, "requestLayout: ", new Exception()); super.requestLayout(); }}Copy the code

However, the code logic of requestLayout() is working fine, so check the View#invalidate() process. The invalidate() code looks like this:

 public void invalidate() {
        invalidate(true);
}
Copy the code
public void invalidate(boolean invalidateCache) {
        invalidateInternal(0, 0, mRight - mLeft, mBottom - mTop, invalidateCache, true);
}
Copy the code
Void invalidateInternal(int L, int t, int r, int b, Boolean invalidateCache, Boolean fullInvalidate) {// *** omit some codeif((mPrivateFlags & (PFLAG_DRAWN | PFLAG_HAS_BOUNDS)) == (PFLAG_DRAWN | PFLAG_HAS_BOUNDS) || (invalidateCache && (mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == PFLAG_DRAWING_CACHE_VALID) || (mPrivateFlags & PFLAG_INVALIDATED) ! = PFLAG_INVALIDATED || (fullInvalidate && isOpaque() ! // *** final ViewParent p = mLastIsOpaque);if(p ! = null && ai ! = null && l < r && t < b) { final Rect damage = ai.mTmpInvalRect; damage.set(l, t, r, b); P.invalidatechild (this, damage); } // *** omit some code}}Copy the code

As you can see from the above code, invalide() will only refresh the current View area, so overriding the Invalide () method directly in FirstActivityRootLayout will not find the problem View because it may not be full-screen. Notice the invalidateChild() method in the above code, which has the following code in ViewGroup:

    /**
     * Don't call or override this method. It is used for the implementation of * the view hierarchy. * * @deprecated Use {@link #onDescendantInvalidated(View, View)} instead to observe updates to * draw state in descendants. */ @Deprecated @Override public final void invalidateChild(View child, final Rect dirty) { final AttachInfo attachInfo = mAttachInfo; if (attachInfo ! = null && attachInfo.mHardwareAccelerated) { // HW accelerated fast path onDescendantInvalidated(child, child); return; } ViewParent parent = this; if (attachInfo ! Do {View View = null; if (parent instanceof View) { view = (View) parent; } / / * * * to omit part of the code parent. = the parent invalidateChildInParent (location, dirty); } while (parent! = null); }}Copy the code

This method is final and cannot be overridden by subclasses; “Hardware acceleration” : “onDescendantInvalidated” () “; otherwise, “invalidateChildInParent” () will eventually invoke viewrootimp #scheduleTraversals(), Trigger the View’s drawing process logic and notice that neither method is final. Change the FirstActivityRootLayout code:

public class FirstActivityRootLayout extends FrameLayout {
    private static final String TAG = "FirstActivityRootLayout";

    public FirstActivityRootLayout(Context context, AttributeSet attributeSet) {
        super(context, attributeSet);
    }

    @Override
    public void requestLayout() {
        Log.i(TAG, "requestLayout: ", new Exception());
        super.requestLayout();
    }

    @Override
    public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
        Log.i(TAG, "invalidateChildInParent: ", new Exception());
        return super.invalidateChildInParent(location, dirty);
    }

    @Override
    public void onDescendantInvalidated(@NonNull View child, @NonNull View target) {
        super.onDescendantInvalidated(child, target);
        Log.i(TAG, "onDescendantInvalidated: ", new Exception()); }}Copy the code

Finally, the full screen repeated log:

In the customTextView#onDraw()Call thesetTextColor()Indirectly calledinvalidate(), causing a loop call, resulting in the main thread is full of requests to draw messages
onPause()
onDestroy()

  • Standing on the shoulders of giants, thank you againZCJ wind flyBosses:

    An in-depth analysis of the Android Activity onStop and onDestroy() callback delay and delay 10s