再读Activity启动

394 阅读7分钟

为什么要再读Activity的启动呢,因为在启动弹窗的时候,出现了一个这个报错:

Process: com.librty.crashsolution, PID: 7037 android.view.WindowManager$BadTokenException:   Unable to add window -- token null is not valid; is your activity running? 
at android.view.ViewRootImpl.setView(ViewRootImpl.java:1189) 
at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:400) 
at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:138)

里面个讲到一个window.token值。由于这个值的不正确导致的。
首先需要找到抛出异常的位置:在ViewRootImpl的setView阶段:

class ViewRootImpl{
    public void setView(View view,WindowManager.LayoutParams attrs,View panelParentView,int userId){
    ...
    int res = mWindowSession.addToDisplayAsUser(mWindow, mWindowAttributes,
            getHostVisibility(), mDisplay.getDisplayId(), userId,
            mInsetsController.getRequestedVisibilities(), inputChannel, mTempInsets,
            mTempControls);

    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?");
            case WindowManagerGlobal.ADD_NOT_APP_TOKEN:
                throw new WindowManager.BadTokenException(
                        "Unable to add window -- token " + attrs.token
                        + " is not for an application");

        }
    }
    ...
    }
}

可以看到res的值是通过mWindowSession.addToDisplayerAsUser方法获取到的。
获取过程如下:
第一步:搞明白mWindowSession对象的获取过程

class ViewRootImpl{
    final IWindowSession mWindowSession;
    public ViewRootImpl(Context context, Display display) {
        this(context, display, WindowManagerGlobal.getWindowSession(),
                false);
    }
    public ViewRootImpl(Context cotext,Display display,IWindowSession session,boolean useSfChoreographer){
        mWindowSession = sesssion;
    }
}

可以看到mWindowSession是通过WindowManagerGlobal.getWindowSession()方法获取的。可以看到这里是通过Binder通信去获取WindowManagerService服务。

public static IWindowSession getWindowSession() {
    synchronized (WindowManagerGlobal.class) {
        if (sWindowSession == null) {
            try {
              @UnsupportedAppUsage  InputMethodManager.ensureDefaultInstanceForDefaultDisplayIfNecessary();
                IWindowManager windowManager = getWindowManagerService();
                sWindowSession = windowManager.openSession(
                        new IWindowSessionCallback.Stub() {
                            @Override
                            public void onAnimatorScaleChanged(float scale) {
                                ValueAnimator.setDurationScale(scale);
                            }
                        });
            } catch (RemoteException e) {
                throw e.rethrowFromSystemServer();
            }
        }
        return sWindowSession;
    }
}

class WindowManagerService extends IWindowManager.Stub
        implements Watchdog.Monitor, WindowManagerPolicy.WindowManagerFuncs{
    public IWindowSession openSession(IWindowSessionCallback callback) {
        return new Session(this, callback);
    }
}

可以看到iWindowSession其实就是Session对象。

第二步调用addToDisplayAsUser方法

class Session{
    final WindowManagerService mService;
    public int addToDisplayAsUser(IWindow window, WindowManager.LayoutParams attrs,
            int viewVisibility, int displayId, int userId, InsetsVisibilities requestedVisibilities,
            InputChannel outInputChannel, InsetsState outInsetsState,
            InsetsSourceControl[] outActiveControls) {
        return mService.addWindow(this, window, attrs, viewVisibility, displayId, userId,
                requestedVisibilities, outInputChannel, outInsetsState, outActiveControls);
    }
}

可以看到又调用了WindowManagerService的addWindow方法:

class WindowManagerService{
    public int addWindow(Session session, IWindow client, LayoutParams attrs, int viewVisibility,
            int displayId, int requestUserId, InsetsVisibilities requestedVisibilities,
            InputChannel outInputChannel, InsetsState outInsetsState,
            InsetsSourceControl[] outActiveControls) {
    }
}

