Android---Window&WindowManager

上三篇文章主要讲了与 View 有关的一些知识,今天我们来学习一下与 View 和 Activity 相关联的一个东西—-Window。

Window 算是 Activity 与 View 之间的一个媒介,从 View 的事件分发中我们可以知道,事件是由硬件捕获,通过 WMS 经过 IPC 过程传递给 Activity, Activitiy 利用属于自身的 Window 将事件传递给顶级 View —- DecorView,然后再由 DecorView 传递给我们的 View。

Window 本身是一个抽象类,它的实现类是 PhoneWindow,但该类的代码是自动生成的。创建一个 Window 十分简单,外界只要调用 WindowManager 来实现即可,这是是外界访问 Window 的入口。Window 的具体实现位于 WindowManagerService 中, WindowManager 与 WindowManagerService 的交互是一个 IPC 的过程。

Android 中的所有视图都是附加在 Window 上的,因此 Window 是 View 的直接管理者。

Part.1 Window 与 WindowManager

1.1 Window 的创建

除了我们平时不经意间调用 Window 外,实际上我们也可以通过 WindowManager 的方法便可以十分轻易的添加 Window。

    ImageView imageView;
    WindowManager.LayoutParams mLayoutParams;
    WindowManager manager;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        manager = (WindowManager) getApplicationContext().getSystemService(Context.WINDOW_SERVICE);
        imageView = new ImageView(this);
        imageView.setBackgroundResource(R.mipmap.ic_launcher);
        mLayoutParams = new WindowManager.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT
                , ViewGroup.LayoutParams.WRAP_CONTENT,0,0, PixelFormat.TRANSPARENT);
        mLayoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL |
                WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE ;
        mLayoutParams.gravity = Gravity.LEFT | Gravity.TOP;
        mLayoutParams.type = WindowManager.LayoutParams.TYPE_SYSTEM_ERROR;
        mLayoutParams.x = 100;
        mLayoutParams.y = 300;
        manager.addView(imageView,mLayoutParams);

        super.onCreate(savedInstanceState);
    }

要设置 Window ,首先需调用 WindowManager.LayoutParams 对要 Window 的参数进行配置,然后在通过 WindowManager 的 addView 方法将 View 和 LayoutParams 一并设置。

其中需要关注的属性是几个标记位,他们可以控制 Window 的显示特性:

  1. FLAG_NOT_TOUCH_MODAL
    表示 Window 不需要获取焦点,也不需要接受各种输入事件,最终事件会传递给下层的具有焦点的 Window。

  2. FLAG_NOT_TOUCH_MODAL
    在此模式下,系统会将当前 Window 区域以外的单机事件传递给底层的 Window,当前 Window 区域以内的单机事件则交由自己处理。这个标记十分重要,一般而言都要开启,否则其他 Window 将无法获得焦点。

另外LayoutParams 中还存在这个以 type 属性,该属性负责控制 Window 的类型:应用 Window、子 Window 和系统 Window。

应用 Window 一般对应着一个 Activity。子 Window 不能独立存在,他需要附属在特定的父 Window 之中,例如 Dialog 就是一个子 Window。系统 Window 是需要权限声明才能创建的 Window ,比如 Toast 和 Notification 都是系统 Window。

Window 是分层的,每个 Window 都存在对应的 z-order 属性,层级大的会覆盖在层级小的上面,应用 Window 是 1~99,子 Window 是 1000~1999,系统 Window 是 2000~2999,这些层级对应着 type 参数。如果想要自己的 Window 位于所有层最上方,则使用 TYPE_SYSTEM_ERROR 属性即可,该属性需要申请权限。

<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>

1.2 WnidowManager 详细

WindowManager 的父类是 ViewManager,在该类中只有三个方法:

public interface ViewManager
{
    public void addView(View view, ViewGroup.LayoutParams params);
    public void updateViewLayout(View view, ViewGroup.LayoutParams params);
    public void removeView(View view);
}

