Android senior engineer interview: Token verification of Android-Window mechanism principle (why Application Context cannot show dialog)

Overview

Note: This article is based on the Android 10 source code. For the sake of brevity, the source code may be omitted.

I saw an analytical 为什么不能使用 Application Context 显示 Dialogarticle on the Nuggets today. After reading it, I felt that the author had overlooked a very important object-parentWindow, so the source logic could not be completely stringed together when explaining. After referring to the previous analysis of the principle of Android-Window mechanism , I re-read the source code and decided to use this question to record the logic of Token verification when Android WMS is addingWindow, so as to explain why the Application Context cannot be used to display Dialog.

Android does not allow the use of Context other than Activity to display ordinary Dialog (non-System Dialog, etc.). When running the following code, an error will be reported:

val dialog = Dialog(applicationContext)
dialog.show()

// ------- error -------
android.view.WindowManager$BadTokenException: Unable to add window -- token null is not valid; is your activity running?
    at android.view.ViewRootImpl.setView(ViewRootImpl.java:840)
    at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:356)
    at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:94)
    at android.app.Dialog.show(Dialog.java:329)
    // ...

If you add a line of code, it is found that the show can be successful (note that it must window?.attributes?.tokenbe called after being assigned, you can use a delay or View.post to call dialog.show):

val dialog = Dialog(applicationContext)
dialog.window?.attributes?.token = window?.attributes?.token
dialog.show()

Next, analyze the logic related to Token verification from the perspective of source code. Before starting the content of this part, it is best to have a certain understanding of the startActivity startup source code and the principles of the Window mechanism. Here, we will sort out the related processes. As can be seen from the Android-Activity startup process , the token-related steps in the startActivity process are briefly described as follows:

The App process calls the Context.startActivity method, and then gives it to AMS (system_server process) to do some processing. The following Token creation is done here. In addition, if the Activity that needs to be started is a new process, then system_server will initiate to zygote A request to create a new process. After the target process is successfully created, the logic is transferred from AMS to the target Activity process. The ActivityThread of the target process will call the performLaunchActivity method to create the target Activity instance, and then call the Activity.attach method. The following WindowManager object is Created here, the life cycle method of Activity will be called back one after another, in which a DecorView object is created in setContentView of onCreate, and then after the onResume callback is completed, the DecorView object will be added through WindowManager.addView (see Android-Window mechanism principle , Called through Binder, completed with WMS).

After understanding this process in general (the process above is simplified, if you don’t want to read the source code of startActivity, you can remember the process above, which will be used in the next analysis), let’s take a look at how Token goes in each stage work. For Binder-related information, please refer to here: Android-Binder principle series , in short, the Binder IPC method is a C/S architecture. The server and client processes hold Binder references and agents, and they can cross processes. transfer.

The final summary part will output this process as a flow chart. If you have any questions, please leave a message to correct me!

Token creation

According to the above process, let’s start with AMS and take a look at how Token is created. If you have read the source code of Activity startup, you can know that there are the following codes in the ActivityStarter.startActivity method (the AMS thread in the system_server process at this time, and WMS has different threads in the same process, please refer to Android-init-zygote ):

// ActivityStarter
private int startActivity(/*...*/) {
    // ...
    ActivityRecord r = new ActivityRecord(mService, callerApp, callingPid, callingUid,
            callingPackage, intent, resolvedType, aInfo, mService.getGlobalConfiguration(),
            resultRecord, resultWho, requestCode, componentSpecified, voiceSession != null,
            mSupervisor, checkedOptions, sourceRecord);
    // ...
}

Then we look at the ActivityRecord class:

final class ActivityRecord extends ConfigurationContainer implements AppWindowContainerListener {

    // Binder 服务端对象
    static class Token extends IApplicationToken.Stub {
        // 持有外部 ActivityRecord 的弱引用
        private final WeakReference<ActivityRecord> weakActivity;
        private final String name;

        Token(ActivityRecord activity, Intent intent) {
            weakActivity = new WeakReference<>(activity);
            name = intent.getComponent().flattenToShortString();
        }
        // ...
    }

    ActivityRecord(/*...*/) {
        appToken = new Token(this, _intent);
        // ...
    }
}

Therefore, in the startActivity process, the appToken in the ActivityRecord object is instantiated. Then go down again, to the ActivityStack.startActivityLocked method:

