第8章 理解Window和WindowManager

Window表示一个窗口,是View的实际管理者。在第4章的事件分发中已经知道了,点击事件是通过Window->DecorView->View来传递的。

Window是一个抽象类,具体实现是PhoneWindow类。我们可以通过WindowManager来操作Window,具体实现是在WindowManagerService中实现的;WindowManager和WindowManagerService通过IPC连接。

8.1 Window和WindowManager

8.1.1 创建一个Window

首先,通过WindowManager添加一个窗口:

        Button button = new Button(this);
        button.setText("button");
        WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
                WindowManager.LayoutParams.WRAP_CONTENT,
                WindowManager.LayoutParams.WRAP_CONTENT,
                WindowManager.LayoutParams.TYPE_APPLICATION, 0,
                PixelFormat.TRANSPARENT);
        lp.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
                | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
                | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED;
        lp.gravity = Gravity.LEFT | Gravity.TOP;
        lp.x = 100;
        lp.y = 300;
        WindowManager wm = (WindowManager) getSystemService(WINDOW_SERVICE);
        wm.addView(button, lp);

这里注意,原书中代码报错,必须给窗口指定一个type,我这里指定的WindowManager.LayoutParams.TYPE_APPLICATION;不清楚是否是系统版本的问题。

8.1.2 Type

Type有三种类型:

  • 应用Window
    应用类Window对应一个Activity。取值在1~99之间。

  • 子Window
    子Window附属于一个特定的父Window,而不能单独存在。取值在1000~1999之间。

  • 系统Window
    系统Window需要系统权限才能创建。取值在2000~2999之间。

8.1.3 Flag

Flag参数表示Window的属性,可以控制Window的显示特性。

  • FLAG_NOT_FOCUSABLE
    表示Window不需要获取焦点,也不需要接受各种输入事件。

  • FLAG_NOT_TOUCH_MODAL
    表示Window会处理当前区域以内的单击事件,同时把当前Window区域以外的单击事件传递到底层。

  • FLAG_SHOW_WHEN_LOCKED
    表示Window可以显示在锁屏的界面上。

8.1.4 权限

想要显示系统级的Window是需要申请权限的。例如当type = TYPE_SYSTEM_ERROR的时候,需要在AndroidManifest中添加<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />

在Android 6.0及以后,需要手动开启悬浮窗权限:

    /**
     * 检查是否有悬浮窗权限
     */
    @TargetApi(Build.VERSION_CODES.M)
    private void checkOverlay() {
        if (!Settings.canDrawOverlays(this)) {
            AlertDialog dialog = new AlertDialog.Builder(this)
                    .setTitle("权限禁止")
                    .setMessage("悬浮窗权限被禁止,点击确定前往设置")
                    .setPositiveButton("确定", 
                        (dialog1, which) -> jumpToOverlaySetting())
                    .setCancelable(false)
                    .create();
            dialog.show();
        }
    }

    private void jumpToOverlaySetting() {
        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
            Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION);
            startActivityForResult(intent, 1);
        }
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (requestCode == 1) {
            checkOverlay();
        }
    }

然后在onActivityResult()中,再判断是否开启权限。

如果不想申请权限,使用type = TYPE_TOAST也可以创建一个悬浮窗。但是在Android 7.1.1(API 25)开始,TYPE_TOAST被限制使用[1]。如果想要在7.1.1以后使用悬浮窗,必须开启权限。

8.2 Window的内部机制

Window是一个抽象概念,而不是实际存在的。每一个Window都对应着一个View和一个ViewRootImpl,Window和View是通过ViewRootImpl来建立联系的。Window实际存在的形式是View。
对Window的操作只能通过WindowManager来进行。WindowManager主要提供了三个方法:

  • addView(View, LayoutParams)
  • removeView(View)
  • updateView(View, LayoutParams)

可以看到,WindowManager的操作对象是View。

8.2.1 Window的添加过程

在WindowManager的实现类WindowManagerImpl中,找到addView()方法:

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

    @Override
    public void updateViewLayout(View view, ViewGroup.LayoutParams params) {
        mGlobal.updateViewLayout(view, params);
    }

    @Override
    public void removeView(View view) {
        mGlobal.removeView(view, false);
    }

而mGlobal的定义:

    private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();

这个单例类,在第四章[2]的时候已经接触过了。这里我们看到,WindowManager的添加、删除、更新三个方法,其实都全权交给了WindowManagerGlobal。

