开发艺术之Window

Window 表示窗口,可以实现悬浮窗效果。Window 是一个抽象类,它的具体实现是 PhonewWindow。创建一个Window 是很简单的事,只需要通过 WindowManager 即可完成。WindowManager 是外界访问 Window 的入口,Window 的具体实现位于 WindowManagerService,WindowManager 和 WindowManagerSevcie 的交互是一个 IPC 过程。

一、WIndow 和 WindowManager

通过 WindowManager 添加 Window,下面代码将一个 Button 添加到屏幕坐标为 (100, 300) 的位置上:

    private void alterWindow() {
        mFloatingButton = new Button(this);
        mFloatingButton.setText("button");
        mLayoutParams = new WindowManager.LayoutParams(
                WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.WRAP_CONTENT,
                0, 0, PixelFormat.TRANSPARENT
        );
        mLayoutParams.gravity = Gravity.START | Gravity.TOP;
        mLayoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL |
                WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
        mLayoutParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
        mLayoutParams.x = 100;
        mLayoutParams.y = 300;
        mFloatingButton.setLayoutParams(mLayoutParams);
        mWindowManager = (WindowManager) getSystemService(Context.WINDOW_SERVICE);
        mWindowManager.addView(mFloatingButton, mLayoutParams);
    }

其中 LayoutParams 中的 flags 和 type 这两个参数比较重要:

  • Flags 参数表示 Window 的属性,可以控制 Window 的显示特性,常用选项如下:
    • FLAG_NOT_FOCUSABLE,表示 Window 不需要获取焦点,最终事件会直接传递给下层具有焦点的 Window。
    • FLAG_NOT_TOUCH_MODAL,此模式下系统会将当前 Window 区域以外的单击事件传递给底层 Window,当前 Window 区域以内的单击事件则自己处理。
    • FLAG_SHOW_WHEN_LOCKED,此模式可以让 WIndow 显示在锁屏的界面上。
  • Type 参数表示 Window 的类型,可分为三类:
    • 应用 Window,对应 Activity
    • 子 Window,不能单独存在,需要附属在特定的父 Window 之中,比如 Dialog
    • 系统 WIndow需要权限才能创建的 Window,比如 Toast 和系统状态栏

Window 的层级概念:

  1. 每个 Window 都有对应的 z-ordered,层级大的会覆盖在层级小的上面。

  2. 应用 Window 层级范围是 1-99,子 WIndow 层级范围是 1000-1999,系统 Window 层级范围是 2000-2999

  3. 可以指定 type 属性为 TYPE_APPLICATION_OVERLAY,让它处于系统层级,同时注意权限问题,如下所示:

    if (!Settings.canDrawOverlays(this)) {
     Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION,  Uri.parse("package:" + getPackageName()));
                startActivityForResult(intent, OVERLAY_PERMISSION_REQ_CODE);
            } else {
                alterWindow();
            }
    

WindowManager 继承于 ViewManager,可以实现往 Window 中添加View、更新 View、删除 View 的功能:

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 设置 onTouchlistener 监听,来实现拖动效果。


二、Window 的内部机制

从 WindowManager 的三个方法可以看出作用对象都是 View,所以 Window 实际上是以 View 的形式存在的。每一个 Window 都对应一个 View 和一个 ViewRootImpl,Window 和 View 通过 ViewRootImpl 来建立连接。

WindowManager 是一个接口,所以它的 addView 方法由它的实现类 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);
    }

WindowManagerImpl 没有直接实现这三个方法,而是将操作委托给了 WindowManagerGlobal 对象。

1、Window 的添加过程

WindowManagerGlobal 的 addView 方法分为下面几步:

  • 检查参数是否合法,如果是子 Window 还需要调整布局参数
        // WindowManagerGlobal#addView
        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");
        }
  • 创建 ViewRootImp 并将 View 添加到列表中
    // WindowManagerGlobal
    // 存储所有 Window 对应的 View
    private final ArrayList mViews = new ArrayList();
    // 存储所有 Window 对应的 ViewRootImpl
    private final ArrayList mRoots = new ArrayList();
    // 存储所有 Window 对应的布局参数
    private final ArrayList mParams =
            new ArrayList();
            // WindowManagerGlobal#addView
            ViewRootImpl root;
            root = new ViewRootImpl(view.getContext(), display);
            view.setLayoutParams(wparams);
            mViews.add(view);
            mRoots.add(root);
            mParams.add(wparams);
  • 通过 ViewRootImpl 来更新界面
    // WindowManagerGlobal#addView
    root.setView(view, wparams, panelParentView);
    // ViewRootImpl#setView
    requestLayout();

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

    // View 绘制入口,通过 IPC 过程最终会调用到下面 WindowManagerService 的 addWindow 方法
    void scheduleTraversals() {
        if (!mTraversalScheduled) {
            mTraversalScheduled = true;
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
            if (!mUnbufferedInputDispatch) {
                scheduleConsumeBatchedInput();
            }
            notifyRendererOfFramePending();
            pokeDrawLockIfNeeded();
        }
    }