void startActivityLocked(ActivityRecord r, ActivityRecord focusedTopActivity,
        boolean newTask, boolean keepCurTransition, ActivityOptions options) {
    // ...
    r.createWindowContainer();
    // ...
}

// ActivityRecord
void createWindowContainer() {
    mWindowContainerController = new AppWindowContainerController(taskController, appToken, /*...*/);
    // ...
}

// AppWindowContainerController
public AppWindowContainerController(TaskWindowContainerController taskController, IApplicationToken token, /*...*/) {
    atoken = createAppWindow(mService, token, /*...*/);
}

AppWindowToken createAppWindow(WindowManagerService service, IApplicationToken token, /*...*/) {
    return new AppWindowToken(service, token,  /*...*/);
}

// AppWindowToken --> WindowToken
AppWindowToken(WindowManagerService service, IApplicationToken token,  /*...*/) {
    super(service, token != null ? token.asBinder() : null, TYPE_APPLICATION, /*...*/);
    appToken = token;
    mVoiceInteraction = voiceInteraction;
    mFillsParent = fillsParent;
    mInputApplicationHandle = new InputApplicationHandle(this);
}

// WindowToken
WindowToken(WindowManagerService service, IBinder _token, int type, /*...*/) {
    super(service);
    token = _token;
    windowType = type;
    mPersistOnEmpty = persistOnEmpty;
    mOwnerCanManageAppTokens = ownerCanManageAppTokens;
    mRoundedCornerOverlay = roundedCornerOverlay;
    onDisplayChanged(dc);
}

void onDisplayChanged(DisplayContent dc) {
    dc.reParentWindowToken(this);
    // ...
}

// DisplayContent
void reParentWindowToken(WindowToken token) {
    // ...
    addWindowToken(token.token, token);
}

private void addWindowToken(IBinder binder, WindowToken token) {
    // HashMap<IBinder, WindowToken> mTokenMap
    // key--ActivityRecord.Token(IApplicationToken.Stub); value--WindowToken
    mTokenMap.put(binder, token);
    // ...
}

The above code only gives the key steps. It can be clearly seen that the client process calls startActivity to start an Activity, and then creates an IApplicationToken.Stub object in the AMS (AMS thread of the system_server process) processing flow , This is a Binder server, and then an AppWindowToken object is created and stored in DisplayContent.mTokenMap. Here, both AMS and WMS are in the system_server process, and mTokenMap will be used in subsequent WMS.addWindow to verify Token (here on whether mTokenMap has thread safety issues, if you are interested, you can take a closer look at the details).

WindowManager object acquisition

Then look at the difference between using the Activity Context to call the getSystemService method and using the Application Context to call the getSystemService method (only for WMS services):

// Activity
public Object getSystemService(@ServiceName @NonNull String name) {
    if (WINDOW_SERVICE.equals(name)) {
        return mWindowManager;
    }
    // ...
}

// Application 调用的是父类 ContextImpl 的方法
// ContextImpl
public Object getSystemService(String name) {
    return SystemServiceRegistry.getSystemService(this, name);
}

// SystemServiceRegistry
registerService(Context.WINDOW_SERVICE, WindowManager.class, new CachedServiceFetcher<WindowManager>() {
    @Override
    public WindowManager createService(ContextImpl ctx) {
        return new WindowManagerImpl(ctx);
    }});

What is obtained in Activity is the mWindowManager object, which is assigned in the Activity.attach method. From the Android-Activity startup principle, it can be known that this method is called back during the startActivity process (after AMS processing, the method of calling the target Activity through the Binder):