这三个方法已经满足了开发者日常的应用需求,通过这三个方法我们可以对 Window 中的 View 进行添加删除更新操作。

由于 Window 是一个抽象的概念,每一个 Window 都对应着一个 View 和一个 ViewRootImpl,Window 和 View 通过 ViewRootImpl 来建立关系。在平时的使用之中,我们只能通过 WindowManager 的三个方法访问 Window。

由于 WindowManager 和 ViewManager 均是接口,因此我们需要找到其实现类,它们真正的实现类是 WindowManagerImpl。

    private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();

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

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

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

可以看到,此处交给了 mGlobal 对象进一步进行设置,它是 WindowManagerGlobal 的一个单例对象,它全权负责 WindowManagerImpl 中的实际操作。

1.2.1 Window 的添加过程

    if (view == null) {
        throw new IllegalArgumentException("view must not be null");
    }
    if (display == null) {
        throw new IllegalArgumentException("display must not be null");
    }
    if (!(params instanceof WindowManager.LayoutParams)) {
        throw new IllegalArgumentException("Params must be WindowManager.LayoutParams");
    }

    final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;
    if (parentWindow != null) {
        parentWindow.adjustLayoutParamsForSubWindow(wparams);
    }     

addView 方法首先对设置进行检查,如果发现是子 Window 的话还要另外调整一些参数。

然后该类创建 ViewRootImpl 对象并将 View 添加到列表中

    private final ArrayList<View> mViews = new ArrayList<View>();
    private final ArrayList<ViewRootImpl> mRoots = new ArrayList<ViewRootImpl>();
    private final ArrayList<WindowManager.LayoutParams> mParams =
            new ArrayList<WindowManager.LayoutParams>();
    private final ArraySet<View> mDyingViews = new ArraySet<View>();

    root = new ViewRootImpl(view.getContext(), display);
    view.setLayoutParams(wparams);

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

其中 mViews 列表负责存放所有 Window 对应的 View,mRoots 列表存放的是所有的 ViewRootImpl, mParams 列表存放的所有 Window 所对应的的布局参数,而 mDyingViews 则负责存放那些即将被删除但是仍未删除的对象。

紧接着,addView 方法会通过调用 root 的 setView 方法来完成添加的过程。该方法以前也提到过,它会调用 requestLayout 方法

    @Override
    public void requestLayout() {
        if (!mHandlingLayoutInLayoutRequest) {
            checkThread();
            mLayoutRequested = true;
            scheduleTraversals();
        }
    }

该方法调用了 scheduleTraversals 异步方法,之后会调用 ViewRoot 的 performTraversals 函数,该函数就包含了 View 的绘制的代码,这里就不展开了。

    try {
            mOrigWindowType = mWindowAttributes.type;
            mAttachInfo.mRecomputeGlobalAttributes = true;
            collectViewAttributes();
            res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
                            getHostVisibility(), mDisplay.getDisplayId(),
                            mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
                            mAttachInfo.mOutsets, 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();
            }
        }

上面的代码会借助 WindowSession 类的对象来调用 addToDisplay 方法,mWindowSession 的类型是 IWindowSession ,它是一个 Binder 对象,其真正的实现类是 Session,这实际上是一个 IPC 的过程。

Session内部会通过 WindowManagerService 来实现 Window 的加载,在 WindowManagerService 内部会为每个应用保留一个独立的 session。

1.2.2 Window 删除过程

