Activity和dialog的窗口添加源码分析

本文是最近看Window有关源码的部分记录和分析, 可能有差错; 另有很多代码,比较枯燥

先说部分结论

  1. Window是一个抽象类, 具体的实现是PhoneWindow
  2. android系统中, 每个界面,对应着一个window; 但其实在android系统中window也是一个抽象的概率,它是以view的形式存在; 在使用中, 无法直接访问Window, 只能通过WindowManager才能访问Window; 每个Window都对应一个View和一个ViewRootImpl, ViewRootImpl是连接Window和WMS的桥梁, WMS的一些消息,通过ViewRootImpl转发给View;
  3. WindowManager继承自ViewManager(间接证明Window其实对应的是View?),常用的只有三个方法:addView、updateView和removeView
  4. 各种Window的不同, 主要是 token及type的不同
  5. app中控制window, 是通过WindowManager.LayoutParams去控制, eg: 通过x,y,gravity去控制位置...

Activity 的window添加

Activity创建之后, 在ActivityThread中 调用 Activity#attach, 进行一些初始化操作

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) {
    attachBaseContext(context);
    ....
    //创建对应的Window 并设置callback, 其实为PhoneWindow
    mWindow = new PhoneWindow(this);
    mWindow.setCallback(this);
    mWindow.setOnWindowDismissedCallback(this);
    ....
    // 设置Window的WindowManager, 对Window的mWindowManager赋值,
    // 事实上, Window中 并未使用传递进去的windowManager, 而是在此方法中 调用WindowManagerImpl.createLocalWindowManager 重新创建了一个
    mWindow.setWindowManager(
            (WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
            mToken, mComponent.flattenToString(),
            (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
    if (mParent != null) {
        mWindow.setContainer(mParent.getWindow());
    }
    // 把window对象中的 windowManager 关联到 Activity的 mWindowManager
    mWindowManager = mWindow.getWindowManager();
    ...
  }

在Activity#attach中, 可以看出创建了一个PhoneWindow对象, 并且通过context.getSystemService(Context.WINDOW_SERVICE)设置了WindowManager对象, 还通过Window#getWindowManager对Activity的mWindowManager赋值, 这样 Activity的Window WindowManager就关联起来了;

接着看上面代码,里面会通过context.getSystemService(Context.WINDOW_SERVICE)拿到一个windowManager对象, 这个window对象是什么呢? 如果对Context有过研究的话, 可以知道Context的实际对象是ContetImpl, 其中所有通过 getSystemService返回的对象, 都是通过static的代码块静态添加的, 只需找WINDOW_SERVICE注册的地方(在6.0中 注册的代码 已经提取到'android.app.SystemServiceRegistry#registerService'中了)

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

可以看见,实际的对象是WindowManagerImpl; (另外 Activity本身重写了getSystemService方法,如果使用android.app.Activity#getSystemService, 返回的其实不是这个对象, 下面会说到);

上面注释说到, android.view.Window#setWindowManager中, 并未使用传递进去的WindowManager,而创建了一个新对象, 可以看一下代码

public void setWindowManager(WindowManager wm, IBinder appToken, String appName,
            boolean hardwareAccelerated) {
        // activity把token传进来, 保存在Window.mAppToken
        mAppToken = appToken;
        mAppName = appName;
        mHardwareAccelerated = hardwareAccelerated
                || SystemProperties.getBoolean(PROPERTY_HARDWARE_UI, false);
        if (wm == null) {
            wm = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE);
        }
        //创建了一个新的WindowManager, 并且把this 传递给了WindowManager
        mWindowManager = ((WindowManagerImpl)wm).createLocalWindowManager(this);
    }

而在android.view.WindowManagerImpl#createLocalWindowManager中, 可以明显看出

// Context.getSystemService 获得的WindowManager实例, 是没有parentWindow的
public WindowManagerImpl(Display display) {
        this(display, null);
    }

private WindowManagerImpl(Display display, Window parentWindow) {
    mDisplay = display;
    mParentWindow = parentWindow;
}

public WindowManagerImpl createLocalWindowManager(Window parentWindow) {
    // 将传递的 window对象保存, 对于activity来说,会将PhoneWindow对应的对象传入, 而对于 Context.getSystemService 获得的WindowManager实例, 是没有parentWindow的
    return new WindowManagerImpl(mDisplay, parentWindow);
}

window时我们看见的窗口,接下来看在Activity的window是怎么显示出来, 看Activity.makeVisiable

void makeVisible() {
    if (!mWindowAdded) {
        ViewManager wm = getWindowManager();
        // windowManger中 添加window的根view( 即 decorView)
        wm.addView(mDecor, getWindow().getAttributes());
        mWindowAdded = true;
    }
    mDecor.setVisibility(View.VISIBLE);
}

上面已经说过, WindowManager对应的实例是WindowManagerImpl, 接着看android.view.WindowManagerImpl#addView

 private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();

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

代码很简单, 最后调用的是 WindowManagerGlobal#addView, 并且WindowManagerGlobal是一个单例, 看下android.view.WindowManagerGlobal#addView的代码

public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow) {
              ...
         // 首先获得 layoutParams
        final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;
        if (parentWindow != null) {
            // parentWindow是 WindowManagerImpl的成员变量mParentWindow;
            // 对于 activity来说, 即在mWindow.setWindowManager中 调用WindowManagerImpl.createLocalWindowManager(Window)去创建 mWindowManager实例时, 传入的phoneWindow对象
            // 调用下面这个方法, 会调整 WindowManager.LayoutParams, 主要是 设置 token 和硬件加速的标志位
            parentWindow.adjustLayoutParamsForSubWindow(wparams);
        } else {
            // If there's no parent, then hardware acceleration for this view is
            // set from the application's hardware acceleration setting.
            final Context context = view.getContext();
            if (context != null
                    && (context.getApplicationInfo().flags
                            & ApplicationInfo.FLAG_HARDWARE_ACCELERATED) != 0) {
                wparams.flags |= WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED;
            }
        }

        ViewRootImpl root;
        View panelParentView = null;
        ....
        // ViewRootImpl是一个很重要的类, 是ViewParent的子类, 可以说是Window和WMS之间的桥梁, 所有和View有关的Evnet,layout,draw,都在底层产生后通过WMS->ViewRootImpl传递给view
        root = new ViewRootImpl(view.getContext(), display);

        view.setLayoutParams(wparams);

        mViews.add(view);
        mRoots.add(root);
        mParams.add(wparams);

        ...
        root.setView(view, wparams, panelParentView);
        ...
}

在WindowManagerGlobal中, 会为每一个window创建对应的ViewRootImpl, 并把对应的 decorView, ViewRootImpl 和 WindowManager.LayoutParams 保存, 最后把decorView 通过android.view.ViewRootImpl#setView添加进WMS 妈蛋,好累啊, 终于快到WMS了

接着看一下android.view.ViewRootImpl#setView

public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
      ...
      // 把Window对应的View保存下来, 对应的其实就是 PhoneWindow的 mDecorView
      mView = view;
      ...
      // 调用WMS后返回的结果
     int res; /* = WindowManagerImpl.ADD_OKAY; */

     // Schedule the first layout -before- adding to the window
     // manager, to make sure we do the relayout before receiving
     // any other events from the system.
     // 请求 layout
     requestLayout();
    ....

    // 调用 WMS添加window, 并返回一个结果 用于判定添加的结果
    res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
                            getHostVisibility(), mDisplay.getDisplayId(),
                            mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
                            mAttachInfo.mOutsets, mInputChannel);
        ......                              

    // 根据返回的结果 判断是否添加成功, 没成功 就throw exception, 经常看见的services打开dialog的异常 就在这儿产生
    if (res < WindowManagerGlobal.ADD_OKAY) {
        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");   
            .....
          }                          
     ......
}