// Activity
final void attach(/*...*/) {
    attachBaseContext(context);
    mWindow = new PhoneWindow(this, window, activityConfigCallback);
    mWindow.setWindowManager((WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
        mToken, mComponent.flattenToString(), (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
    mWindowManager = mWindow.getWindowManager();
    // ...
}

// Window
public void setWindowManager(WindowManager wm, IBinder appToken, String appName,
        boolean hardwareAccelerated) {
    mAppToken = appToken;
    mAppName = appName;
    if (wm == null) {
        wm = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE);
    }
    mWindowManager = ((WindowManagerImpl)wm).createLocalWindowManager(this);
}

// WindowManagerImpl
public WindowManagerImpl createLocalWindowManager(Window parentWindow) {
    return new WindowManagerImpl(mContext, parentWindow);
}

public WindowManagerImpl(Context context) {
    this(context, null);
}

private WindowManagerImpl(Context context, Window parentWindow) {
    mContext = context;
    mParentWindow = parentWindow;
}

From the above source code, we can see that the difference between using the Activity's Context to call the getSystemService method and using the Application's Context to call the getSystemService method is: the parentWindow in the WindowManager object in the Activity is the PhoneWindow object in the Activity, and the parentWindow in the WindowManager object in the Application is null .

As for the mToken object above (this is the mToken of the client process, which is different from the Token object created on the AMS side above!) where it comes from, you can look at the call of Activity.attach:

// ActivityThread
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
    activity.attach(appContext, this, getInstrumentation(), r.token, /*...*/);
}

You can see mToken objects are ActivityClientRecord.token, note that at this time we live in is the process where the target Activity, directly from the start Activity parse the source code can know that this is ActivityClientRecord.token AMS in ActivityRecord.token of Binder agent specific The object transfer code is no longer posted, and it looks boring to post too much code. If you want to see here, you can directly refer to the previous blog.

All in all, the mAppToken in the target Activity process is a Binder proxy object, and its Binder server is the Token object (IApplicationToken.Stub) in the ActivityRecord of AMS.

WindowManager.addView

Then we have reached the process of WindowManager.addView adding DecorView. At this time, Activity has just started and the interface is not yet visible.

// WindowManagerImpl
private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();

@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
    mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
}

// WindowManagerGlobal.java
public void addView(View view, ViewGroup.LayoutParams params, Display display, Window parentWindow) {
    final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;
    if (parentWindow != null) {
        // 由上面可知在Activity中的WindowManager里,parentWindow是PhoneWindow对象
        parentWindow.adjustLayoutParamsForSubWindow(wparams);
    }
    // ...
    ViewRootImpl root = new ViewRootImpl(view.getContext(), display);
    // ...
    root.setView(view, wparams, panelParentView);
}

The above code is in the client process where the Activity is located. Since parentWindow is not empty, it is a PhoneWindow object, so take a look at the Window.adjustLayoutParamsForSubWindow method:

// Window
void adjustLayoutParamsForSubWindow(WindowManager.LayoutParams wp) {
    if (wp.type >= WindowManager.LayoutParams.FIRST_SUB_WINDOW &&
        wp.type <= WindowManager.LayoutParams.LAST_SUB_WINDOW) {
        // ...
    } else if (wp.type >= WindowManager.LayoutParams.FIRST_SYSTEM_WINDOW &&
        wp.type <= WindowManager.LayoutParams.LAST_SYSTEM_WINDOW) {
        // ...
    } else {
        // 由于这是Application级别的window,因此走这个流程
        if (wp.token == null) {
            wp.token = mContainer == null ? mAppToken : mContainer.mAppToken;
        }
    }
}

You can see that LayoutParams.token takes the Binder proxy of the above Token object on the client . Remember here, the following will be used. Next, take a look at the related logic of ViewRootImpl.setView:

// ViewRootImpl
public ViewRootImpl(Context context, Display display) {
    mContext = context;
    // 继承于IWindow.Stub的W对象
    mWindow = new W(this);
    mAttachInfo = new View.AttachInfo(mWindowSession, mWindow, display, this, mHandler, this, context);
    // ...
}

// View.AttachInfo
AttachInfo(IWindowSession session, IWindow window, Display display,
        ViewRootImpl viewRootImpl, Handler handler, Callbacks effectPlayer,
        Context context) {
    mWindow = window;
    mWindowToken = window.asBinder();
    mViewRootImpl = viewRootImpl;
    // ...
}

public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
    synchronized (this) {
        if (mView == null) {
            mView = view;
            mWindowAttributes.copyFrom(attrs);
            // 通过mWindowSession会调用到WMS.addWindow
            res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
                    getHostVisibility(), mDisplay.getDisplayId(), mWinFrame,
                    mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
                    mAttachInfo.mOutsets, mAttachInfo.mDisplayCutout, mInputChannel);
            // ...
        }
    }
}

You can pay attention to the View.AttachInfo construction method above, but also use it below. This class represents the attach information of View. Then comes the logic of WMS:

