preface
Recently found a very strange screen phenomenon in the project, spent a lot of time to investigate and trace the source code, finally find out the reason and solve, then issued to share with you.
The phenomenon of
Without further ado, just look at the pictureThe demo above is very simple, adding a Button and a ProgressBar(to show the current stuck-up) to the MainActivity under a project generated directly by Android Studio. As you can see, the interface freezes after I click the Button.
So it’s time for questions: What happens when I press a Button? What might have caused it?
I think most people instinctively want to say: Did you perform a time-consuming task on the main thread or trigger a deadlock that caused the ANR?
However, if you look closely at the timer on the left of the screenshot, you can see that the ANR dialog does not pop up after 10 seconds. Also, if you look closely at the Profiler screen, you can see that there is no significant change in CPU or memory during the whole process. (The only fluctuation is caused by the ProgressBar. If removed, there is no fluctuation at all), and the click event can still respond when stuck.
Without further suspense, let’s look at my code in Button’s OnClickListener:
button.setOnClickListener {
textView.viewTreeObserver.addOnPreDrawListener(object : ViewTreeObserver.OnPreDrawListener{
override fun onPreDraw(a): Boolean {
textView.viewTreeObserver.removeOnPreDrawListener(this) / / 1
return false / / 2
}
})
container.removeView(textView) //3 Container is the root container of the current Activity, which is just a LinearLayout
}
Copy the code
See? No time-consuming tasks. (For hands-on students, you can use this code to run and see the effect.)
explore
OnPreDrawListener? So let’s see what the OnPreDrawListener is.
Interface definition for a callback to be invoked when the view tree is about to be drawn. – developer.android.com
That onPreDraw callback is what origin:
Callback method to be invoked when the view tree is about to be drawn. At this point, all views in the tree have been measured and given a frame. Clients can use this to adjust their scroll bounds or even to request a new layout before drawing occurs. – developer.android.com
A simple translation is:
OnPreDrawListener defines the interface for the callback function to execute when the view tree is to be drawn; OnPreDraw is the callback function that is executed when the view tree is about to be drawn. At this point all views are measured and the boundaries are defined. Clients can use this method to adjust scroll borders or even request a new layout before drawing.
The practical applications I have seen in real projects are mainly in two aspects:
- Used to get the size measurement of the View. As we all know, View should be measured in order to get the correct width and height, and onPreDraw is just measured before the time of drawing; When customizing a View, OnPreDrawListener is useful when the customization behavior depends on the actual size of the View.
- Performance monitoring. Such as calculating the rendering time of the first frame, or calculating the frame rate.
This seems like a harmless API
…………………
Until I interrupted to find out that onPreDraw wouldconstantlyThe callback. I did it on a callbackremoveOnPreDrawListener
Remove listener!
Analysis of the
With a question mark in my head, I set out to trace the source code. The removeOnPreDrawListener did not execute successfully. As for why it didn’t work, I turned to the ViewTreeObserver first, and took a look at the source code
//View.java
public ViewTreeObserver getViewTreeObserver(a) {
if(mAttachInfo ! =null) {
return mAttachInfo.mTreeObserver;
}
if (mFloatingTreeObserver == null) {
mFloatingTreeObserver = new ViewTreeObserver(mContext);
}
return mFloatingTreeObserver;
}
Copy the code
Boy, viewTreeObserver used to have two faces. When the View is attached to the window, getViewTreeObserver returns the mTreeObserver from attachInfo, If the View is not attached to the window (that is, removed from the parent or never added to any parent), another ViewTreeObserver is returned. This explains why my removeOnPreDrawListener didn’t work:
- So when I call addOnPreDrawListener, the textView is attached to the View tree, right here
OnPreDrawListener
Is held by the attachInfo mTreeObserver of the current View tree - The first onPreDraw callback happens after I call removeView(note 3)
- In comment 2, since the textView has already been removed, attachInfo is empty and a new one is created
ViewTreeObserver
The new ViewTreeObserver does not hold the one we just addedOnPreDrawListener
.
PS:ViewTreeObserver and attachInfo will be explained later
So what can we do about this? Should I also own and maintain ViewTreeObserver? So I did a lot of research and found what the Engineers at Google thought about this problem. Here is an excerpt:
When you receive ViewTreeObserver from a View, it returns a fake one if it is not attached. When it is attached to the window, it moves all listeners to the actual window’s listener. Unfortunately, it does not remove them from the window’s listener if the view is detached. At this point, it is very hard to change the behavior of this API because it is in the framework and there are probably apps relying on this behavior. I’ll keep the bug open and maybe we’ll consider something for O release.
A brief translation:
The listener you add to a view detach when the view is attached will help you adopt it to the ViewTreeObserver of the view tree, but when the view is subsequently detach, It does not remove or transfer the listener you added earlier to the mFloatingTreeObserver. This is an early bug that can’t be fixed at the moment, maybe something will be considered for Android 8 (it’s Android 11 now).
Still, Google’s engineers suggest ways to get around it:
As a workaround for your use case, you should always receive the view tree observer from a view that you know won’t be detached. (e.g. the RecyclerView). There is only a single real view tree observer for your view tree so it does not matter from which view you receive it.
Let me translate it again: There is only one valid ViewTreeObserver globally for a View tree. If you want to get this variable, you should get it from the top View that will not be removed. (The mistake we made above was to get the ViewTreeObserver from textView, The textView will be removed), and you will get the temp mFloatingTreeObserver, causing the ViewTreeObserver to be inconsistent.
For this point, we can also check the source code (the following paragraph if you do not understand, please read the View drawing process related article).
//View.java
void dispatchAttachedToWindow(AttachInfo info, int visibility) { mAttachInfo = info; . }void dispatchDetachedFromWindow(a) {... mAttachInfo =null; . }Copy the code
Above we said that the real ViewTreeObserver is provided in the View’s mAttachInfo, MAttachInfo is passed in the dispatchAttachedToWindow method parameter, and the caller of dispatchAttachedToWindow is our old friend ViewRootImpl
//ViewRootImpl.java
public ViewRootImpl(Context context, Display display) {... mAttachInfo =new View.AttachInfo(mWindowSession, mWindow, display, this, mHandler, this, context); . }private void performTraversals(a) {
// cache mView since it is used so much below...
finalView host = mView; .if (mFirst) { // If it is the first time to draw
host.dispatchAttachedToWindow(mAttachInfo, 0); }... }Copy the code
As you can see from the code above, attachInfo is created with the ViewRoot and is passed to all views in the entire View tree on the first draw, so the ViewTreeObserver obtained from any View on the View tree is the same.
So a simple change to my code solves this problem:
button.setOnClickListener {
container.viewTreeObserver.addOnPreDrawListener(object : ViewTreeObserver.OnPreDrawListener{
override fun onPreDraw(a): Boolean {
container.viewTreeObserver.removeOnPreDrawListener(this) //1 no longer uses textView to get viewTreeObserver
return false / / 2
}
})
container.removeView(textView) //3 Container is the root container of the current Activity, which is just a LinearLayout
}
Copy the code
further
Through the above modification, the problem was solved, but it did not solve my other question: why does this cause the problem? OnPreDraw calls back frequently (and does nothing time-consuming in callback), but does not cause ANR, so why does the whole screen freeze, but click events respond?
Let’s revisit OnPreDrawListener:
public interface OnPreDrawListener {
/**
* Callback method to be invoked when the view tree is about to be drawn. At this point, all
* views in the tree have been measured and given a frame. Clients can use this to adjust
* their scroll bounds or even to request a new layout before drawing occurs.
*
* @returnReturn true to proceed with the current drawing pass, or false to cancel. Pay attention to this sentence! * *@see android.view.View#onMeasure
* @see android.view.View#onLayout
* @see android.view.View#onDraw
*/
public boolean onPreDraw(a);
}
Copy the code
For the return value of onPreDraw, the documentation means that returning true will continue the drawing, false will cancel. The text is a little pale, we still go to the source code. Since the OnPreDrawListener is held by the ViewTreeObserver, let’s start with it:
//ViewTreeObserver.java
/ * * *@return True if the current draw should be canceled and resceduled, false otherwise.
*/
public final boolean dispatchOnPreDraw(a) {
boolean cancelDraw = false;
final CopyOnWriteArray<OnPreDrawListener> listeners = mOnPreDrawListeners;
if(listeners ! =null && listeners.size() > 0) {
CopyOnWriteArray.Access<OnPreDrawListener> access = listeners.start();
try {
int count = access.size();
for (int i = 0; i < count; i++) {
cancelDraw |= !(access.get(i).onPreDraw());
}
} finally{ listeners.end(); }}return cancelDraw;
}
Copy the code
Pay attention to the return value of this method, if return true, the rendering process will be cancelled (canceled) or delay (resceduled), false, continued to draw attention to onPreDraw is on the contrary, the key in this statement: cancelDraw | =! (access.get(i).onPreDraw()); I took the inverse here.
Where does dispatchOnPreDraw work? A search found:
//ViewRootImpl.java
private void performTraversals(a) {...booleancancelDraw = mAttachInfo.mTreeObserver.dispatchOnPreDraw() || ! isViewVisible;if(! cancelDraw && ! newSurface) {// Here is the point. performDraw(); }else {
if (isViewVisible) {
// Try again
scheduleTraversals();
} else if(mPendingTransitions ! =null && mPendingTransitions.size() > 0) {... }}}Copy the code
As we all know, View rendering starts from performTraversals() and goes through three traversals: performMeasure(), performLayout(), and performDraw(). But if dispatchOnPreDraw returns true then performDraw will not be traversed and will wait for the next scheduling traversals (Vsync signal). But next time dispatchOnPreDraw returns true and continues to skip performDraw. And so on and so forth, the View can’t draw at all, and it’s stuck. But here only the performDraw() method does not run, otherwise no process is blocked, the main thread message queue is not affected, so you can’t see the ANR dialog.
conclusion
The code I posted at the beginning made two mistakes:
- From views that may be removed
ViewTreeObserver
(resulting in aOnPreDrawListener
Deregistration failed) OnPreDrawListener.onPreDraw
Returns false (causing the drawing process to fail)
experience
- To obtain
ViewTreeObserver
Should be by comparisonThe top layer may not be removedThe View of - use
OnPreDrawListener
Be careful when onPreDraw returns false (at least not all the time)
One More Thing
- In addition to the examples I’ve given above, do you know of any other scenarios that might trigger this screen freeze problem?
Without further ado, go to the code
Class MyTextView: TextView{// Constructor omits override fun onPreDraw(): Boolean {// What if I call super.onpredraw ()? Return false}} / / using literally set a MovementMethod myTextView. MovementMethod = LinkMovementMethod ()Copy the code
Take a run… What’s up? Is it stuck? Why is that?
- Although the original example in this article was obtained through Experience 1, that is, through views that are not removed
ViewTreeObserver
If onPreDraw returns false, it doesn’t matter. It is recommended to return true as possible, since returning false will drop frames (see above).
You can try it out with the following example:
class MyAdapter : RecyclerView.Adapter<MyAdapter.MyViewHolder>() { private var data: List<Item> = ArrayList() override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder { return MyViewHolder( LayoutInflater.from(parent.context) .inflate(R.layout.recycler_item_layout, parent, false) ) } override fun getItemCount() = data.size override fun onBindViewHolder(holder: MyViewHolder, position: Int) { holder.itemView.viewTreeObserver.addOnPreDrawListener(object: ViewTreeObserver.OnPreDrawListener{ override fun onPreDraw(): Boolean {holder. ItemView. ViewTreeObserver. RemoveOnPreDrawListener (this) return false}})}} / / omit a lot of boilerplate codeCopy the code
Run with a RecyclerView and quickly swipe to see:
Try changing the return value to true
OnPreDraw returns false.