Window 的删除过程和添加过程一样,都是通过 WindowManagerImpl 后,再进一步通过 WindowManagerGlobal 来实现的。

    public void removeView(View view, boolean immediate) {
        if (view == null) {
            throw new IllegalArgumentException("view must not be null");
        }

        synchronized (mLock) {
            int index = findViewLocked(view, true);
            View curView = mRoots.get(index).getView();
            removeViewLocked(index, immediate);
            if (curView == view) {
                return;
            }

            throw new IllegalStateException("Calling with view " + view
                    + " but the ViewAncestor is attached to " + curView);
        }
    }

    private void removeViewLocked(int index, boolean immediate) {
        ViewRootImpl root = mRoots.get(index);
        View view = root.getView();

        if (view != null) {
            InputMethodManager imm = InputMethodManager.getInstance();
            if (imm != null) {
                imm.windowDismissed(mViews.get(index).getWindowToken());
            }
        }
        boolean deferred = root.die(immediate);
        if (view != null) {
            view.assignParent(null);
            if (deferred) {
                mDyingViews.add(view);
            }
        }
    }    

其中的 removeViewLocked 方法通过 index 获得了 View 对应的 ViewRoot 对象,然后通过调用 die 方法通过 IPC 通知 WMS 当前 View 要被销毁。该方法中会调用 dispatchDetachedFromWindow 方法,真正的删除方法就在这里面实现。另外我们要注意 immediate 属性,当该属性为 true 的时候,die 方法中会立刻执行 doDie 方法,一般来说我们都不需要如此操作。当我们将immediate设置为 false 的时候,WindowRoot 会任务交到了 handler 中再执行 doDie 方法,这样就使得删除的过程变得安全了。

在删除的过程中会做四件事:

  1. 垃圾回收相关工作。

  2. 通过 session 的 remove 方法删除 Window,这是一个 IPC 过程。

  3. 调用 View 的 dispatchDetachedFromWindow 方法,在内部会调用 View 的 onDetachedFromWindow() 以及 onDetachedFromWindowInternal()

  4. 调用 WindowManagerGlobal 的 doRemoveView 方法刷新数据,包括 mRoots、mParams 以及 mDyingViews.

1.2.3 Window 的更新过程

最后我们看看 updateViewLayout 方法,实际上过程也差不多

    public void updateViewLayout(View view, ViewGroup.LayoutParams params) {
        if (view == null) {
            throw new IllegalArgumentException("view must not be null");
        }
        if (!(params instanceof WindowManager.LayoutParams)) {
            throw new IllegalArgumentException("Params must be WindowManager.LayoutParams");
        }

        final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams)params;

        view.setLayoutParams(wparams);

        synchronized (mLock) {
            int index = findViewLocked(view, true);
            ViewRootImpl root = mRoots.get(index);
            mParams.remove(index);
            mParams.add(index, wparams);
            root.setLayoutParams(wparams, false);
        }
    }

与 addView 一样,首先去做检验设置的操作,然后取出新的 params 并加载到 View 中,接着再去更新 ViewRootImpl 中的 LayoutParams。最后, ViewRootImpl 中会像 scheduleTraversals 方法一样重新对 View 布局测量和重绘。

Part.2 Window 的创建过程

从上面的分析我们可以看出, View 不能单独存在,每个 View 都是依附在 Window 这个抽象的概念上,Android 中可以提供视图的地方有 Activity、Dialog、Toast 等,他们都对应着一个 Window。

2.1 Activity 的 Window 创建过程

Activity 是我们最常接触到的组件,它依附的 Window 在 Activity 创建之出就被创建好了,具体发生在 AcitvityThread 的 performLaunchActivity 方法中。Acitvity 的启动过程是一个 IPC 过程,经过两次 IPC,ActivityThread 的 handleLauchActivity 方法,该方法调用了 performLaunchActivity 方法创建 Activity。

            ...
        Window window = null;
        if (r.mPendingRemoveWindow != null && r.mPreserveWindow) {
            window = r.mPendingRemoveWindow;
            r.mPendingRemoveWindow = null;
            r.mPendingRemoveWindowManager = null;
        }
        activity.attach(appContext, this, getInstrumentation(), r.token,
                        r.ident, app, r.intent, r.activityInfo, title, r.parent,
                        r.embeddedID, r.lastNonConfigurationInstances, config,
                        r.referrer, r.voiceInteractor, window);
        ...

