WindowManager添加view如何认证的

363 阅读4分钟

本文所有代码基于安卓9.0

一、相关的三个token

当我们调用Dialog或者PopupWindow来显示一个窗口时,对于系统是如何认证是无感知的。首先从启动一个Activity讲起,启动一个新的Activity时,系统会在创建一个在创建ActivityRecord的时候创建第一个appToken,在一系列启动步骤中会核对ActivityRecord中mWindowContainerController是否为空,由于是第一次启动自然是空的,系统会调用createWindowContainer方法来创建一个

void createWindowContainer() {
    ...
    mWindowContainerController = new AppWindowContainerController(taskController, appToken,
            this, Integer.MAX_VALUE /* add on top */, info.screenOrientation, fullscreen,
            (info.flags & FLAG_SHOW_FOR_ALL_USERS) != 0, info.configChanges,
            task.voiceSession != null, mLaunchTaskBehind, isAlwaysFocusable(),
            appInfo.targetSdkVersion, mRotationAnimationHint,
            ActivityManagerService.getInputDispatchingTimeoutLocked(this) * 1000000L);
    ...
}

在创建AppWindowContainerController的过程中会创建第二个token--AppWindowToken(Dialog向WMS添加窗口的时候使用的是该token)

public AppWindowContainerController(TaskWindowContainerController taskController,
        IApplicationToken token, AppWindowContainerListener listener, int index,
        int requestedOrientation, boolean fullscreen, boolean showForAllUsers, int configChanges,
        boolean voiceInteraction, boolean launchTaskBehind, boolean alwaysFocusable,
        int targetSdkVersion, int rotationAnimationHint, long inputDispatchingTimeoutNanos,
        WindowManagerService service) {
        ...

        atoken = createAppWindow(mService, token, voiceInteraction, task.getDisplayContent(),
                inputDispatchingTimeoutNanos, fullscreen, showForAllUsers, targetSdkVersion,
                requestedOrientation, rotationAnimationHint, configChanges, launchTaskBehind,
                alwaysFocusable, this);
         ...
}
AppWindowToken createAppWindow(WindowManagerService service, IApplicationToken token,
        boolean voiceInteraction, DisplayContent dc, long inputDispatchingTimeoutNanos,
        boolean fullscreen, boolean showForAllUsers, int targetSdk, int orientation,
        int rotationAnimationHint, int configChanges, boolean launchTaskBehind,
        boolean alwaysFocusable, AppWindowContainerController controller) {
    return new AppWindowToken(service, token, voiceInteraction, dc,
            inputDispatchingTimeoutNanos, fullscreen, showForAllUsers, targetSdk, orientation,
            rotationAnimationHint, configChanges, launchTaskBehind, alwaysFocusable,
            controller);
}

AppWindowToken是WindowToken的子类,在创建AppWindowToken的过程中会调用super方法对WindowToken进行初始化

WindowToken(WindowManagerService service, IBinder _token, int type, boolean persistOnEmpty,
        DisplayContent dc, boolean ownerCanManageAppTokens, boolean roundedCornerOverlay) {
    super(service);
    ...
    onDisplayChanged(dc);
}

接着调用onDisplayChanged

void onDisplayChanged(DisplayContent dc) {
    dc.reParentWindowToken(this);
    mDisplayContent = dc;

    // The rounded corner overlay should not be rotated. We ensure that by moving it outside
    // the windowing layer.
    if (mRoundedCornerOverlay) {
        mDisplayContent.reparentToOverlay(mPendingTransaction, mSurfaceControl);
    }

    // TODO(b/36740756): One day this should perhaps be hooked
    // up with goodToGo, so we don't move a window
    // to another display before the window behind
    // it is ready.

    super.onDisplayChanged(dc);
}

每一个屏幕对应着一个DisplayContent(简称dc),调用reParentWindowToken以ActivityRecord中的appToken作为key,AppWindowToken作为value保存到dc的mTokenMap变量中。经过N多步骤后终于走到了Activity的attach方法,在这里创建了PhoneWindow,并且将appToken传给了Window

