Dead loop drawing problem caused by ViewPager in Android x

Posted by jmelnick on Sun, 30 Jan 2022 14:58:19 +0100

brief introduction

Recently, when the project was upgraded to Android x, some problems suddenly appeared. The onStop and onDestroy of Activity became very slow, almost ten seconds, resulting in abnormal performance of some pages. So we started to solve this problem.

Find the reason

Causes of onStop and onDestroy callback delays

Let's first look at the causes of onStop and onDestroy callback delays and find an article that is well written: Deeply analyze the onStop and onDestroy() callback delay and 10s delay of Activity in Android To sum up, there are always messages to be processed in the Message queue in Looper, and there is no opportunity to process idle messages.

What message are you dealing with?

Use Looper's setMessageLogging method to print out what message is being processed. Open the APP and wait for a while to see the output log

The eyes are full of Choreographer$FrameHandler. It turns out that the drawing message has been executing all the time, but there is no such problem when using android support! Look for information and find an article Choreographer source code understanding , it is found that all drawings trigger the scheduleTraversals method of ViewRootImpl,
Next, you can only debug the source code. Open the simulator and debug the scheduleTraversals method to see where it is triggered. Look at the call stack as follows

We see a code in the trigger drawing ViewCompat: requestApplyInsets, which is invoked in the setOnApplyWindowInsetsListener method, as follows:

static void setOnApplyWindowInsetsListener(final @NonNull View v,
                final @Nullable OnApplyWindowInsetsListener listener) {
            ......

            ......

            v.setOnApplyWindowInsetsListener(new View.OnApplyWindowInsetsListener() {
                WindowInsetsCompat mLastInsets = null;

                @Override
                public WindowInsets onApplyWindowInsets(final View view,
                        final WindowInsets insets) {
                    WindowInsetsCompat compatInsets = WindowInsetsCompat.toWindowInsetsCompat(
                            insets, view);
                    if (Build.VERSION.SDK_INT < 30) {
                        callCompatInsetAnimationCallback(insets, v);

                        if (compatInsets.equals(mLastInsets)) {
                            // We got the same insets we just return the previously computed insets.
                            return listener.onApplyWindowInsets(view, compatInsets)
                                    .toWindowInsets();
                        }
                    }
                    mLastInsets = compatInsets;
                    compatInsets = listener.onApplyWindowInsets(view, compatInsets);

                    if (Build.VERSION.SDK_INT >= 30) {
                        return compatInsets.toWindowInsets();
                    }

                    // On API < 30, the visibleInsets, used to built WindowInsetsCompat, are
                    // updated after the insets dispatch so we don't have the updated visible
                    // insets at that point. As a workaround, we re-apply the insets so we know
                    // that we'll have the right value the next time it's called.
                    requestApplyInsets(view);
                    // Keep a copy in case the insets haven't changed on the next call so we don't
                    // need to call the listener again.

                    return compatInsets.toWindowInsets();
                }
            });
        }

requestApplyInsets(view) triggers the requestApplyInsets of view,

	// View
	
    public void requestFitSystemWindows() {
        if (mParent != null) {
            mParent.requestFitSystemWindows();
        }
    }

    public void requestApplyInsets() {
        requestFitSystemWindows();
    }

This method triggers the requestFitSystemWindows of the mParent by default, and finally triggers the requestFitSystemWindows method of the mParent of the DecorView (that is, ViewRootImpl), and executes scheduleTraversals for redrawing

	// ViewRootImpl
    @Override
    public void requestFitSystemWindows() {
        checkThread();
        mApplyInsetsRequested = true;
        scheduleTraversals();
    }

requestApplyInsets(view) is triggered when the API is below 30, and the last saved WindowInsetsCompat object is different from this one. Which layouts have onapplywindoinsetslistener set? Find references directly. They are not used in custom layouts and project codes. They are used in the following layouts

WindowInsets

So how is WindowInsets distributed? Continue to search for information and find a clearly written article: Distribution of WindowInsets
It is written in the article that CollapsingToolbarLayout is consumed. Finally, it is written that the Insets returned by ViewPager are not consumed. This may be the problem.
Combined with your own project, write a demo to test. A very simple demo includes ViewPager in Activity and CoordinatorLayout, AppBarLayout and CollapsingToolbarLayout in ViewPager. By default, drawing will be called in an endless loop. We customize a ViewPager, reset setonapplywindoinsetslistener, and then add consumesystemwindoinsets() in the final return

public class CustomViewPager extends ViewPager {
 ......
 return applied
 		.replaceSystemWindowInsets(res.left, res.top, res.right, res.bottom)
 		.consumeSystemWindowInsets();
 ......
}

Re run, and sure enough, there is no more circular drawing.

summary

Let's look at the trigger conditions first

  • Upgrade to Android x
  • Full screen is used (View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN)
  • There is a ViewPager in the page
  • CollapsingToolbarLayout in ViewPager
  • The root layout does not use fitsSystemWindows
    This problem occurs when these conditions are met at the same time.
    When the full screen is not used, Insets will be consumed by the status bar (the status bar is drawn); When the root layout uses fitsSystemWindows, it will be consumed by the root layout; It will not be passed into ViewPager again.
    When this problem occurs on the home page, it will cause the onStop and onDestroy callback delay of all pages. If there is business logic in onStop or onDestroy, it will be affected. Moreover, because the message queue is always full, it will easily lead to ANR.

Topics: Java Android viewpager