// WindowManagerService
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,
            InsetsState outInsetsState)

如此一来,Window 添加 View 的请求就交给了 WindowManagerService。

2、Window 的删除过程

同添加过程一样,删除过程也是先通过 WindowManagerImpl,再进一步通过 WindowManagerGlobal 来实现的,实现如下:

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

首先通过 findViewLocked 找到待删除的 View 的索引,然后通过 removeViewLocked 来做进一步的删除。

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

        if (view != null) {
            InputMethodManager imm = view.getContext().getSystemService(InputMethodManager.class);
            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);
            }
        }
    }

上面代码中先调用 die 方法,然后将 view 添加到 mDyingViews 中,表示待删除的 View 列表。其中 die 方法如下所示:

    // ViewRootIml
    boolean die(boolean immediate) {
        // Make sure we do execute immediately if we are in the middle of a traversal or the damage
        // done by dispatchDetachedFromWindow will cause havoc on return.
        if (immediate && !mIsInTraversal) {
            doDie();
            return false;
        }

        if (!mIsDrawing) {
            destroyHardwareRenderer();
        } else {
            Log.e(mTag, "Attempting to destroy the window while drawing!\n" +
                    "  window=" + this + ", title=" + mWindowAttributes.getTitle());
        }
        mHandler.sendEmptyMessage(MSG_DIE);
        return true;
    }

如果是异步删除,那么会发送一个 MSG_DIE 消息,ViewRootImpl 中的 Handler 就会调用 doDie 方法;如果是立刻删除,那么就直接调用 doDie 方法。

在 doDie 内部会调用 dispatchDetachedFromWindow 方法,真正删除 View 的逻辑就在该方法内部实现,该方法主要做四件事:

  • 垃圾回收相关工作,比如清除数据和消息、移除回调
  • 通过 Seesion 的 remove 方法删除 Window,这也是一个 IPC 过程
  • 调用 View 的 dispatchDetachedFromWindow 方法,做一些资源回收的工作,比如终止动画、停止线程
  • 调用 WindowManagerGlobal 的 doRemoveView 方法刷新数据,把 mRoots、mParams 以及 mDyingViews 中关联当前 Window 的对象从列表中删除
3、Window 的更新过程
    // WindowManagerGlobal
    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 的 LayoutParams
        view.setLayoutParams(wparams);

        synchronized (mLock) {
            int index = findViewLocked(view, true);
            ViewRootImpl root = mRoots.get(index);
            // 更新该 Window 存储的布局参数列表
            mParams.remove(index);
            mParams.add(index, wparams);
            // 更新 ViewRootImpl 的 LayoutParams,
            root.setLayoutParams(wparams, false);
        }
    }

在此方法中会调用 ViewRootImpl 的 scheduleTraversals 方法,对 View 重新布局,包括测量、布局、重绘三个过程。它还会通过 WindowSession 来更新 Window 的视图,它同样是一个 IPC 过程。


三、Window 的创建过程

View 是 Android 中的视图的呈现方式,但是 View 不能单独存在,它必须依附在 Window 这个抽象的概念上。Android 中可以提供视图的地方有 Activity、Dialog、Toast,有视图的地方就有 Window。

1、Activity 的 Window 创建过程

Activity 中的 Window 创建过程涉及到 Activity 的启动过程,最终启动过程是由 ActivityThread 中的 performLaunchActivity 方法完成的,在这个方法内部会通过类加载器创建 Activity 的实例对象,并调用其 attach 方法来关联上下文。

// Activity#performLaunchActivity
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
    java.lang.ClassLoader cl = appContext.getClassLoader();
            activity = mInstrumentation.newActivity(
            cl, component.getClassName(), r.intent);

    if (activity != 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, r.configCallback,
            r.assistToken);
        ...
    }
...
}

在 Activity 的 attach 方法中,系统会创建 Activity 所属 Window 对象并为其设置回调接口:

// Activity#attach
mWindow = new PhoneWindow(this, window, activityConfigCallback);
mWindow.setWindowControllerCallback(this);
mWindow.setCallback(this);
mWindow.setOnWindowDismissedCallback(this);
mWindow.getLayoutInflater().setPrivateFactory(this);

到这里 Window 已经创建完成了,下面分析 Activity 的视图是怎么附属在 Window 上的。

由于 Activity 的视图由 setContentView 方法提供,我们就来看看这个方法的实现:

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

Activity 将实现交给了 Window 处理,Window 的具体实现是 PhoneWindow

    //PhoneWindow
    @Override
    public void setContentView(int layoutResID) {
        if (mContentParent == null) {
            // 1、创建 DecorView
            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 {
            // 2、将 View 添加到 DecorView 的 mContentParent 中
            mLayoutInflater.inflate(layoutResID, mContentParent);
        }
        mContentParent.requestApplyInsets();
        final Callback cb = getCallback();
        if (cb != null && !isDestroyed()) {
            // 3、回调 Activity 的 onContentChanged 方法通知 Activity 视图已经发生改变
            cb.onContentChanged();
        }
        mContentParentExplicitlySet = true;
    }

到这里 DecorView 创建并初始化完毕,Activity 的布局文件也成功添加到了 DecorView 的 mContentParent 中了,但是此时 DecorView 还没有被 WIndowManager 正式添加到 Window 中。

在 Activity 的 handleResumeActivity 中会调用 Activity 的 onResume 方法,接着会调用 Activity 的makeVisible 方法,在这个方法中,DecorView 真正完成了添加和显示过程:

    void makeVisible() {
        if (!mWindowAdded) {
            ViewManager wm = getWindowManager();
            wm.addView(mDecor, getWindow().getAttributes());
            mWindowAdded = true;
        }
        mDecor.setVisibility(View.VISIBLE);
    }
2、Dialog 的 Window 创建过程

在 Dialog 的构造方法中,会创建 Window 并设置监听:

    // Dialog
    Dialog(@NonNull Context context, @StyleRes int themeResId, boolean      createContextThemeWrapper) {
        ...
        mWindowManager = (WindowManager)    context.getSystemService(Context.WINDOW_SERVICE);

        final Window w = new PhoneWindow(mContext);
        mWindow = w;
        w.setCallback(this);
        w.setOnWindowDismissedCallback(this);
        w.setOnWindowSwipeDismissedCallback(() -> {
            if (mCancelable) {
                cancel();
            }
        });
        w.setWindowManager(mWindowManager, null, null);
        w.setGravity(Gravity.CENTER);

        mListenersHandler = new ListenersHandler(this);
    }

通过 setContentView 方法来初始化 DecorView 并将 Dialog 的视图添加到 DecorView 中:

    // Dialog
    public void setContentView(@LayoutRes int layoutResID) {
        mWindow.setContentView(layoutResID);
    }
    
    public void setContentView(@NonNull View view) {
        mWindow.setContentView(view);
    }   

    public void setContentView(@NonNull View view, @Nullable ViewGroup.LayoutParams             params) {
        mWindow.setContentView(view, params);
    }

最后使用 Dialog 的 show 方法,通过 WindowManager 将 DecorView 添加到 Window 中:

    // Dialog
    public void show() {
        ...
        mDecor = mWindow.getDecorView();
        mWindowManager.addView(mDecor, l);
        mShowing = true;
        ...
    }
3、Toast 的 Window 创建过程
  • Toast 具有定时取消功能,所以系统采用了 Handler
  • Toast 内部有两类 IPC 过程,第一类是 Toast 访问 NotificationManagerService(下面简称NMS),第二类是 NotificationManagerService 回调 Toast 里的 TN 接口

Toast 提供了 showcancel 方法,它们内部是一个 IPC 过程:

    // Toast
    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;
        final int displayId = mContext.getDisplayId();

        try {
            // 调用了 NMS 的 enqueueToast 方法
            service.enqueueToast(pkg, tn, mDuration, displayId);
        } catch (RemoteException e) {
            // Empty
        }
    }

    public void cancel() {
        mTN.cancel();
    }

它们最终会调用 handleShowhandleHide 两个方法真正实现显示和隐藏 Toast:

// Toast.TN
public void handleShow(IBinder windowToken) {
    ...
    mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
    // 将 Toast 视图添加到 Window 中
    mWM.addView(mView, mParams);
    ...
}
// Toast.TN
public void handleHide() {
    if (mView.getParent() != null) {
        if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);
        mWM.removeViewImmediate(mView);
    }
}

你可能感兴趣的:(开发艺术之Window)