转到WindowManagerGlobal的addView()方法中:

    public void addView(View view, 
        ViewGroup.LayoutParams params, 
        Display display, 
        Window parentWindow) {
        // ... 省略若干代码

        // 创建ViewRoot
        ViewRootImpl root;
        root = new ViewRootImpl(view.getContext(), display);

        view.setLayoutParams(wparams);

        // 将View添加到列表中
        mViews.add(view);
        mRoots.add(root);
        mParams.add(wparams);

        // do this last because it fires off messages to start doing things
        try {
            root.setView(view, wparams, panelParentView);
        } catch (RuntimeException e) {
            // BadTokenException or InvalidDisplayException, clean up.
            if (index >= 0) {
                removeViewLocked(index, true);
            }
            throw e;
         }
    }

这里主要是将待添加的View、ViewRoot、LayoutParams保存到列表中,创建了ViewRoot,以及调用了ViewRoot的setView方法。
接下来继续跟踪setView方法。

    public void setView(View view, 
          WindowManager.LayoutParams attrs, 
          View panelParentView) {
          // ...省略若干代码

          // 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.
          requestLayout();
          // ...
          res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
                   getHostVisibility(), mDisplay.getDisplayId(),
                   mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
                   mAttachInfo.mOutsets, mInputChannel);
          // ...
    }

requestLayout()之前有一个注释:

// 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.

大概意思是说,为了保证在接收到其他系统事件之前布局好,我们在将View添加到Window之前进行第一次的布局。

然后调用了WindowSession.addToDisplay()方法。mWindowSession是通过WindowManagerGlobal.getWindowSession()来获取的,而跟踪进去发现发现这是一个Binder对象,进行了和WindowManagerService之间的IPC。也就是说,Window的添加最后交给了WindowManagerService来进行。

8.2.2 Window的删除过程

Window的删除的过程和添加类似,通过WindowManager.removeView() -> ViewRoot.die() -> WindowSession.remove()最终交付WindowManagerService来处理。
在这个过程中,会调用View的onDetachedFromWindow()回调。然后会调用WindowManager.doRemoveView()来将之前添加到列表中的View、ViewRoot、LayoutParams(8.2.1)给删掉。

8.2.3 Window的更新过程

更新过程相较添加和删除比较简单,WindowManager.updateViewLayout() -> ViewRoot.setLayoutParams() -> ViewRoot.scheduleTraversals()
最终会调用ViewRoot.performTraversals()方法,在这里会对View进行measure、layout、draw的步骤。同时会调用relayoutWindow -> WindowSession.relayoutWindow(),通过WindowSession来更新Window视图,这个过程仍然是经过IPC,由WindowManagerService来实现的。

8.2.* 小结

  • Window是一个抽象概念,实际上是不存在的,通过View来体现。View不能单独存在,而必须依托于Window;而Window的具体呈现方式则是通过View。
  • 一个Window对应一个ViewRoot对应一个View,ViewRoot是WindowManager和View之间的纽带。而ViewRoot所代表的意义,则是这个Window的ViewTree的根节点。
    WindowManager ----> ViewRoot --(IPC)-> WindowManagerService 这是操作Window的一般流程。而所有关于Window的操作,最终都是在WindowManagerService中实现的。

8.3 Window的创建过程

8.3.1 Activity的Window

在第四章里面,我分析过Activity的启动流程:第4章 View的工作原理

当系统将要启动一个Activity的时候,会调用ActivityThread.performLaunchActivity()方法,在这个方法中会创建Activity实例,并调用Activity的attach方法。在attach方法中,会赋予Activity上下文,创建Window并将其和Activity绑定。Activity实现了Window.Callback接口,所以Activity可以收到Window的若干回调,比如onAttachedToWindow、onDetachedFromWindow、dispatchTouchEvent等。

Window实例的创建,是通过PolicyManager的工厂方法makeNewWindow(Context)来进行的。
当我们在Activity调用setContentView方法时,会创建DecorView并将xml的布局加载进content中。DecorView虽然已经创建完毕,但是他还没有被WindowManager添加到Window中。Window的概念,更多表示的是一种抽象的功能集合。在Window和DecorView都创建完毕之后,ActivityThread会调用handleResumeActivity方法->makeVisible方法,在其中会调用WindowManager.addView(),这就回到了8.2.1的流程了,通过addView将DecorView添加到Window。
(有点吃惊,书中的内容和我之前在第四章中分析的几乎一模一样,不过这也说明我没有误入歧途。)

8.3.2 Dialog的Window

Dialog和Activity创建Window的方式类似。

1、创建Window
2、初始化DecorView并将Dialog的视图添加进去
3、将DecorView添加到Window中

在Dialog被关闭时,他会通过WindowManager来移除DecorView。

8.3.3 Toast的Window

Toast也是基于Window来实现的,不过过程和Dialog不同。
总的来说,Toast是通过IPC过程调用NotificationManagerService来进行显示和隐藏的。

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

推荐阅读更多精彩内容