final void attach(Context context, ActivityThread aThread,
        Instrumentation instr, IBinder token, int ident,
        Application application, Intent intent, ActivityInfo info,
        CharSequence title, Activity parent, String id,
        NonConfigurationInstances lastNonConfigurationInstances,
        Configuration config, String referrer, IVoiceInteractor voiceInteractor,
        Window window, ActivityConfigCallback activityConfigCallback) {
    ...
    mWindow.setWindowManager(
            (WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
            mToken, mComponent.flattenToString(),
            (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
    ...
}

在调用了onResume之后,ActivityThread会调用Activity的makeVisible方法从而将窗口添加到WMS。在WindowManagerGlobal的addView方法中会创建一个ViewRootImpl的实例,在初始化ViewRootImpl的过程中会创建第三个token--mWindow,并且该token会传给View.AttachInfo中,当调用View的getWindowToken会返回该token(PopopWindow向WMS添加窗口的过程中使用的就是该token)。

二、WMS如何判断添加窗口是否“合法”

在WindowManagerGlobal调用addView方法时会调用ViewRootImpl的setView方法,从而将窗口添加到WMS中。在App进程中使用Session与WMS进行交互。

public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
    synchronized (this) {
        if (mView == null) {
            ...
            try {
                mOrigWindowType = mWindowAttributes.type;
                mAttachInfo.mRecomputeGlobalAttributes = true;
                collectViewAttributes();
                res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
                        getHostVisibility(), mDisplay.getDisplayId(), mWinFrame,
                        mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
                        mAttachInfo.mOutsets, mAttachInfo.mDisplayCutout, mInputChannel);
            } catch (RemoteException e) {
                mAdded = false;
                mView = null;
                mAttachInfo.mRootView = null;
                mInputChannel = null;
                mFallbackEventHandler.setView(null);
                unscheduleTraversals();
                setAccessibilityFocus(null, null);
                throw new RuntimeException("Adding window failed", e);
            } finally {
                if (restore) {
                    attrs.restore();
                }
            }

            ...
            if (res < WindowManagerGlobal.ADD_OKAY) {//对WMS认证结果进行判断并抛出异常
                mAttachInfo.mRootView = null;
                mAdded = false;
                mFallbackEventHandler.setView(null);
                unscheduleTraversals();
                setAccessibilityFocus(null, null);
                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?");
                    case WindowManagerGlobal.ADD_NOT_APP_TOKEN:
                        throw new WindowManager.BadTokenException(
                                "Unable to add window -- token " + attrs.token
                                + " is not for an application");
                    case WindowManagerGlobal.ADD_APP_EXITING:
                        throw new WindowManager.BadTokenException(
                                "Unable to add window -- app for token " + attrs.token
                                + " is exiting");
                    case WindowManagerGlobal.ADD_DUPLICATE_ADD:
                        throw new WindowManager.BadTokenException(
                                "Unable to add window -- window " + mWindow
                                + " has already been added");
                    case WindowManagerGlobal.ADD_STARTING_NOT_NEEDED:
                        // Silently ignore -- we would have just removed it
                        // right away, anyway.
                        return;
                    case WindowManagerGlobal.ADD_MULTIPLE_SINGLETON:
                        throw new WindowManager.BadTokenException("Unable to add window "
                                + mWindow + " -- another window of type "
                                + mWindowAttributes.type + " already exists");
                    case WindowManagerGlobal.ADD_PERMISSION_DENIED:
                        throw new WindowManager.BadTokenException("Unable to add window "
                                + mWindow + " -- permission denied for window type "
                                + mWindowAttributes.type);
                    case WindowManagerGlobal.ADD_INVALID_DISPLAY:
                        throw new WindowManager.InvalidDisplayException("Unable to add window "
                                + mWindow + " -- the specified display can not be found");
                    case WindowManagerGlobal.ADD_INVALID_TYPE:
                        throw new WindowManager.InvalidDisplayException("Unable to add window "
                                + mWindow + " -- the specified window type "
                                + mWindowAttributes.type + " is not valid");
                }
                throw new RuntimeException(
                        "Unable to add window -- unknown error code " + res);
            }
           ...
        }
    }
}

最终调用到WMS的addWindow方法中。