ViewRootImpl里面, 首先会把window对应的decorView存一份, 然后通过mWindowSession向WMS发消息, mWindowSession是通过WindowManagerGlobal的静态方法获得, 前面说过,这玩意是一个单例, 在哪儿都可以拿到; android本身是一个CS架构, 调用WMS,需要先获取WMS对应的client, 而client端的获取,就是同 WindowManagerGlobal.initialize()进行, 连通WMS的服务端; android.view.WindowManagerGlobal#getWindowSession, 是通过getWindowManagerService().openSession()获得, 最后返回的是com.android.server.wm.Session, 而mWindowSession.addToDisplay最终调用的会是 com.android.server.wm.Session#addToDisplay, 最终会还是会调用com.android.server.wm.WindowManagerService#addWindow; 这一段在WMS Server端的代码也比较多, 就不贴代码了, 最后在WindowManagerService#addWindow, 会对WindowManager.LayoutParams做一些检验 并返回值, 下面会说到;

上面说过事件是通过WMS传递给ViewRootImpl,然后传递给View,Activity, 具体事件在ViewRootImpl的分发过程, 可以看这篇博客here

dialog的Window创建过程

1.dialog的窗口创建显示过程,其实与activity相似, 最后对应的Window其实也是PhoneWindow 首先直接看Dialog的构造方法

    Dialog(@NonNull Context context, @StyleRes int themeResId, boolean createContextThemeWrapper) {
        ......
        // 用getSystemService获取 WindowManger实例
        mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
        // 直接创建一个 PhoneWindow的实例
        final Window w = new PhoneWindow(mContext);
        mWindow = w;
        w.setCallback(this);
        w.setOnWindowDismissedCallback(this);
        w.setWindowManager(mWindowManager, null, null);
        w.setGravity(Gravity.CENTER);
        mListenersHandler = new ListenersHandler(this);
    }

