Exploration of the picture rotation problem caused by View's onAttachedToWindow
foreword
This article is on View's postDelayed method deep thinking All the basics of this article are studied theoretically, which can be said to be View's postDelayed method deep thinking The practice of knowledge points in this article.
One day, when a colleague, Xjin, was making a list page to add a carousel Banner requirement, he sent a problem that the carousel interval time would occasionally be disordered.
I saw his implementation of the carousel: use Handle.postDelayed to interval the carousel duration and send it in a loop after each execution of the carousel;
The code seems to have no major problems, but it seems that removeCallbacks should be invalid~!
Handle#removeCallbacks
I found relevant information on stackoverflow Why to use removeCallbacks() with postDelayed()? , and then try to change the postDelayed to post if it is unreliable, and found that the problem that the carousel interval time seems to be disordered has been solved~!
Although it is not clear what caused the problem to no longer reappear, the follow-up investigation was not continued due to other work interruptions.
A few days later, the problem of disordered rotation intervals appeared once again.
This time we use a custom Handler for removeCallBacks and postDelayed, which perfectly solves the problem.
Let's record the thinking in the process of solving the whole problem~!
outstanding issues
- Is View.removeCallbacks really reliable;
- Why is the bug recurrence frequency lower than View.post and View.postDelayed;
View#dispatchAttachedToWindow
Is the removeCallBacks removal method of Handle unreliable? If the current task is not running, the task must be removed.
In other words, what Handle#removeCallBacks removes is the Message waiting to be executed in the queue.
So where is the problem, and why is the recurrence probability of postDelayed replaced by post problem reduced?
For some time this time, I followed the source code and found that messages sent using View#postDelayed may not be immediately placed in the message queue.
look back before View's postDelayed method deep thinking The description in the summary of View.postDelayed in this article:
When the postDelayed method is called, if the current View is not attached to the Window, first cache the Runnable in the RunQueue queue. After View.dispatchAttachedToWindow is called, it will be postDelayed by ViewRootHandler again. In this process, the same Runnable will only be postDelay ed once.
We print the Handler instance of ViewPager#getHandler when the stopTimer and startTimer methods are executed, and find that most of them are null when the list slides quickly.
Well, I ignored this Banner being View#dispatchDetachedFromWindow during the sliding process. Calling this method will cause the Handle inside the View to be null.
If the Handle of View is null, the execution of Message may be affected.
exist View's postDelayed method deep thinking In this article, the impact of mAttachInfo on View.postDelayed is also analyzed. Here we pick up the main source code and read it.
//View.java void dispatchAttachedToWindow(AttachInfo info, int visibility) { mAttachInfo = info; /****Some codes are omitted*****/ // Transfer all pending runnables. if (mRunQueue != null) { mRunQueue.executeActions(info.mHandler); mRunQueue = null; } performCollectViewAttributes(mAttachInfo, visibility); onAttachedToWindow(); /****Some codes are omitted*****/ } public boolean postDelayed(Runnable action, long delayMillis) { final AttachInfo attachInfo = mAttachInfo; if (attachInfo != null) { return attachInfo.mHandler.postDelayed(action, delayMillis); } // Postpone the runnable until we know on which thread it needs to run. // Assume that the runnable will be successfully placed after attach. getRunQueue().postDelayed(action, delayMillis); return true; } public boolean post(Runnable action) { final AttachInfo attachInfo = mAttachInfo; if (attachInfo != null) { return attachInfo.mHandler.post(action); } // Postpone the runnable until we know on which thread it needs to run. // Assume that the runnable will be successfully placed after attach. getRunQueue().post(action); return true; } public boolean removeCallbacks(Runnable action) { if (action != null) { final AttachInfo attachInfo = mAttachInfo; if (attachInfo != null) { attachInfo.mHandler.removeCallbacks(action); attachInfo.mViewRootImpl.mChoreographer.removeCallbacks( Choreographer.CALLBACK_ANIMATION, action, null); } getRunQueue().removeCallbacks(action); } return true; }
post and postDelayed in View's postDelayed method deep thinking As explained in this article, the Message stored in the RunQueue will be executed when the View executes the dispatchAttachedToWindow method.
RunQueue.executeActions is called in ViewRootImpl.performTraversal;
RunQueue.executeActions is called after executing host.dispatchAttachedToWindow(mAttachInfo, 0);
RunQueue.executeActions is called every time ViewRootImpl.performTraversal is executed;
The parameter of RunQueue.executeActions is the Handler in mAttachInfo, which is ViewRootHandler;
From here, there is no problem. The messages we use View#post will be executed when the View is Attached;
During the development of general programs, if the use of containers is involved, two situations of production and consumption must be considered.
In the source code above, we have seen the logic of message execution (eventually all messages will be consumed in MainLooper). What if the messages involved are removed?
public class HandlerActionQueue { public void removeCallbacks(Runnable action) { synchronized (this) { final int count = mCount; int j = 0; final HandlerAction[] actions = mActions; for (int i = 0; i < count; i++) { if (actions[i].matches(action)) { // Remove this action by overwriting it within // this loop or nulling it out later. continue; } if (j != i) { // At least one previous entry was removed, so // this one needs to move to the "new" list. actions[j] = actions[i]; } j++; } // The "new" list only has j entries. mCount = j; // Null out any remaining entries. for (; j < count; j++) { actions[j] = null; } } } }
When removing the message, if the mAttahInfo of the current View is empty, then we will only remove the cached message in the RunQuque. . .
oh oh
It turned out to be like this~!
That's really the only way~!
To sum up, if View#mAttachInfo is not empty then hello, me, and everyone. Otherwise, the View#post message will wait to be added in the cache queue, but the removed message can only remove the cached message in the RunQueue. If the message in the RunQueue has been synchronized to the MainLooper at this time, then, sorry, the concubine cannot be removed without View#mAttachInfo.
According to the previous business code, if the current View is dispatchedDetachedFromWindow After the message removal operation is performed, the messages already in the MainLooper queue cannot be removed and if you continue to add carousel messages, it will cause frequent carousel code blocks implement.
The text description may not be easy to understand for a while, the following is a simple analysis diagram of an unexpected carousel (why there are multiple carousel messages):
Let's talk about post and postDelayed
If I only look at the relevant source code, I feel that I can't find the problem, because the postDelayed method is also executed at the end of the post. So the comparison between the two is just a time difference. What impact can this time difference cause?
Looking back at the articles I wrote before Another year of thinking on the Android message mechanism (Handler&Looper) , one of which is called a synchronization barrier.
Synchronous barrier: Ignore all synchronous messages and return asynchronous messages. In other words, the synchronization barrier adds a simple priority mechanism to the Handler message mechanism, and the priority of asynchronous messages is higher than that of synchronous messages.
The most commonly used synchronization barrier is page refresh (ViewRootImpl#mTraversalRunnable). Related articles can be read Choreographer for Android ,and Monologue of ViewRootImpl, I am not a View (layout) This article describes that the View#dispatchAttachedToWindow method is triggered by ViewRootImpl#performTraversals.
Why do we say synchronization barrier? It can be seen from the flow chart of the unexpected carousel above that the method call of View#dispatchAttachedToWindow is very important to the whole process. Remove and add two messages. If there are other messages inserted in the middle due to postDelayed, the synchronization barrier is the most likely message to be inserted and this message will change View#mAttachInfo.
This makes the code with some small problems worse, and the bug is easier to reproduce.
Speaking of RecycleView
Why mention this question, because many times we use View.post to perform tasks without any problems (PS: I feel that this view is also the original source of this problem).
We know that RecycleView's internal sub-View is just one more preloaded View than the screen size, and exceeding this range or entering this range will cause View to be added and removed.
public class RecyclerView extends ViewGroup implements ScrollingView, NestedScrollingChild2 { /***Some codes omitted***/ private void initChildrenHelper() { this.mChildHelper = new ChildHelper(new Callback() { public int getChildCount() { return RecyclerView.this.getChildCount(); } public void addView(View child, int index) { RecyclerView.this.addView(child, index); RecyclerView.this.dispatchChildAttached(child); } public int indexOfChild(View view) { return RecyclerView.this.indexOfChild(view); } public void removeViewAt(int index) { View child = RecyclerView.this.getChildAt(index); if (child != null) { RecyclerView.this.dispatchChildDetached(child); child.clearAnimation(); } RecyclerView.this.removeViewAt(index); } } /***Some codes omitted***/ } /***Some codes omitted***/ }
If we frequently slide the list back and forth, then this Banner will be continuously executed dispatchAttachedToWindow and dispatchDetachedToWindow.
This causes View#mAttachInfo to be null most of the time, which affects the execution logic of the Message sent to the main thread in the business code.
The article is almost here. Solving this problem has brought me a deep feeling. Before learning the relevant source code of the Android system, it was just that everyone was learning and interviewing.
There are still relatively few knowledge points that can be applied to the actual research and development process. In many cases, it is enough to solve the problem, that is, knowing it but not knowing why.
The problem solved this time can make me feel deeply that fuck the source code is beautifully.
The article is all told here, if you have other needs to communicate, you can leave a message~!
In 2023, I wish you a new year with ever-changing mood, happiness as sugar as honey, friends who value love and righteousness, lovers who will never leave, success at work, and everything goes well!