public int addWindow(Session session, IWindow client, int seq,
        LayoutParams attrs, int viewVisibility, int displayId, Rect outFrame,
        Rect outContentInsets, Rect outStableInsets, Rect outOutsets,
        DisplayCutout.ParcelableWrapper outDisplayCutout, InputChannel outInputChannel) {
       ...
    synchronized(mWindowMap) {
       ...
        if (mWindowMap.containsKey(client.asBinder())) {//client是在ViewRootImpl中创建的token,重复提交
            Slog.w(TAG_WM, "Window " + client + " is already added");
            return WindowManagerGlobal.ADD_DUPLICATE_ADD;
        }

        if (type >= FIRST_SUB_WINDOW && type <= LAST_SUB_WINDOW) {//如果是子窗口则需要判断是否有父窗口
            parentWindow = windowForClientLocked(null, attrs.token, false);
            if (parentWindow == null) {
                Slog.w(TAG_WM, "Attempted to add window with token that is not a window: "
                      + attrs.token + ".  Aborting.");
                return WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN;
            }
            if (parentWindow.mAttrs.type >= FIRST_SUB_WINDOW
                    && parentWindow.mAttrs.type <= LAST_SUB_WINDOW) {//子窗口不能做为子窗口的父窗口
                Slog.w(TAG_WM, "Attempted to add window with token that is a sub-window: "
                        + attrs.token + ".  Aborting.");
                return WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN;
            }
        }

        if (type == TYPE_PRIVATE_PRESENTATION && !displayContent.isPrivate()) {
            Slog.w(TAG_WM, "Attempted to add private presentation window to a non-private display.  Aborting.");
            return WindowManagerGlobal.ADD_PERMISSION_DENIED;
        }

        AppWindowToken atoken = null;
        final boolean hasParent = parentWindow != null;
        // Use existing parent window token for child windows since they go in the same token
        // as there parent window so we can apply the same policy on them.
        WindowToken token = displayContent.getWindowToken(
                hasParent ? parentWindow.mAttrs.token : attrs.token);//
        // If this is a child window, we want to apply the same type checking rules as the
        // parent window type.
        final int rootType = hasParent ? parentWindow.mAttrs.type : type;

        boolean addToastWindowRequiresToken = false;

        if (token == null) {
            if (rootType >= FIRST_APPLICATION_WINDOW && rootType <= LAST_APPLICATION_WINDOW) {
                Slog.w(TAG_WM, "Attempted to add application window with unknown token "
                      + attrs.token + ".  Aborting.");
                return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
            }
            ...
            final IBinder binder = attrs.token != null ? attrs.token : client.asBinder();
            final boolean isRoundedCornerOverlay =
                    (attrs.privateFlags & PRIVATE_FLAG_IS_ROUNDED_CORNERS_OVERLAY) != 0;
            token = new WindowToken(this, binder, type, false, displayContent,
                    session.mCanAddInternalSystemWindow, isRoundedCornerOverlay);
        } else if (rootType >= FIRST_APPLICATION_WINDOW && rootType <= LAST_APPLICATION_WINDOW) {
            atoken = token.asAppWindowToken();
            if (atoken == null) {
                Slog.w(TAG_WM, "Attempted to add window with non-application token "
                      + token + ".  Aborting.");
                return WindowManagerGlobal.ADD_NOT_APP_TOKEN;
            } else if (atoken.removed) {
                Slog.w(TAG_WM, "Attempted to add window with exiting application token "
                      + token + ".  Aborting.");
                return WindowManagerGlobal.ADD_APP_EXITING;
            } 
            ...
        } 
        ...
        else if (token.asAppWindowToken() != null) {//说明该token是AppWindowTokenToken
            Slog.w(TAG_WM, "Non-null appWindowToken for system window of rootType=" + rootType);
            // It is not valid to use an app token with other system types; we will
            // instead make a new token for it (as if null had been passed in for the token).
            attrs.token = null;
            token = new WindowToken(this, client.asBinder(), type, false, displayContent,
                    session.mCanAddInternalSystemWindow);//创建新的token
        }

        final WindowState win = new WindowState(this, session, client, token, parentWindow,
                appOp[0], seq, attrs, viewVisibility, session.mUid,
                session.mCanAddInternalSystemWindow);
        ...
        mWindowMap.put(client.asBinder(), win);
        ...
    return res;
}

以上代码去除了系统相关的type。应用窗口的type得值是1到99,子窗口值是1000到1999,系统窗口值是2000到2999。大概总结一下,

  • 如果添加的窗口是子窗口,则需要判断WMS的mWindowMap是否存在key为ViewRootImpl中创建的token,如果没有或者存在的情况下如果Session也不是一个则说明attrs中带的token是个ADD_BAD_SUBWINDOW_TOKEN。如果存在但是父窗口的type也是子窗口也不行,也就是子窗口不能做子窗口的父窗口;
  • 根据attrs的token来判断是否在dc中存在该token。如果不存在且该待添加窗口类型是应用类型的则返回ADD_BAD_APP_TOKEN,如果是子窗口则创建一个WindowToken。
  • 如果attrs的token存在于dc的WindowToken,并且父窗口(存在的情况下)或者子窗口的类型是应用窗口,但是该token返回AppWindowToken为空(一个activity只有一个AppWindowToken,其余的是WindowToken),则返回ADD_NOT_APP_TOKEN,如果不为空但是被标记被移除了,说明之前添加过,返回ADD_APP_EXITING
  • 获取AppWindowToken不为空,创建一个新的WindowToken。创建WindowToken的过程中会自动以ViewRootImpl的创建的token为key,WindowToken为value保存起来。
  • 这一步中,activity之前以ActivityRecord的token为key在dc中保存了,而且attrs中的token也是该token,所以不会创建WindowToken,最后会将ViewRootImpl中创建的token为key将其保存起来。

Dialog是如何验证的

Dialog中attrs的token使用的是ActivityRecord中创建的token,而且它的类型是应用窗口,所以displayContent.getWindowToken( hasParent ? parentWindow.mAttrs.token : attrs.token)这一步中返回的不是空,也就不会创建WindowToken

PopupWindow是如何验证的

PopupWindow中使用的token是ViewRootImpl中创建的token,它的type是子窗口

public void showAtLocation(View parent, int gravity, int x, int y) {
    mParentRootView = new WeakReference<>(parent.getRootView());
    showAtLocation(parent.getWindowToken(), gravity, x, y);//获取到的是ViewRootImpl中创建的token
}
private int mWindowLayoutType = WindowManager.LayoutParams.TYPE_APPLICATION_PANEL;//子窗口类型

所以它的父窗口不为空,但是dc中并没有保存该token为key的窗口,所以会重新建一个WindowToken并添加到dc中。

Toast是如何验证的

Toast最终的显示是在NotificationManagerService中调用的。

public void enqueueToast(String pkg, ITransientNotification callback, int duration)
{
    ...
    synchronized (mToastQueue) {
        ...
        try {
            ...
            if (index >= 0) {
                ...
            } else {
                Binder token = new Binder();
                mWindowManagerInternal.addWindowToken(token, TYPE_TOAST, DEFAULT_DISPLAY);
                ...
            }
        } finally {
            Binder.restoreCallingIdentity(callingId);
        }
    }
}
public void addWindowToken(IBinder binder, int type, int displayId) {
    ...
    synchronized(mWindowMap) {
        ...
        if (type == TYPE_WALLPAPER) {
            new WallpaperWindowToken(this, binder, true, dc,
                    true /* ownerCanManageAppTokens */);
        } else {
            new WindowToken(this, binder, type, true, dc, true /* ownerCanManageAppTokens */);
        }
    }
}

最终在dc中保存了在enqueueToast方法中创建的token,所以后续添加窗口也可以通过验证

Dialog和PopupWindow使用Application作为Context传入进去能正常显示吗?

  • Dialog不可以。这是因为Dialog依赖于Window的mAppToken作为它的attrs的token,如果使用application作为参数传入进去,在Dialog中获取到的WindowManager是全局唯一的那个,跟使用Activity中返回的不是同一个。在WindowManagerGlobal的addView方法中parentWindow为空,所以最终Dialog的attrs中的token也为空,最终报错
public void addView(View view, ViewGroup.LayoutParams params,
        Display display, Window parentWindow) {
    ...
    if (parentWindow != null) {//parentWindow为空,所以没有设置attrs的token
        parentWindow.adjustLayoutParamsForSubWindow(wparams);
    } 
    ...
}
  • PopupWindow可以。这是因为PopupWindow的token依赖于anchor view,一般作为anchor的view都是当前activity中的view,而该view的getApplicationWindowToken返回的是创建Activity中ViewRootImpl创建的token,这个token在创建时已经保存到dc中去了,可以通过认证

自己可以单独添加窗口吗?

按照上述内容,可以做为token的就两个,一个是ActivityRecord中创建的token,一个是ViewRootImpl中创建的token,而第一个我们是没法直接拿到的,第二个可以通过view.getApplicationWindowToken来获取。那么type可以随便设置吗?按照前面所说,如果设为子窗口,由于没有一个AppWindowToken是以该token为key保存在dc中的,所以会报错。如果设为应用窗口是可以的。