当 Activity 与 Application 和 Context 被成功创建之后,系统会调用 attach 方法将以上的三个部分进行关联。在这个过程中,若是检测到 Window 尚未创建,便会调用 new PhoneWindow 方法对 Window 进行初始化,由于 Activity 实现了 Window 的 CallBack 接口,此处会将 Activity 作为 CallBack 的实例关联到 Window 中,当 Window 发生状态改变的时候就会回调这些接口通知 Activity,其中有一些接口是我们十分熟悉的,例如 dispatchTouchEvent()。

    mWindow = new PhoneWindow(this, window);
    mWindow.setWindowControllerCallback(this);
    mWindow.setCallback(this);
    mWindow.setOnWindowDismissedCallback(this);
    mWindow.getLayoutInflater().setPrivateFactory(this);
    if (info.softInputMode != WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED) {
        mWindow.setSoftInputMode(info.softInputMode);
    }
    if (info.uiOptions != 0) {
        mWindow.setUiOptions(info.uiOptions);
    }

上面的 PhoneWindow 我们前面也提到过,它继承了 Window 类,是它的实现类,而我们平时调用 getWindow() 方法获取的就是该对象,我们在 Activity 中调用的 setContentView 就是利用了该对象的 setContentView 方法进行设置。

    /**
     * Activity#setContentView
     */
    public void setContentView(@LayoutRes int layoutResID) {
        getWindow().setContentView(layoutResID);
        initWindowDecorActionBar();
    }

    /**
     * PhoneWindow#setContentView
     */
    @Override
    public void setContentView(int layoutResID) {
        // Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
        // decor, when theme attributes and the like are crystalized. Do not check the feature
        // before this happens.
        if (mContentParent == null) {
            installDecor();
        } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            mContentParent.removeAllViews();
        }

        if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
                    getContext());
            transitionTo(newScene);
        } else {
            mLayoutInflater.inflate(layoutResID, mContentParent);
        }
        mContentParent.requestApplyInsets();
        final Callback cb = getCallback();
        if (cb != null && !isDestroyed()) {
            cb.onContentChanged();
        }
        mContentParentExplicitlySet = true;
    }

可以看到 PhoneWindow#setContentView 的第一步就是检查是否存在 DecorView,若不存在就通过 installDecor 方法创建它。至于 DecorView 在上几篇中已经反复提到过,实际上就是个 FrameLayout,也是我们的顶级 View。
最终我们设置的 layout 文件就会加载到 DecorView 的 content 的部分。这里面的过程就不详细说了。

最后,该方法会通过 getCallback 方法来获取 Activity 并通知它设置成功,其中 onContentChanged 方法是个空实现,需要的话我们可以在自己的 Activity 方法中覆写它。

经过上面的一系列操作, Window 就算成功的创建了,而且我们的 layout 文件也成功的设置到了依附在该 Window 上的 DecorView 里面。但是此时的 Window 还没有被 WindowManager 识别,因此它仍旧无法接受外界传输过来的事件,而且,此时的界面仍旧是隐藏状态的,没有办法被用户看到。

在 ActivityThread 的 handleResumeActivity 方法中会调用 Activitiy 的 Resume 方法,紧接着会调用 makeVisible 方法,该方法内部会调用 WindowManager 的 addView 方法向系统注册 Window。这样,Acitivity 的 Window 创建过程才算真正完成了。

2.2 Dialog 的 Window 创建过程

Dialog 的 Window 的创建过程和 Activity 类似,我们从 Dialog 的新建开始入手:

        mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);

        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);

可以发现上面的代码跟 Activity 基本上没有区别,实际上正是如此

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

