preface

After the App was launched, one or two bugs were found in almost every version through the bug collection system.

java.lang.IllegalStateException: View android.widget.LinearLayout{9da7609 V.E...... . I. 0,0-0,0} has already been added to the window manager.at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:441) at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:130) at android.widget.Toast$TN.handleShow(Toast.java:588)  at android.widget.Toast$TN$1.handleMessage(Toast.java:484) at android.os.Handler.dispatchMessage(Handler.java:110) at android.os.Looper.loop(Looper.java:219) at android.app.ActivityThread.main(ActivityThread.java:8668) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:513) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1109)Copy the code

As can be seen from the log, it is a bug reported by the system framework layer during Toast#show. Under normal circumstances, Toast singleton is performed for performance optimization in projects, and the code is as follows:

public class ToastUtils { private static Toast toast; public static void show(Context context, String text) { if (toast == null) { toast = Toast.makeText(context, "", Toast.LENGTH_SHORT); } ((TextView) toast.getView().findViewById(android.R.id.message)).setText(text); toast.setDuration(Toast.LENGTH_SHORT); toast.show(); }}Copy the code

Encountered this error, the use of conventional means has been unable to repair, need to in-depth study of the underlying source code, and then determine the solution, the next is a difficult analysis of the process of reinvestigation, pay attention to this article focuses on the framework source code fly analysis, full of dry goods, to have psychological preparation!

The body of the

Cause analysis,

$TN#handleShow() = Toast$TN#handleShow() = Toast$TN#handleShow() = Toast$TN#handleShow()