这里就引出了很重要的一个服务WindowManagerService,它管理了当前系统中的所有window窗口,他主管了设备产生的touch事件,因为windowmanagerservice知道当前哪个window最适合处理事件。

来看下WindowmanagerService里面所有的窗口是如何管理的:

5.jpg

现在继续看addWindow整个过程干了什么:

class WindowManagerService{
    public static final int FIRST_SUB_WINDOW = 1000;
    public static final int FIRST_SYSTEM_WINDOW = 2000;
    public int addWindow(Session session, IWindow client, LayoutParams attrs, int viewVisibility,
            int displayId, int requestUserId, InsetsVisibilities requestedVisibilities,
            InputChannel outInputChannel, InsetsState outInsetsState,
            InsetsSourceControl[] outActiveControls) {
        Arrays.fill(outActiveControls, null);
        int[] appOp = new int[1];
        final boolean isRoundedCornerOverlay = (attrs.privateFlags
                & PRIVATE_FLAG_IS_ROUNDED_CORNERS_OVERLAY) != 0;
        // 第一步 : 进行权限检查
        int res = mPolicy.checkAddPermission(attrs.type, isRoundedCornerOverlay, attrs.packageName,
                appOp);
        if (res != ADD_OKAY) {
            return res;
        }
        // 第二步:获取 Dispay
        final DisplayContent displayContent = getDisplayContentOrCreate(displayId, attrs.token);
        if (displayContent == null) {
                ProtoLog.w(WM_ERROR, "Attempted to add window to a display that does "
                        + "not exist: %d. Aborting.", displayId);
                return WindowManagerGlobal.ADD_INVALID_DISPLAY;
            }
        // 获取窗口类型
        final int type = attrs.type;
        //第三步: 1000 2000
        if (type >= FIRST_SUB_WINDOW && type <= LAST_SUB_WINDOW) {
            parentWindow = windowForClientLocked(null, attrs.token, false);
            if (parentWindow == null) {
                ProtoLog.w(WM_ERROR, "Attempted to add window with token that is not a window: "
                        + "%s.  Aborting.", attrs.token);
                return WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN;
            }
            if (parentWindow.mAttrs.type >= FIRST_SUB_WINDOW
                    && parentWindow.mAttrs.type <= LAST_SUB_WINDOW) {
                ProtoLog.w(WM_ERROR, "Attempted to add window with token that is a sub-window: "
                        + "%s.  Aborting.", attrs.token);
                return WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN;
            }
            
        // 避免窗口的重复添加
        if (mWindowMap.containsKey(client.asBinder())) {
            ProtoLog.w(WM_ERROR, "Window %s is already added", client);
            return WindowManagerGlobal.ADD_DUPLICATE_ADD;
        }
        if (type >= FIRST_SUB_WINDOW && type <= LAST_SUB_WINDOW) {
            parentWindow = windowForClientLocked(null, attrs.token, false);
            if (parentWindow == null) {
                ProtoLog.w(WM_ERROR, "Attempted to add window with token that is not a window: "
                        + "%s.  Aborting.", attrs.token);
                return WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN;
            }
            if (parentWindow.mAttrs.type >= FIRST_SUB_WINDOW
                    && parentWindow.mAttrs.type <= LAST_SUB_WINDOW) {
                ProtoLog.w(WM_ERROR, "Attempted to add window with token that is a sub-window: "
                        + "%s.  Aborting.", attrs.token);
                return WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN;
            }
        }
 
        }
    }
}

第一步: 进行权限检查。看你这个类型的window能够进行创建。
第二步: 获取Display。Display就是屏幕的意思,手机的可显示区域会通过displayId进行标记,每个displayId对应一个display,然后display会被保存在DisplayContent里面。到了这里就要讲到上面的关于token产生的报错问题了。

dd.jpg