public int addWindow(Session session, IWindow client, int seq, LayoutParams attrs, /*...*/) {
    synchronized(mWindowMap) {
        AppWindowToken atoken = null;
        final boolean hasParent = parentWindow != null;
        // 这个逻辑先不看,在后面Dialog添加再说
        WindowToken token = displayContent.getWindowToken(hasParent ? parentWindow.mAttrs.token : attrs.token);
        // 创建WindowState实例
        final WindowState win = new WindowState(this, session, client, token, parentWindow,
            appOp[0], seq, attrs, viewVisibility, session.mUid, session.mCanAddInternalSystemWindow);
        mWindowMap.put(client.asBinder(), win);
        // ...
    }
}

The client here is the Binder server-mWindow in the ViewRootImpl above. The above code only posted the relevant logic, the process of adding Window in the startActivity process can only be seen here. The WindowToken tokenobject in the WMS.addWindow method is used for inspection, and here is the Dialog crash that will be discussed later!

Dialog.show

In the above, I roughly talked about the process of adding DecorView after Activity is started. Then we can start to study what happens after Dialog calls the show method, and its relationship with Token and Activity/Application.

Dialog(@NonNull Context context, @StyleRes int themeResId, boolean createContextThemeWrapper) {
    mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
    final Window w = new PhoneWindow(mContext);
    mWindow = w;
    // ...
}

public void show() {
    onStart();
    // ...
    mDecor = mWindow.getDecorView();
    mWindowManager.addView(mDecor, l);
    mShowing = true;
}

It can be seen that the WM.addView method is also called. So you can continue to look at the WindowManagerGlobal.addView method given above, here are two cases:

  • Passed to Dialog is the Activity context, the parentWindow of WindowManager is not empty
  • Passed to Dialog is the Application context, the parentWindow of WindowManager is empty

We already know that, depending on whether parentWindow is empty, will choose whether to call its parentWindow.adjustLayoutParamsForSubWindow(wparams)methods:

// Window
void adjustLayoutParamsForSubWindow(WindowManager.LayoutParams wp) {
    if (wp.type >= WindowManager.LayoutParams.FIRST_SUB_WINDOW &&
            wp.type <= WindowManager.LayoutParams.LAST_SUB_WINDOW) {
        // 这里是普通对话框,因此走这个流程
        if (wp.token == null) {
            View decor = peekDecorView(); // 通过PhoneWindow拿到DecorView对象
            if (decor != null) {
                wp.token = decor.getWindowToken();
            }
        }
    }
    // ...
}

// View
public IBinder getWindowToken() {
    return mAttachInfo != null ? mAttachInfo.mWindowToken : null;
}

The getWindowToken method above returns the mAttachInfo.mWindowToken object we saw earlier! That is, the mWindow object (a Binder server) created in ViewRootImpl before. And if the Context is the Application, it wp.tokenwill be null.

WMS.addView

So we continue to watch the performance of WMS in the process of showing Dialog:

public int addWindow(Session session, IWindow client, int seq, LayoutParams attrs, /*...*/) {
    WindowState parentWindow = null;
    if (type >= FIRST_SUB_WINDOW && type <= LAST_SUB_WINDOW) {
        // 普通Dialog会走这个流程,获取parentWindow对象
        parentWindow = windowForClientLocked(null, attrs.token, false);
        if (parentWindow == null) {
            // parentWindow为null,返回bad
            return WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN;
        }
        if (parentWindow.mAttrs.type >= FIRST_SUB_WINDOW && parentWindow.mAttrs.type <= LAST_SUB_WINDOW) {
            // parentWindow为普通window,返回bad
            return WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN;
        }
    }
    // ...
}

final WindowState windowForClientLocked(Session session, IBinder client, boolean throwOnError) {
    WindowState win = mWindowMap.get(client);
    // ...
    return win;
}

Let's take a look at the logic of parentWindow first. The client parameter in the windowForClientLocked method is the one mentioned in the previous section wp.token:

  • If Context is Application, if it is null, then the parentWindow returned in WMS is also null, then adding Window fails and returns a bad code.
  • Context is the case of Activity. It is the Binder proxy of the mWindow object in ViewRootImpl. In the process of parsing startActivity and adding DecorView above, we saw that we added a key to mWindowMap as the mWindow object proxy, and the value is the WindowState object created at that time. Will be returned as the parentWindow this time.