Code fragment 1 public void handleShow (IBinder windowToken) {/ / comment 1 if (mHandler. HasMessages (CANCEL) | | mHandler.hasMessages(HIDE)) { return; } if (mView ! = mNextView) { handleHide(); mView = mNextView; Context context = mView.getContext().getApplicationContext(); String packageName = mView.getContext().getOpPackageName(); if (context == null) { context = mView.getContext(); } mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE); final Configuration config = mView.getContext().getResources().getConfiguration(); final int gravity = Gravity.getAbsoluteGravity(mGravity, config.getLayoutDirection()); mParams.gravity = gravity; If ((gravity & gravity.HORIZONTAL_GRAVITY_MASK) == gravity.FILL_HORIZONTAL) {mparams.horizontalweight = 1.0f; } if ((gravity & gravity.VERTICAL_GRAVITY_MASK) == gravity.FILL_VERTICAL) {mparams.verticalweight = 1.0f; } mParams.x = mX; mParams.y = mY; mParams.verticalMargin = mVerticalMargin; mParams.horizontalMargin = mHorizontalMargin; mParams.packageName = packageName; mParams.hideTimeoutMilliseconds = mDuration == Toast.LENGTH_LONG ? LONG_DURATION_TIMEOUT : SHORT_DURATION_TIMEOUT; mParams.token = windowToken; If (mview.getparent ()! = null) { mWM.removeView(mView); } try {// comment 3 mwm. addView(mView, mParams); trySendAccessibilityEvent(); } catch (WindowManager.BadTokenException e) { /* ignore */ } } }Copy the code

WindowManagerImpl#addView is called in comment 3, and WindowManagerGlobal#addView() is finally called. J captures some of the key code as follows:

Public void addView(View View, ViewGroup.LayoutParams params, Display Display, Window parentWindow) { ViewRootImpl root; View panelParentView = null; 1 int index = findViewLocked(view, false); If (index >= 0) {// 表 2 if (mdyingviews.contains (view)) {// 表 3 mroots.get (index).dodie (); } else {// comment 4 Throw new IllegalStateException("View "+ View +" has already been added to the Window Manager); } } root = new ViewRootImpl(view.getContext(), display); view.setLayoutParams(wparams); Mviews.add (view); mRoots.add(root); mParams.add(wparams); // 解 释6 root.setView(view, wparams, wparams) panelParentView); } Catch (RuntimeException e) {BadTokenException or InvalidDisplayException, If (index >= 0) {removeViewLocked(index, true); } throw e; }}}Copy the code

If the process goes to this point, Windows ManagerGlobal maintains three Lis (mViews, mRoots, and mParams). Each time a View is added successfully, there will be a record corresponding to the View to be added. Then go back to comment 1. FindViewLocked is called to check if the view to be added already exists, as follows:

Private int findViewLocked(View View, Boolean Required) {final int index = mviews.indexof (View); if (required && index < 0) { throw new IllegalArgumentException("View=" + view + " not attached to window manager"); } return index; }Copy the code

If the View index is greater than 0, go to comment 2. If the View index is greater than 0, go to comment 3. If the View index is greater than 0, go to comment 3. Deletion will eventually go to WindowMannagerGlobal#doRemoveView, as shown below

Void doRemoveView(ViewRootImpl root) {synchronized (mLock) {final int indexOf(root); if (index >= 0) { mRoots.remove(index); mParams.remove(index); final View view = mViews.remove(index); mDyingViews.remove(view); }}},Copy the code

Comment 1 is corresponding to the code in comment 5 of code fragment 2 WindowMannagerGlobal#addView(), which deletes mViews, mRoots, and mParams maintained by Windows manager global. WindowMannagerGlobal#addView() : the View to be added exists in mViews, but not in mDyingViews. So let’s look at how Kangkang mDyingViews is used in Windows Global. Only removeViewLocked() is called in the entire WindowMannagerGlobal, as follows:

Private void removeViewLocked(int index, Boolean immediate) {ViewRootImpl root = mroots.get (index); View view = root.getView(); # comment 1 Boolean deferred = root.die(immediate); if (view ! = null) {// comment 2 view.assignParent(null); If (deferred) {// comment 3 mdyingviews.add (view); }}}Copy the code

If immediate is true, the ViewRootImpl#doDie() is called immediately. If immediate is false, send MSG through Handler, execute doDie() at Handler’s callback, timing may be a bit slower, and add the View to be deleted at comment 3 to mDyingViews. If the parent of the current View is not null, WindowMangerImpl#remoView() will be called, and removeViewLocked() will be called. Since remoView() was called before addView, the addView() process should not be buggy logic.

The truth is that the removeView() in comment 2 of snippet 1 is either not executed or the execution is not properly executed. Under what circumstances does this happen? In order to understand these things thoroughly, it is necessary to analyze the presentation process of Toast step by step.

Toast display process

Take a look at Toast#show() as follows:

Public void show() {if (mNextView == null) {throw new RuntimeException("setView must have been called"); } INotificationManager service = getService(); String pkg = mContext.getOpPackageName(); TN tn = mTN; tn.mNextView = mNextView; Try {// comment 1 service.enqueueToast(PKG, TN, mDuration); } catch (RemoteException e) { // Empty } }Copy the code

Note 1 can see call NotificationManagerService# enqueueToast () (note NotificationManagerService is running in the system process syster_sever) launched a cross-process communication, Immediately after the call returns as follows:

Public void enqueueToast(String PKG, ITransientNotification callback, int duration) { synchronized (mToastQueue) { int callingPid = Binder.getCallingPid(); long callingId = Binder.clearCallingIdentity(); Try {// omit code} else {// Limit the number of toasts that any given package except the Android // package can enqueue. Prevents DOS attacks and deals with leaks. if (! isSystemToast) { int count = 0; final int N = mToastQueue.size(); for (int i=0; i<N; i++) { final ToastRecord r = mToastQueue.get(i); if (r.pkg.equals(pkg)) { count++; if (count >= MAX_PACKAGE_NOTIFICATIONS) { Slog.e(TAG, "Package has already posted " + count + " toasts. Not showing more. Package=" + pkg); return; } } } } Binder token = new Binder(); / / comment 1 mWindowManagerInternal. AddWindowToken (token, TYPE_TOAST DEFAULT_DISPLAY); record = new ToastRecord(callingPid, pkg, callback, duration, token); // comment 2 mtoastqueue.add (record); index = mToastQueue.size() - 1; keepProcessAliveIfNeededLocked(callingPid); If (index == 0) {// Comment 3 showNextToastLocked(); } } finally { Binder.restoreCallingIdentity(callingId); }}}Copy the code

Note that comment 1 adds a token of type TYPE_TOAST and goes to comment 2. The mToastQueue is empty at first. After add, the size is 1, so we go to comment 3. Enter showNextToastLocked() as follows:

Void showNextToastLocked() {ToastRecord record = mtoastqueue.get (0); while (record ! = null) {try {// annotation 1 Record.callback. show(record.token); Note 2 scheduleTimeoutLocked (record); return; } catch (RemoteException e) { } }Copy the code

Binder: Toast$TN#show() Toast$TN#show() Toast$TN#show()

public void show(IBinder windowToken) {
    if (localLOGV) Log.v(TAG, "SHOW: " + this);
    mHandler.obtainMessage(SHOW, windowToken).sendToTarget();
}
Copy the code

A message is sent to mHandler that eventually walks to code snippet 1 Toast$TN#handleShow() if everything is ok, the Toast popup is displayed, return to comment 2 of code snippet 8, Enter the NotificationManagerService# scheduleTimeoutLocked (), as follows:

private void scheduleTimeoutLocked(ToastRecord r) { mHandler.removeCallbacksAndMessages(r); Message m = Message.obtain(mHandler, MESSAGE_TIMEOUT, r); Configuration == toas.length_long? LONG_DELAY : SHORT_DELAY; / / comment 2 mHandler. SendMessageDelayed (m, delay); }Copy the code

LENGTH_SHORT the future is 2000ms.LENGTH_SHORT the future is 2000ms. Note 2 the future is 2000ms. Eventually reached the NotificationManagerService# cancelToastLocked, the code is as follows:

Void cancelToastLocked(int index) {ToastRecord record = mtoastqueue.get (index); Try {// comment 1 Record.callback.hide (); } catch (RemoteException e) { } ToastRecord lastToast = mToastQueue.remove(index); / / comment 2 mWindowManagerInternal. RemoveWindowToken (lastToast token, true, DEFAULT_DISPLAY); keepProcessAliveIfNeededLocked(record.pid); if (mToastQueue.size() > 0) { // Show the next one. If the callback fails, this will remove // it from the list, so don't assume that the list hasn't changed // after this point. showNextToastLocked(); }}Copy the code

Comment 1 calls back to Toast$TN#hide() to remove Toast’s View from Windows manager global, and comment 2 removes the token, corresponding to comment 1 in snippet 7. Also said from app NotificationManagerService notice process show Toast, give the app only 2000 ms, 2000 ms after whether app process show success, will be removed, Signal (NotificationManagerService NMS) chart is as follows:

In comment 6 back to snippet 2, ViewRootImpl#setView(), as follows:

Code snippets 10 public void setView (View View, WindowManager. LayoutParams attrs, View panelParentView) {... int res; /* = WindowManagerImpl.ADD_OKAY; */ requestLayout(); if ((mWindowAttributes.inputFeatures & WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL) == 0) { mInputChannel = new InputChannel(); } mForceDecorViewVisibility = (mWindowAttributes.privateFlags & PRIVATE_FLAG_FORCE_DECOR_VIEW_VISIBILITY) ! = 0; try { mOrigWindowType = mWindowAttributes.type; mAttachInfo.mRecomputeGlobalAttributes = true; collectViewAttributes(); adjustLayoutParamsForCompatibility(mWindowAttributes); / / comment 1 res = mWindowSession. AddToDisplayAsUser (mWindow mSeq, mWindowAttributes, getHostVisibility (), mDisplay.getDisplayId(), userId, mTmpFrame, mAttachInfo.mContentInsets, mAttachInfo.mStableInsets, mAttachInfo.mDisplayCutout, inputChannel, mTempInsets, mTempControls); setFrame(mTmpFrame); } catch (RemoteException e) { } ... if (res < WindowManagerGlobal.ADD_OKAY) { mAttachInfo.mRootView = null; mAdded = false; mFallbackEventHandler.setView(null); unscheduleTraversals(); setAccessibilityFocus(null, null); ADD_BAD_APP_TOKEN: case WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN: case WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN: case WindowManagerGlobal. throw new WindowManager.BadTokenException( "Unable to add window -- token " + attrs.token + " is not valid; is your activity running?" ); }... // comment 3 view.assignparent (this); }Copy the code

If WindowManagerGlobal.ADD_OKAY is not returned, throw an exception to abort execution. Comment 3 will not be executed when the Window token is removed or the Window token is invaid:

Void assignParent(ViewParent parent) {if (mParent == null) {mParent = parent; } else if (parent == null) { mParent = null; } else { throw new RuntimeException("view " + this + " being added, but" + " it already has a parent"); }}Copy the code

View# assignParent() assigns a value to the View’s mParent. If no View #assignParent() is called, view.getparent () returns empty, returning to code snippet 10, View#setView(), the exception thrown is caught in comment 6 of snippet 2, To catch a BadTokenException in View#setView(), execute comment 8 in snippet 2. Note that index==0, so it will not be executed WindowManagerGlobal#removeViewLocked(), the View to which the exception is added, is in the mViews of WindowManagerGlobal and parent is null, so that when comment 2 in code fragment 1 is executed, We will not execute remoView (), and then execute addView in comment 3, causing the crash of comment 4 in snippet 2.

Scene: the repetition

Combining code snippet 9 with the Toast flowchart, we know that when the system_server process pushes a Toast task out of the queue, The Window token is cleaned by executing windowManagerInternal#removeWindowToken(). If this executes the code in comment 3 of code snippet 1, the flow will go to comment 6 of code snippet 2. If Toast#show() is executed again, comment 4 in snippet 2 will be triggered, so we can assume a scenario where the app process executes Toast#show() on the main Thread because the main Thread is busy. Here we simulate thread.sleep (). Block for nearly 2000ms, and wait until the addView of the annotation 3 of the execution code fragment 1 is about to be added. After 2000ms, WindowManagerInternal# removeWindowToken(), windowManagerInternal#removeWindowToken(), windowManagerInternal#removeWindowToken(), windowManagerInternal#removeWindowToken(), windowManagerInternal#removeWindowToken()

Public class Main5Activity extends AppCompatActivity {@override protected void onCreate(@nullable Bundle) savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); ScreenUtil.fullScreen(this); } public void blockToast(View view) throws InterruptedException { ToastUtils.show(this, "blockToast"); // Annotation 1 thread.sleep (1988); } public void normalToast(View view) { ToastUtils.show(this, "normalToast"); }Copy the code

B Layout code:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" tools:context=".Main5Activity" android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center" android:orientation="vertical"> <Button android:paddingTop="20dp" Android :onClick="blockToast" android:onClick="blockToast" android:layout_height="wrap_content"/> <Button android:paddingTop="20dp" android:layout_width="wrap_content" Android :onClick="normalToast" Android :layout_height="wrap_content"/> </LinearLayout>Copy the code

The code is very simple, there are only two buttons on the interface, click to execute the blocking toast, click the normal toast for several times, and then the bug can be reproduced after a while. Note that you need to try several times, not always be able to reproduce the bug

The solution

Parent = null in WindowManaerGlobal#mView

private static void clearDying(Context context) { try { WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); Log.d("HMToast", "clearDying: " + windowManager); // Reflection gets WindowManagerLobal Field mGlobal = WindowManager.getClass ().getDeclaredField("mGlobal"); mGlobal.setAccessible(true); Log.d("HMToast", "clearDying: " + mGlobal); MViewsField = mglobal.getType ().getDeclaredField("mViews"); Field mRootsField = mGlobal.getType().getDeclaredField("mRoots"); mViewsField.setAccessible(true); mRootsField.setAccessible(true); ArrayList<View> mViews = (ArrayList<View>) mViewsField.get(mGlobal.get(windowManager)); ArrayList mRoots = (ArrayList) mRootsField.get(mGlobal.get(windowManager)); if (CollectionUtil.isEmpty(mViews)) { return; } ArrayList<View> hookViews = new ArrayList<>(mViews.size()); ArrayList hookRoots = new ArrayList<>(mViews.size()); Log.d("HMToast", "clearDying: " + mViews.size()); for (int i = 0; i < mViews.size(); If (mviews.get (I).getparent ()! = null) { hookViews.add(mViews.get(i)); hookRoots.add(mRoots.get(i)); } } Log.d("HMToast", "clearDying: hookViews" + hookViews.size()); If (mviews.size () == hookviews.size ()) {// No view return; } mViewsField.set(mGlobal.get(windowManager), hookViews); mRootsField.set(mGlobal.get(windowManager), hookRoots); } catch (Exception e) { Log.d("HMToast", "clearDying: " + e.getMessage()); e.printStackTrace(); }}Copy the code

Get WindowManaerGlobal#mView and iterate over mViews to remove parent null.

conclusion

A problem caused by Toast, such as View adding display, Window management, interprocess communication and thread blocking, is designed. These two knowledge points are just a tip of the iceberg of accumulated Android knowledge points. You are never too old to learn.