在图上可以看到通过token值从DisplayContent里面获取WindowToken值。windowToken是一组窗口的编号,比如我们在activity里面弹出了一个dialog,那么这个时候dialog的windowtoken和activity的token的值就是一样的,因为dialog是activity的子窗口,他们属于一组窗口,所以windowtoken的值是一样的。

1669356626268.jpg这个token是activity的。 1669356765007.jpg这个token是activity上弹的dialog弹窗。

从我们的实验结果能发现,属于一组窗口的时候他们的windowToken的值是一样的。什么叫一组窗口,就是存在父子关系的,比如activity和他的dialog。两个activity间就不是一组窗口,因此他们的windowtoken也不一样。

lQDPJxbn5v0At5XNAtvNBRuwT8cA5lJ9efEDfQPYo8A2AA_1307_731.jpg 在WindowManagerservice.addWindow()里面会为窗口每次去创建WindowState对象,每个windowSate都保存了windowToken值,所以间接的,通过windowToken管理了一组windowState,是怎么实现的呢?
WindowToken继承自WindowContainer,然后在WindowContainer里面有一个WindowList数组,这个数组就存下了相同token下的所有窗口。

captrue.png lQDPJxbqOR7xeyDNAbrNAxqwqPAI_ypMD0EDgNFGKkDOAA_794_442.jpg

captrue.png

captrue.png 可以看到一组相同的windowToken会通过windowList保存所有的windowState。 5.jpg 经过这么一通分析,上面这张图已经搞懂了WindowToken和WidowState的关系了。但是我现在还有一个疑问这个WindowToken的值是从哪来的?继续分析.....

lQDPJxbqOv9bUtnNAnDNA16ws9drjwKi7p8DgNRZXMBwAA_862_624.jpg

captrue.png WindowToken是通过IBinder值从DisplayContent获取到的。然后IBinder值是通过保存在WindowManager.LayoutParams里的token值来获取的。那现在就一步步往上追,这个WindowManager.LayoutParams.token的赋值时机:

captrue.png

① 调用者Session:

captrue.png

② 调用者ViewRootImpm.setView

captrue.png

③ 调用者WindowManagerGlobal.addView:

captrue.png

④ 调用者WindowManagerImpl.addView:

captrue.png

⑤ 调用者ActivityThread.handleResumeActivity:

captrue.png

现在分析每步干了什么:
⑤的位置: 首先获取到一个空的LayoutParams,因为在我们每次创建Window对象的时候,都会立马创建一个空的WindManager.LayoutParams对象。然后在⑤位置给我们的window设置了type值WindowManager.LayoutParams.TYPE_BASE_APPLICATION这个常量值是1也就代表这是一个应用窗口。

④的位置: 没干任何事情。直接就掉用了WindowManagerGlobal.addView方法。

③的位置: 对token进行赋值

captrue.png 这里的parentWindow并不为空,因为在Activity的attach阶段,创建了我们的windowManagetImpl对象

103.png

captrue.png

现在看回adjustLayoutParamsForSubWindow:

class Window{