We then look down:

public int addWindow(Session session, IWindow client, int seq, LayoutParams attrs, /*...*/) {
    // ...
    AppWindowToken atoken = null;
    final boolean hasParent = parentWindow != null;
    // 取parentWindow.mAttrs.token
    WindowToken token = displayContent.getWindowToken(hasParent ? parentWindow.mAttrs.token : attrs.token);
    if (token == null) {
        if (rootType >= FIRST_APPLICATION_WINDOW && rootType <= LAST_APPLICATION_WINDOW) {
            return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
        }
        if (rootType == TYPE_INPUT_METHOD) {
            return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
        }
    }
    // ...
}

// DisplayContent
WindowToken getWindowToken(IBinder binder) {
    // key--ActivityRecord.Token(IApplicationToken.Stub); value--WindowToken
    return mTokenMap.get(binder);
}

For the case where the incoming is Activity, because the parentWindow is not empty, it can be seen that hasParent = true. From the above, it can be known that the LayoutParams.token of parentWindow takes the Binder proxy of the Token object created by AMS on the client side, and it has been added to mTokenMap long ago The element of this key ! Therefore, if it is Activity, the returned token has a value, and its value is the AppWindowToken object created by AMS.

You should now know, why in the beginning we added this line of code dialog.window?.attributes?.token = window?.attributes?.tokenlater, Dialog can be a normal show! Because we manually set the token for Dialog itself, the token value is the mAppToken (proxy) created when the Activity is started.

Exception thrown

After the above WMS returns, return to the ViewRootImpl.setView method:

public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
    synchronized (this) {
        if (mView == null) {
            res = mWindowSession.addToDisplay(/*...*/)
            if (res < WindowManagerGlobal.ADD_OKAY) {
                switch (res) {
                    case WindowManagerGlobal.ADD_BAD_APP_TOKEN:
                    case WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN:
                        throw new WindowManager.BadTokenException(
                            "Unable to add window -- token " + attrs.token
                            + " is not valid; is your activity running?");
                    // ...
                }
            }
            // ...
        }
    }
}

Seeing this, we finally found out what happened to the crash log we saw at the beginning.

to sum up

Use a picture to summarize the Token verification process (a new requirement came in hand, and the time is rushed. If there is a problem with the flow chart, please correct me. If there is a problem with the previous analysis, please point out and correct it! If you think it is good, you can more Like~):

Interview review notes:

This information will be published in various blogs and forums starting from the spring recruitment. Collect high-quality intermediate and advanced interview questions for Android development on the website, and then find the best solution for the whole network. Every interview question is 100% real questions + best answers. Package knowledge + many details.
Save everyone's time to search for information on the Internet to learn, you can also share with friends around to learn together.
Leave a small like for the article and you can receive it for free~

Poke me to receive: Android online brutality interview guide , super hard core Android interview knowledge notes , 3000 pages of core knowledge notes for Android developer architects

"960 pages of Android development notes"

"1307-page Android Development Interview Collection"

Including Tencent, Baidu, Xiaomi, Ali, LeTV, Meituan, 58, Cheetah, 360, Sina, Sohu and other first-line Internet companies interviewed questions. Familiarity with the knowledge points listed in this article will greatly increase the chance of passing the first two rounds of technical interviews.

"507 pages of Android development related source code analysis"

As long as it is a programmer, whether it is Java or Android, if you do not read the source code and only look at the API documentation, it will just stay on the skin, which is not good for the establishment and completeness of our knowledge system and the improvement of actual combat technology.

The one who can really exercise the ability is to read the source code directly, not only reading the source code of major systems, but also including various excellent open source libraries.

The information has been uploaded on my GitHub ; quick start channel: ( click here ) to download! Full of sincerity! ! !

I heard that all the fans of the one-click three consecutive interviews succeeded? If this blog is helpful to you, please support the editor

Quick start channel: ( click here ) to download! Full of sincerity! ! !

Android Advanced Interview Selected Questions, Architect Advanced Practical Document Portal: My GitHub

It is not easy to organize, friends who feel helpful can help like, share and support the editor~

Your support, my motivation; I wish you all a bright future and constant offers! ! !

Guess you like

Origin blog.csdn.net/Androiddddd/article/details/110195969