此处的 mWindow 就是 PhoneWindow,接下来的操作再度进入了 PhoneWindow 中的 setContentView 中,具体就不再赘述了。Activity 是通过 makeVisiable 来显示的,而 Dialog 是通过 show 方法来将 DecorView 加载在 Window 中

        WindowManager.LayoutParams l = mWindow.getAttributes();
        if ((l.softInputMode
                & WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION) == 0) {
            WindowManager.LayoutParams nl = new WindowManager.LayoutParams();
            nl.copyFrom(l);
            nl.softInputMode |=
                    WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION;
            l = nl;
        }

        mWindowManager.addView(mDecor, l);
        mShowing = true;

        sendShowMessage();

可以发现上述的过程与 Activity 创建 Window 的过程并没有太大的区别,唯一需要注意的就是在新建 Dialog 的时候需要传递 Activity 的 Context 而不能是 Application 的 Context,否则会报错。

    Dialog dialog = new Dialog(this.getApplicationContext());
    TextView tv = new TextView(this);
    tv.setText("Test");
    dialog.setContentView(tv);
    dialog.show();

上面的代码是错误的,会报找不到 token 的错,这是因为在 addView 的过程中由于使用的是 IPC 的方法,如果是用 ApplicationContext 的话是不包含 token 的,而在远端 token 是用于标识 Activity 是否相同的,因而会报错。

但是我们实际上是可以规避这个问题的,在开始的时候我们说过 LayoutParams.type 参数代表着 Window 的类型,我们只要将 type 设置为 TYPE_SYSTEM_OVERLAY 并在 AndroidManifest 设置对应的权限就好了。

2.3 Toast 的 Window 创建过程

Toast 与 Activity 和 Dialog 相比就不一样了,它虽然也是通过 Window 来实现的,但是由于 Toast 具有定时取消这一功能,所以系统采用了 Handler。在 Toast 内部存在着两个 IPC 过程,第一个是 Toast 访问 NotificationManagerService,第二类是 NotificationManagerService 回调 Toast 内部的 TN 接口。

    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
        }
    }

这里的 getService() 方法实际上就是返回一个 binder,对应着 NotificationManagerService。紧接着调用了 enqueueToast() 方法,该方法第一个参数是当前应用的包名,第二个参数是远程回调的对象,第三个参数代表着 Toast 持续的时间。该方法将 Toast 请求封装为 ToastRecord,然后将其添加到一个 Toast 的队列中。该队列是一个 ArrayList,一般而言它针对每个 pkg 的最大长度为50,超过这个数量的 Toast 请求都会被拒绝。

                        if (!isSystemToast) {
                            int count = 0;
                            final int N = mToastQueue.size();
                            for (int i=0; i<N; i++) {
                                 final ToastRecord r = mToastQueue.get(i);
                                 if (r.pkg.equals(pkg)) {
                                     count++;
                                     if (count >= MAX_PACKAGE_NOTIFICATIONS) {
                                         Slog.e(TAG, "Package has already posted " + count
                                                + " toasts. Not showing more. Package=" + pkg);
                                         return;
                                     }
                                 }
                            }
                        }

最后该方法会调用 showNextToastLocked 方法,该方法取出队列中的第一个 ToastRecord,然后调用 record.callback.show() 方法,此处的 callback 实际上是一开始传递过来的 TN 对象,这是一个运行在 APP 本地 Binder 池的 Binder,最终 TN 中的 show 方法被调用,它利用 handler 将线程从 Binder 线程池切换到 Toast 请求的线程中从而将 Toast 显示出来。

当 show 调用完毕后,showNextToastLocked 中还会调用 scheduleTimeoutLocked 方法,该方法利用了 handler delay 延时调用 handleTimeout 方法,与 show 一样,此处远程调用 TN 中的 hide 方法,将 Toast 隐藏并将它从 NMS 的 Toast 队列中删除掉。

需要注意的是,Toast 的 View 是在 TN 类中的 handleShow 方法中被添加到 Window 中,且在 handleHide 方法中从 Window 中移除。

通过本次学习,我们已经理清了从 Window 层到 View 层的一些关系,在下一篇中,我们会复习四大组件是如何工作的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值