    void adjustLayoutParamsForSubWindow(WindowManager.LayoutParams wp) {
        CharSequence curTitle = wp.getTitle();
        // 不符合这里1000 - 1999
        if (wp.type >= WindowManager.LayoutParams.FIRST_SUB_WINDOW &&
                wp.type <= WindowManager.LayoutParams.LAST_SUB_WINDOW) {
            if (wp.token == null) {
                View decor = peekDecorView();
                if (decor != null) {
                    wp.token = decor.getWindowToken();
                }
            }
            if (curTitle == null || curTitle.length() == 0) {
                final StringBuilder title = new StringBuilder(32);
                if (wp.type == WindowManager.LayoutParams.TYPE_APPLICATION_MEDIA) {
                    title.append("Media");
                } else if (wp.type == WindowManager.LayoutParams.TYPE_APPLICATION_MEDIA_OVERLAY) {
                    title.append("MediaOvr");
                } else if (wp.type == WindowManager.LayoutParams.TYPE_APPLICATION_PANEL) {
                    title.append("Panel");
                } else if (wp.type == WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL) {
                    title.append("SubPanel");
                } else if (wp.type == WindowManager.LayoutParams.TYPE_APPLICATION_ABOVE_SUB_PANEL) {
                    title.append("AboveSubPanel");
                } else if (wp.type == WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG) {
                    title.append("AtchDlg");
                } else {
                    title.append(wp.type);
                }
                if (mAppName != null) {
                    title.append(":").append(mAppName);
                }
                wp.setTitle(title);
            }
        } //这也不符合2000 - 2999
        else if (wp.type >= WindowManager.LayoutParams.FIRST_SYSTEM_WINDOW &&
                wp.type <= WindowManager.LayoutParams.LAST_SYSTEM_WINDOW) {
            if (curTitle == null || curTitle.length() == 0) {
                final StringBuilder title = new StringBuilder(32);
                title.append("Sys").append(wp.type);
                if (mAppName != null) {
                    title.append(":").append(mAppName);
                }
                wp.setTitle(title);
            }
        } //进到这里因为应用级别的type 1 - 999
        else {
            if (wp.token == null) {
            //对于我们的activity这个mContainer是null
                wp.token = mContainer == null ? mAppToken : mContainer.mAppToken;
            }
            if ((curTitle == null || curTitle.length() == 0)
                    && mAppName != null) {
                wp.setTitle(mAppName);
            }
        }
        if (wp.packageName == null) {
            wp.packageName = mContext.getPackageName();
        }
        if (mHardwareAccelerated ||
                (mWindowAttributes.flags & FLAG_HARDWARE_ACCELERATED) != 0) {
            wp.flags |= FLAG_HARDWARE_ACCELERATED;
        }
    }
}

到这里就结束了,LayoutParams该设置的值都设置结束了。

标记1的地方可以看到WindowManagerService有一个成员变量RootWindowContainer。我们都知道WindowManagerService可以管理所有窗口,这就是答案所在。在RootWindowContainer里面有一个数组专门用来保存创建的窗口。

class RootWindowContainer{
protected final WindowList<E> mChildren = new WindowList<E>();
}

标记2的地方,通过token值从RootWindowContainer里面获取对应存在的DisplayContent。在我们创建一个activity的时候,我可以给你讲,这个token的值是null。token的赋值操作可以通过获取Widow的WindowManager.LayoutParams直接进行赋值的:

val p = WindowManager.LayoutParams()
p.flags = WindowManager.LayoutParams.FLAG_SPLIT_TOUCH
p.type = WindowManager.LayoutParams.TYPE_APPLICATION_PANEL
p.token = applicationContext.getActivityToken()
var mWindowManager = getSystemService(WINDOW_SERVICE) as WindowManager
mWindowManager.addView(view, p)

标记3:通过token获取不到值的时候,就通过displayId来获取,

class RootWindowContainer{
    DisplayContent getDisplayContentOrCreate(int displayId) {
    // 从WindowList里面取,看能不能取到
        DisplayContent displayContent = getDisplayContent(displayId);
        if (displayContent != null) {
            return displayContent;
        }
        if (mDisplayManager == null) {
            return null;
        }
        //先获取display
        final Display display = mDisplayManager.getDisplay(displayId);
        if (display == null) {
            return null;
        }
        //创建一个和display对应的displaycontent对象。
        displayContent = new DisplayContent(display, this);
        //保存到windowlist里面
        addChild(displayContent, POSITION_BOTTOM);
        return displayContent;
    }
}

其实看到这里我又不懂了,这个displayId是什么,Display又是什么?我没搞懂 (2022年11月24日,今天我搞懂了。)