代码很明显, 首先获取windowManager实例, 然后创建window对象,然后在设置callback,对PhoneWindow的实例设置WindowManager;(这里有一点和Activity不同, Activity是调用Window的setWindowManager后, 把Window对应的WindowManger获取,然后赋值给自己的变量Activity#mWindowManger, 而dialog没这个操作; dialog对应的Window对象,就只是传入的Context实例去getSystemService所得); 接下来,就是设置dialog的view,和Activity一样,都是调用setContentView, 看下代码

public void setContentView(@LayoutRes int layoutResID) {
        mWindow.setContentView(layoutResID);
    }

代码很简单,直接调用的mWindow.setContentView, 把对应的视图,添加到PhoneWindow的DecorView中去; 接下来就是显示了, 看一下Dialog#show方法

    public void show() {
        ...
        // 调用Dialog#onCreate方法, 通常也是在onCreate方法中,改变Window.LayoutParams, 从而调整dialog的宽高, 动画, 显示位置
        onStart();
        .......
        mDecor = mWindow.getDecorView();
        WindowManager.LayoutParams l = mWindow.getAttributes();
        // 调用WindowManager去显示
        mWindowManager.addView(mDecor, l);
        mShowing = true;

    }

显示的代码逻辑,和Activity显示的一样, 就是调用WindowManger#addView方法 在这儿就有一个开发中常遇到的问题, Activity可以直接调用show()显示一个dialog, 而Service调用会报错,解决这个问题也很简单,一般就是声明使用系统Window的权限, 然后在设置dialog的type, dialog.getWindow().setType(LayoutParams.TYPE_SYSTEM_ERROR)

一般错误如下

那产生这种情况,具体原因是什么呢?

首先说下结论,产生这个情况是因为WindowManger实例的缘故;我们看代码,分析具体的原因,首先肯定传入的Context对象不同,而context的不同,会影响什么呢, 在上面dialog创建过程中说过, dialog中的WindowMange对象, 是Context#getSystemService获得, 在Activity获取WindowManger时候,我提到过Activity重写了getSystemService方法, 看一下Activity#getSystemService的代码

@Override
    public Object getSystemService(@ServiceName @NonNull String name) {
        ...
        // 获取WMS时, 直接返回自身的windowManger
        if (WINDOW_SERVICE.equals(name)) {
            return mWindowManager;
        }
        ...
        return super.getSystemService(name);
    }

在传入的Context是Activity实例时, 获取到的WindowManger, 就是Activity自身的mWindowManager,而其他的context对象,获取的都是默认的WindowManger; 这2个WindowManger实例有什么不同呢, 上面也说到过,Activity的WindowManger创建过程中, 把其对应的PhoneWindow实例传给了WindowManager#mParentView, 最后在调用WindowManagerGlobal#addView时, 会调用parentWindow.adjustLayoutParamsForSubWindow(wparams), 从而调整了LayoutParams, 设置了window的token;

下一个问题,调用dialog.getWindow().setType(LayoutParams.TYPE_SYSTEM_ERROR)为啥就不报错了呢?

首先,看一下这个错误在哪儿抛出来的, 在上面的Activity的window显示流程中,说道window创建过程,最后会调用android.view.ViewRootImpl#setView, 这个方法中, 会调用WMS服务端, 并且返回一个值, 根据返回值,确定WMS服务端是否添加成功, 如果返回值不是WindowManagerGlobal.ADD_OKAY, 就抛出的异常, 其中有一个异常就是;

接着在看WMS服务端,什么时候会返回这个, 看一下com.android.server.wm.WindowManagerService#addWindow

//com.android.server.wm.WindowManagerService#addWindow
public int addWindow(Session session, IWindow client, int seq,
            WindowManager.LayoutParams attrs, int viewVisibility, int displayId,
            Rect outContentInsets, Rect outStableInsets, Rect outOutsets,
            InputChannel outInputChannel) {
              ...
        // 检查权限, mPolicy是 com.android.server.policy.PhoneWindowManager 的实例
        int res = mPolicy.checkAddPermission(attrs, appOp);
        if (res != WindowManagerGlobal.ADD_OKAY) {
            return res;
        }
        ...
        // token为null时, 对type进行判断
        WindowToken token = mTokenMap.get(attrs.token);
        if (token == null) {
            if (type >= FIRST_APPLICATION_WINDOW && type <= LAST_APPLICATION_WINDOW) {
                Slog.w(TAG, "Attempted to add application window with unknown token "
                      + attrs.token + ".  Aborting.");
                return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
            }
            ...
          }
        .....
}

//com.android.server.policy.PhoneWindowManager#checkAddPermission
com.android.server.policy.PhoneWindowManager#checkAddPermission{
    int type = attrs.type;
    ....
    // 根据类型 判断权限
    String permission = null;
    switch (type) {
        case TYPE_TOAST:
            // XXX right now the app process has complete control over
            // this...  should introduce a token to let the system
            // monitor/control what they are doing.
            outAppOp[0] = AppOpsManager.OP_TOAST_WINDOW;
            break;
        case TYPE_DREAM:
        case TYPE_INPUT_METHOD:
        case TYPE_WALLPAPER:
        case TYPE_PRIVATE_PRESENTATION:
        case TYPE_VOICE_INTERACTION:
        case TYPE_ACCESSIBILITY_OVERLAY:
            // The window manager will check these.
            break;
        case TYPE_PHONE:
        case TYPE_PRIORITY_PHONE:
        case TYPE_SYSTEM_ALERT:
        case TYPE_SYSTEM_ERROR:
        case TYPE_SYSTEM_OVERLAY:
            permission = android.Manifest.permission.SYSTEM_ALERT_WINDOW;
            outAppOp[0] = AppOpsManager.OP_SYSTEM_ALERT_WINDOW;
            break;
        default:
            permission = android.Manifest.permission.INTERNAL_SYSTEM_WINDOW;
    }
    ....

    //权限不够  也返回错误代码
    if (mContext.checkCallingOrSelfPermission(permission)
                    != PackageManager.PERMISSION_GRANTED) {
        return WindowManagerGlobal.ADD_PERMISSION_DENIED;
    }
    ...
}

上面代码可以看见, 首先回去检测权限, 如果是type系统的window, 则需要权限, 如果没有权限, 最后一样会抛出异常

当token为null时, 会去检查type, 如果type和对, 也会抛异常;

而Activity可以显示dialog, 上面的代码已经分析了, 是对dialog的windowLayoutParams设置了token, 从而通过检测

上面说了, 设置系统弹窗, 需要声明权限, 没有权限也会失败,在国内各种rom的手机上, 系统弹窗的权限,有可能会被默认禁止掉, 有没有 不用系统弹窗权限, 而显示系统弹窗 的呢 其实也是可以的,可以设置弹窗的类型是Toast(其实toast也是一个window,下面再说), 从而规避这个, 具体的分析可以看下面2篇博客 有些手机上弹框需要权限,通过TYPE_TOAST突破权限的分析 here, 和here


还有PopupWindow 和Toast的window, 以后在写吧 - -

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,298评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,701评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,078评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,687评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,018评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,410评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,729评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,412评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,124评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,379评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,903评论 1 257
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,268评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,894评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,014评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,770评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,435评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,312评论 2 260

推荐阅读更多精彩内容