在android里面display就是一个可以展示的屏幕,displayid就是标记是哪个屏幕。surfaceflinger就是通过displayid将合成的图片分发给指定的屏幕。在我们手机只有一个屏幕的时候这个dispalyid就是0.如果有两个屏幕分别为0 和 1(就是这么个意思,也可能不是0 1。,反正就是两个不同的数字)。有一个屏幕就有一个display对象,有两个屏幕就有两个display对象。

5.jpg

第三步: 当type大于1000,小于1999,说明这个窗口是一个子窗口。如果是子窗口就会在第三步去找到他的父窗口也就是代码里面的parentWindow,activity是应用窗口所以在第三步不会去找他的父窗口。Dialog是一个子窗,这个时候会去找他的父窗口,现在来研究下是怎么找的。

class WindowManagerService{
    parentWindow = windowForClientLocked(null, attrs.token, false);
}

可以看到在第二步的时候去判断了窗口类型。

看到这里总结一下:在创建ViewRootImpl对象的时候,会去获取WindowManagerService服务去创建Session对象。然后调用session对象的addToDisplayAsUser方法,他的返回值结果用来表示窗口添加结果情况情况。


现在来总结下全部过程,在ActivityThread的performLaunchActivity阶段会去创建Activity对象,并调用Activity的attach方法。在attach里面会去创建PhoneWindow对象。然后给当前创建的PhoneWindow创建对应的WindowManagerImpl对象,通过方法window.setWindowManager()。创建WindowManagerImpl对象。PhoneWindow持有WindowManagerImpl的引用。WindowManagerImpl也持有window的引用。在这个阶段还涉及了一个很重的参数token:IBinder。每个Activity都有一个对应的token:IBinder。

class Activtiy{
    public void attach(){
        mWindow = PhoneWindow();
        mWindow.setWindowManager((WindowManager)context.getSystemService(Context.WINDOW_SERVICE),mToken,mComponent.flattenToString(),(info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED)!=0);
    }
}
class Window{
    public void setWindowManager(WindowManager wm,IBinder appToken,String appName,boolean hardwareAccelerated){
        mWindowManager = ((WindowManagerImpl)wm).createLocalWindowManager(this);
    }
}
class WindowManagerImpl{
    public WindowManagerImpl crateLocalWindowManager(Window parentWindow){
            return new WindowManagerImpl(mContext,parentWindow,mWindowContextToken);
    }
}

然后在ActivityThread的handleResumeActivity阶段,设置Activity对应的window属性type为应用窗口属性1,然后调用WindowManagerImpl的addView方法。在addView方法又再次调用了WindowManagerGlobal.addView方法。这里需要注意,WindowManagerGlobal对象是一个单例对象,一个进程只会存在一份,然后windowmanagerImpl对象是在每次创建窗口都会新建一个。
接着来到了windowmanagetGlobal.addView方法。在这个时候会进行很重要的一次操作,就是给window设置token:IBinder值。token值用来标记了哪些窗口是一组的,比如activity和他的dialog,他们的token值就是一样的,两个activity的token值就是不一样的。
设置完window的token值,接着创建了ViewRootImpl对象。然后调用了ViewRootImpl的setView方法。这里面干了很重的操作,不仅调用了requestlayout触发了整个view树的measure layout draw操作,还干了件大事就是完成了window窗口的添加。调用方法windowsession.addToDisplayAsUser方法。这里就涉及到了跨进程通信。windowSession是通过跨进程的方式从windowManagerService里面获取到的session对象。接着看addToDisplayToUser方法。这个时候就来到了system_server进程。
这个时候就来到了一个新的领域,android窗口添加过程了。在session调用addDisplayToUser的时候会调用到WindowManagerService.addWindow方法。然后通过displayid或者token值获取对应的displayContent。如果用户手机有多个屏幕的话就会有多个Displaycontent对象。如果只有一个屏幕。那只会有一个DisplayContent对象。通过token从DisplayContent里面获取windowtoken值。然后为窗口创建windowstate对象。通过windowtoken维护着一组windowstate。然后displaycontent维护着一组windowtoken。至此结束。