【Android】深入理解Window和WindowManager

1、Window简介

Window是一个抽象的"窗口"对象,用来承载视图。它负责承载所有UI,所有View最终都必须附着在一个Window中才能显示。不管是Activity、Dialog,还是Toast,它们的视图都是附加在Window上面的。例如要在桌面上显示一个悬浮窗,就需要Window来实现。

实际上,Window是一个抽象类,它的唯一实现类是PhoneWindow,Activity中的DecorView,Dialog中的View都是在PhoneWindow中创建的。因此Window实际是View的直接管理者。

总结一下,Window是View的载体,View是Window的表现形式

2、Window和WindowManager

先来看看如何通过WindowManager添加一个Window:

java 复制代码
windowManager = (WindowManager) getSystemService(WINDOW_SERVICE);

// 创建一个要添加到 Window 的 View
floatingView = new TextView(this);
floatingView.setText("Window");
floatingView.setTextSize(20f);
floatingView.setBackgroundColor(Color.WHITE);
floatingView.setTextColor(Color.BLACK);

// Window 参数
WindowManager.LayoutParams params = new WindowManager.LayoutParams(
        WindowManager.LayoutParams.WRAP_CONTENT,
        WindowManager.LayoutParams.WRAP_CONTENT,
        0,
        0,
        PixelFormat.TRANSLUCENT
);

// Window 类型(应用内窗口)
params.type = WindowManager.LayoutParams.TYPE_APPLICATION;

// 不获取焦点、不拦截事件
params.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
| WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;

// 显示位置(右下角偏移)
params.gravity = Gravity.END | Gravity.BOTTOM;
params.x = 100;
params.y = 300;

// 添加到 WindowManager
windowManager.addView(floatingView, params);

这样就可以在页面右下角显示一个Window,而该Window的表现形式就是一个TextView

可以看到最后是通过addView方法将该Window显示在屏幕上的,该方法有两个参数:View和WindowManager.LayoutParams,View就是待会要显示的View,LayoutParams是该View的布局参数,关于LayoutParams有两个很重要的属性:type和flags。

2.1 type属性

type属性表示Window的类型,用于告诉系统这是哪种窗口。它的层级、权限、位置如何处理。类型分为三类:

  • 应用窗口:对应一个Activity
  • 子窗口:子窗口不能单独存在,需要附属在特定的父窗口中,比如常见的Dialog。
  • 系统窗口:是需要声明权限才能创建的窗口,比如Toast和系统状态栏

常用属性:

类型 说明 权限
TYPE_APPLICATION 默认 Activity Window;也可用来在 Activity 内添加悬浮 View
TYPE_APPLICATION_PANEL 附着在应用窗口上的面板,如 Dialog
TYPE_APPLICATION_ATTACHED_DIALOG 依附在 Activity 上的对话框类窗口
TYPE_APPLICATION_SUB_PANEL 子面板,比如 PopupWindow
TYPE_BASE_APPLICATION Activity 的基础窗口
TYPE_APPLICATION_OVERLAY 系统悬浮窗(微信悬浮球、悬浮记事本),需要权限(SYSTEM_ALERT_WINDOW

另外,窗口类型的区分就决定了Window的显示层序。如果一个Window是系统窗口,那么它一定会显示在其他应用程序Window上面。而控制显示层级顺序的属性是Z-Order,即Z轴的值。这里指定了哪种窗口,就相当于指定了Z轴的值:

  • 应用窗口:1~99
  • 子窗口:1000~1999
  • 系统窗口:2000~2999

值越大,显示的越上面,就是说type值大的Window可以盖住type值小的Window。

2.2 flags属性

flags表示Window的属性,通过flags可以控制Window的显示特性。比如决定窗口是否可触摸,是否覆盖状态栏,是否点击穿透等等。

常用选项:

flags 作用
FLAG_NOT_FOCUSABLE 不获取焦点 , 其他控件不会受影响(常用于悬浮窗)
FLAG_NOT_TOUCH_MODAL 自己范围外的触摸事件继续传递给底层窗口,只处理自身区域的触摸
FLAG_NOT_TOUCHABLE 不接收任何触摸事件(完全穿透),常用于屏幕水印、提示层
FLAG_LAYOUT_IN_SCREEN 允许 window 填满整个屏幕区域,全屏窗口使用
FLAG_FULLSCREEN 隐藏状态栏,进入全屏显示,用于视频全屏、游戏、沉浸模式
FLAG_SHOW_WHEN_LOCKED 在锁屏界面上显示Window,如来电界面、闹钟界面
FLAG_KEEP_SCREEN_ON 防止屏幕熄灭,用于视频播放、导航、长时间展示的窗口
FLAG_TURN_SCREEN_ON 显示窗口时自动亮屏,用于闹钟、定时提醒、特殊警告界面

另外设置flags还可以用下面的方式:

java 复制代码
// 在现有flags上"追加"一个flag
getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);

// 第二个参数表示"mask",通常与 flags 相同。
// setFlags 会根据 mask 覆盖,而不是追加。
getWindow().setFlags(
        WindowManager.LayoutParams.FLAG_FULLSCREEN,
        WindowManager.LayoutParams.FLAG_FULLSCREEN
);

2.3 WindowManager

WindowManager所提供的功能很简单,常用的只有三个方法:添加View、更新View和删除View。

这表明WindowManager是继承ViewManager这个接口的,同时WindowManager本身也是一个接口。

这三个方法是定义在ViewManager中的:

java 复制代码
public interface ViewManager {     
    public void addView(View view, ViewGroup.LayoutParams params);    
    public void updateViewLayout(View view, ViewGroup.LayoutParams params);    
    public void removeView(View view);
}

3、Window的内部机制

Window是Android中的窗口抽象,Window并不是实际存在的,它是以View的形式存在。在WindowManager中提供的三个接口方法都是针对View的,这说明View才是Window存在的实体。每个Window都对应一个View和一个ViewRootImpl,Window和View通过ViewRootImpl来建立联系。WindowManager是管理Window的接口,在实际使用中无法直接访问Window,对Window的访问必须通过WindowManager。为了弄清楚Window的内部机制,这里从Window的添加、删除和更新说起。

3.1 Window的添加过程

Window的添加过程是通过addView来实现的:

java 复制代码
windowManager.addView(floatingView, params);

WindowManager是一个接口,它的真正实现类是WindowManagerImpl类,在WindowManagerImpl中三个操作实现如下:

java 复制代码
@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
    applyTokens(params);
    mGlobal.addView(view, params, mContext.getDisplayNoVerify(), mParentWindow,
            mContext.getUserId());
}

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

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

可以看出,WindowManagerImpl并没有直接实现Window的三大操作,而是调用mGlobal去处理。这个mGlobal是WindowManagerGlobal实例,而且它还是一个APP全局单例。WindowManagerImpl这工作模式是典型的桥接模式,将所有操作全部委托给WindowManagerGlobal来实现。

这就是说WindowManagerGlobal是WindowManager的真正逻辑实现,接下来看看WindowManagerGlobal中的addView方法:

java 复制代码
public void addView(View view, ViewGroup.LayoutParams params,
        Display display, Window parentWindow, int userId) {
    // 参数检查
    // ...
    if (parentWindow != null) {
        // 如果是子窗口,那么就需要调整一些布局参数
        parentWindow.adjustLayoutParamsForSubWindow(wparams);
    } else {
        // ...
    }

    // ...

    // ViewRootImpl:窗口和系统交互的桥梁(事件分发、绘制)
    // panelParentView:用于存放父窗口的 View(如果是子窗口)
    ViewRootImpl root;
    View panelParentView = null;

    // 给整个Window添加过程加锁,确保线程安全
    synchronized (mLock) {
        // ...
        // 普通窗口和父窗口分开处理,创建ViewRootImpl
        if (windowlessSession == null) {
            root = new ViewRootImpl(view.getContext(), display);
        } else {
            root = new ViewRootImpl(view.getContext(), display,
                    windowlessSession, new WindowlessWindowLayout());
        }
        // 将窗口参数绑定到View
        view.setLayoutParams(wparams);
        // 记录View、ViewRootImpl、LayoutParams,用于全局窗口管理和事件分发
        mViews.add(view);
        mRoots.add(root);
        mParams.add(wparams);

        try {
            // 通过ViewRootImpl的setView方法真正把View添加到Window
            root.setView(view, wparams, panelParentView, userId);
            // ...
        } catch (RuntimeException e) {
            // ...
            }
            throw e;
        }
    }
}

方法很长,但是整体逻辑比较简单。由于WindowManagerGlobal是WindowManager真正的逻辑实现类,所以在WindowManagerGlobal内部会记录下要处理的Window,包括Window,Window对应的View,Window对应的ViewRootImpl。

在WindowManagerGlobal中通过这么几个列表来存储:

java 复制代码
@UnsupportedAppUsage
private final ArrayList<View> mViews = new ArrayList<View>();
@UnsupportedAppUsage
private final ArrayList<ViewRootImpl> mRoots = new ArrayList<ViewRootImpl>();
@UnsupportedAppUsage
private final ArrayList<WindowManager.LayoutParams> mParams =
        new ArrayList<WindowManager.LayoutParams>();
private final ArraySet<View> mDyingViews = new ArraySet<View>();
  • mViews:存放所有Window对应的View
  • mRoots:存放所有Window对应的ViewRootImpl
  • mParams:放每个 View 对应的窗口参数
  • mDyingViews:存放正在被移除但还未完成销毁的View

在addView中通过

csharp 复制代码
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);

三个方法来添加到列表中,用于全局窗口管理和事件分发。

接着看看ViewRootImpl中的setView方法的实现:

java 复制代码
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView,
            int userId) {
        synchronized (this) {
            if (mView == null) {
                // ...
                // 在被添加到WindowManager之前调用一次,触发View的测量和布局
                requestLayout();
                // ...

                try {
                    // 保证窗口尺寸、布局、Insets、安全区域都正确
                    mOrigWindowType = mWindowAttributes.type;
                    mAttachInfo.mRecomputeGlobalAttributes = true;
                    collectViewAttributes();
                    adjustLayoutParamsForCompatibility(mWindowAttributes,
                            mInsetsController.getAppearanceControlled(),
                            mInsetsController.isBehaviorControlled());
                    controlInsetsForCompatibility(mWindowAttributes);
                    
                    // 通过WindowSession来完成IPC调用,完成创建Window
                    Rect attachedFrame = new Rect();
                    final float[] compatScale = { 1f };
                    // 向系统 WindowManagerService 添加窗口
                    res = mWindowSession.addToDisplayAsUser(mWindow, mWindowAttributes,
                            getHostVisibility(), mDisplay.getDisplayId(), userId,
                            mInsetsController.getRequestedVisibleTypes(), inputChannel, mTempInsets,
                            mTempControls, attachedFrame, compatScale);
                    // ...
                } catch (RemoteException | RuntimeException e) {
                    // ...
                } finally {
                    if (restore) {
                        attrs.restore();
                    }
                }

                // ...
            }
        }
    }

在这段代码中,在被添加到WindowManager之前会调用requestLayout()方法完成异步刷新请求,看看它的具体实现:

java 复制代码
@Override
public void requestLayout() {
    if (!mHandlingLayoutInLayoutRequest) {
        checkThread();
        mLayoutRequested = true;
        scheduleTraversals();
    }
}

实际上,scheduleTraversals()方法就是View绘制的入口。

接着会调用mWindowSession.addToDisplayAsUser方法来完成Window的添加过程。mWindowSession的类型是IWindowSession,它是一个Binder对象,而IWindowSession是一个Binder接口。它的实现类在WindowManagerService中,这里通过mWindowSession完成了IPC通信。

这样就把添加Window的逻辑交给WindowManagerService来处理了,由于WindowManagerService比较复杂,这里就不过多介绍了。

在WindowManagerService中在Session类中完成Window的添加:

java 复制代码
@Override
public int addToDisplay(IWindow window, int seq, WindowManager.LayoutParams attrs,int viewVisibility, int displayId, Rect outFrame, Rect outContentInsets,
 Rect outStableInsets, Rect outOutsets, DisplayCutout.ParcelableWrapper outDisplayCutout, InputChannel outInputChannel, InsetsState outInsetsState) {
    return mService.addWindow(this, window, seq, attrs, viewVisibility, displayId, outFrame,outContentInsets, outStableInsets, outOutsets, outDisplayCutout, outInputChannel,outInsetsState);
}

最后总结一下:

  1. 首先调用WindowManagerImpl.addView(),在addView中把具体逻辑委托给WindowManagerGlobal.addView()
  2. WindowManagerGlobal.addView()中创建ViewRootImpl 赋值给了 root 。然后将 view,params,root 全部存入了各自的列表中。最后调用了 ViewRootImpl.setView()
  3. ViewRootImpl.setView()中,先调用requestlayout()完成异步刷新请求,接着会通过IWindowSession来完成最终的Window添加过程,IWindowSession 是一个 Binder 对象,真正的实现类是 Session,也就是说 Window 的添加过程试一次 IPC 的调用。最后在 Session 中会通过 WindowManageServer 来实现 Window 的添加。

3.2 Window的更新过程

Window的更新过程和创建过程一样,都是先通过WindowManagerImpl,再进一步通过WindowManagerGlobal来实现的。从WindowManagerGlobal中的updateViewLayout开始看起:

java 复制代码
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的param
    view.setLayoutParams(wparams);

    synchronized (mLock) {
        int index = findViewLocked(view, true);
        ViewRootImpl root = mRoots.get(index);
        mParams.remove(index);
        mParams.add(index, wparams);
        // 通知 ViewRootImpl 更新布局参数
        root.setLayoutParams(wparams, false);
    }
}

这个方法做的事情就比较简单了,首先是更新View中的LayoutParams,接着再更新ViewRootImpl中的LayoutParams,通过ViewRootImpl中的setLayoutParams方法完成,在这个方法中,会对View重新策略,布局,重绘。

除了View本身的重绘之外,ViewRootImpl还会通过WindowSession来更新Window视图,这个过程是由WindowManagerServer的relayoutWindow来实现的,这同样是一个IPC过程。

3.3 Window的删除过程

还是从WindowManagerGlobal方法看起:

java 复制代码
// UnsupportedAppUsage表示这是系统内部API,不对三方应用公开,但应用能使用
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
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在views列表中的索引,然后调用removeViewLocked方法来做进一步的删除:

java 复制代码
private void removeViewLocked(int index, boolean immediate) {
    // 先根据index取出对应窗口(root和view) 
    ViewRootImpl root = mRoots.get(index);
    View view = root.getView();

    if (root != null) {
        root.getImeFocusController().onWindowDismissed();
    }
    // root.die():销毁窗口
    // immediate为true:立即销毁窗口
    // immediate为false:请求延迟销毁
    boolean deferred = root.die(immediate);
    if (view != null) {
        // 把View从它的父亲(ViewRootImpl)解绑
        view.assignParent(null);
        if (deferred) {
            // 如果延迟销毁,不能马上删除View,因为ViewRootImpl还没真正死完
            // 将当前View放入mDyingViews
            mDyingViews.add(view);
        }
    }
}

在removeViewLocked方法中,删除操作是通过ViewRootImpl实现的。在WindowManager中提供两种删除接口removeViewremoveViewImmediate分别是同步删除和异步删除。

一般不用removeViewImmediate删除Window,以免发生意错误。

这里主要说异步删除的情况,具体的删除操作由ViewRootImpl的die方法完成。在异步删除的情况下,die方法只是发送了一个请求删除的信息后就立刻返回了,这个时候View并没有完成删除操作,所以最后会将其添加到mDyingViews中,mDyingViews表示待删除的View列表。看看ViewRootImpl中的die方法实现:

java 复制代码
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) {
        // 立即销毁:调用者要求立即销毁并且当前不在View测量/布局/绘制(Traversal)中
        // 执行doDie方法立即删除窗口
        doDie();
        // 返沪false表示不需要延迟处理
        return false;
    }
    // 若不是立即销毁,处理硬件加速层
    if (!com.android.graphics.hwui.flags.Flags.removeVriSketchyDestroy()) {
        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;
}

die方法内部只是一个简单的判断,如果是异步删除,那么就发送一个MSG_DIE的消息ViewRootImpl中的Handle会处理此消息并调用doDie()方法。如果是同步删除,那么就不发消息直接调用doDie()方法,这就是两种删除的区别。看看doDie()方法的具体实现:

java 复制代码
void doDie() {
        // Window操作必须在创建ViewRootImpl的线程(主线程)执行,否则会导致UI状态错乱
        checkThread();
        if (LOCAL_LOGV) Log.v(mTag, "DIE in " + this + " of " + mSurface);
            // 进入同步块,防止多线程重复销毁 
        synchronized (this) {
            if (mRemoved) {
                return;
            }
            mRemoved = true;
            // ...

            if (mAdded) {
                // 真正的删除逻辑是在此方法中
                dispatchDetachedFromWindow();
            }

            destroyHardwareRenderer();

            // ...

            mAdded = false;
            // ...
        }
        // ...
        WindowManagerGlobal.getInstance().doRemoveView(this);
    }

在doDie方法内部,重点在dispatchDetachedFromWindow内部,在该方法中,实现了Window的删除逻辑:

java 复制代码
void dispatchDetachedFromWindow() {
    // Make sure we free-up insets resources if view never received onWindowFocusLost()
    // because of a die-signal
    mInsetsController.onWindowFocusLost();
    mFirstInputStage.onDetachedFromWindow();
    if (mView != null && mView.mAttachInfo != null) {
        mAttachInfo.mTreeObserver.dispatchOnWindowAttachedChange(false);
        // 分析1
        mView.dispatchDetachedFromWindow();
    }
    
    // 分析2
    mAccessibilityInteractionConnectionManager.ensureNoConnection();
    mAccessibilityInteractionConnectionManager.ensureNoDirectConnection();
    removeSendWindowContentChangedCallback();
    if (android.view.accessibility.Flags.preventLeakingViewrootimpl()
            && mAccessibilityInteractionController != null) {
        mAccessibilityInteractionController.destroy();
        mAccessibilityInteractionController = null;
    }

    destroyHardwareRenderer();

    setAccessibilityFocus(null, null);

    mInsetsController.cancelExistingAnimations();

    mView.assignParent(null);
    mView = null;
    mAttachInfo.mRootView = null;

    destroySurface();

    if (mInputQueueCallback != null && mInputQueue != null) {
        mInputQueueCallback.onInputQueueDestroyed(mInputQueue);
        mInputQueue.dispose();
        mInputQueueCallback = null;
        mInputQueue = null;
    }
    try {
        // 分析3
        mWindowSession.remove(mWindow.asBinder());
    } catch (RemoteException e) {
    }
    // Dispose receiver would dispose client InputChannel, too. That could send out a socket
    // broken event, so we need to unregister the server InputChannel when removing window to
    // prevent server side receive the event and prompt error.
    if (mInputEventReceiver != null) {
        mInputEventReceiver.dispose();
        mInputEventReceiver = null;
    }

    unregisterListeners();
    unscheduleTraversals();
}

上面方法主要干了三件事:

  1. 分析1处:调用View的dispatchDetachedFromWindow方法,在该方法内部会调用onDeteachedFromWindow方法,该方法在做自定义View时,可以在该方法内部做一些资源回收工作,比如终止动画、停止线程等。
  2. 分析2处,垃圾回收相关工作,比如清除数据和消息,移除回调等。
  3. 分析3处,通过Session的remove方法来删除Window,这里也是一个IPC过程,真正删除的地方还是WindowManagerService。

4、Window的创建过程

通过上面的分析可以看出,View是Window地表现形式,而Window是View地载体,因此任何有视图地地方都有Window,比如Activity、Dialog、Toast等。本节将分析这些视图元素中的Window创建过程,这非常有利于理解Android系统。

4.1 Activity的Window创建过程

要分析Activity的Window创建过程,必须要了解Activity的的启动流程,这里就先不过多说启动流程相关的内容了,在Activity启动的过程中,最终会由ActivityTread中的performLaunchActivity()来完成整个启动过程。

在这个方法中会通过类加载器创建Activity的实例对象,并调用其attach方法为其关联运行过程中所依赖的一系列上下文环境变量:

java 复制代码
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
    // ...
    Activity activity = null;
    try {
        java.lang.ClassLoader cl;
        if (isSandboxedSdkContextUsed) {
            cl = activityBaseContext.getApplicationContext().getClassLoader();
        } else {
            cl = activityBaseContext.getClassLoader();
        }
        // ...
    } catch (Exception e) {
        // ...
    }

    try {
        // ...
            Window window = null;
            // ...
            activity.attach(activityBaseContext, 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.activityConfigCallback,
                    r.assistToken, r.shareableActivityToken, r.initialCallerInfoAccessToken);

    }
    return activity;
}

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

java 复制代码
mWindow = new PhoneWindow(this, window, activityConfigCallback);
mWindow.setWindowControllerCallback(mWindowControllerCallback);
mWindow.setCallback(this);
mWindow.setOnWindowDismissedCallback(this);
mWindow.getLayoutInflater().setPrivateFactory(this);

这里创建了PhoneWindow实例,而且设置了回调,同时Acivity自己就实现了接口,所以当Window接收到外界的状态改变时就会回调Activity的方法。Callback接口中有一些我们比较熟悉的方法:

java 复制代码
public interface Callback {
    // 处理键盘事件(物理键、软键、音量键等) 
    public boolean dispatchKeyEvent(KeyEvent event);
    // 当菜单被打开时回调
    boolean onMenuOpened(int featureId, @NonNull Menu menu);
    // 菜单项被点击时回调
    boolean onMenuItemSelected(int featureId, @NonNull MenuItem item);
    // 窗口焦点变化时回调
    public void onWindowFocusChanged(boolean hasFocus);
    // 窗口被添加到 WindowManager 时回调
    public void onAttachedToWindow();
    // 窗口从 WindowManager 移除时回调
    public void onDetachedFromWindow();
    // ...
}

这些方法就是在attach方法中设置的回调。到这里Window已经创建完成了,下面分析一下Activity视图是怎么依附到Window上的,由于Activity的视图时从setContentView方法提供的,接下来我们看看setContentView方法:

java 复制代码
public void setContentView(@LayoutRes int layoutResID) {
    getWindow().setContentView(layoutResID);
    initWindowDecorActionBar();
}

这里发现其实是调用了PhoneWindow的setContentView方法

java 复制代码
@Override
public void setContentView(int layoutResID) {
    // 如果没有DecorView,那么就创建它
    if (mContentParent == null) {
        //  这个方法创建DecorView
        installDecor();
    } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
        mContentParent.removeAllViews();
    }
    
    // 布局加载逻辑
    // if中只是开启了过渡动画,else中直接加载布局,没有过渡动画
    if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
        final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
                getContext());
        transitionTo(newScene);
    } else {
        // 普通布局加载,直接使用LayoutInflater.inflate
        // 把布局XML填充到mContentParent中
        mLayoutInflater.inflate(layoutResID, mContentParent);
    }
    mContentParent.requestApplyInsets();
    // 回调Activity的onContentChanged方法通知Activity视图已经发生改变
    final Callback cb = getCallback();
    if (!isDestroyed()) {
        if (cb != null) {
            // 回调onContentChanged方法
            // Activity可以在这里做findViewById()初始化控件 
            cb.onContentChanged();
        }
        if (mDecorContentParent != null) {
            mDecorContentParent.notifyContentChanged();
        }
    }
    mContentParentExplicitlySet = true;
}

这个方法主要做了以下几件事:

  • 如果DecorView不存在就创建DecorView,DecorView是Activity中的顶级View,一般来说它包括标题栏和内容栏。创建DecorView由installDecor();方法完成。
  • 将View添加到DecorView的mContentparent中:这一步非常简单,第一步已经创建过DecorView了,这一步直接将Activity的视图添加到DecorView中的mContentParent即可:通过mLayoutInflater.inflate(layoutResID, mContentParent);将布局XML填充到mContentParent中。
  • 回调Activity的onContentChanged方法通知Activity视图已经发生改变:也非常简单,由于Activity已经实现了Window的Callback接口,回调此方法表示Activity的视图已经被添加到DecorView的mContentparent中了,需要通知Activity,使其做相应的处理。

通过上方的步骤,现在DecorView已经创建并初始化完成,Activity的布局也添加到DecorView的内容栏中,但是这个时候DecorView还没有被WindowManager添加到Window中

在ActivityThread的handleResumeActivity会调用Activity的onResume方法,并且会调用ViewManager的addView方法把DecorView添加到WindowManager中:

java 复制代码
    @Override
    public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward,
            String reason) {
        //调用Activity的onResume方法
        final ActivityClientRecord r = performResumeActivity(token, finalStateRequest, reason);
        // ...
        if (r.window == null && !a.mFinished && willBeVisible) {
            r.window = r.activity.getWindow();
            View decor = r.window.getDecorView();
            decor.setVisibility(View.INVISIBLE);
            // 获取WindowMagaer
            ViewManager wm = a.getWindowManager();
            WindowManager.LayoutParams l = r.window.getAttributes();
            a.mDecor = decor;
            l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
         
            if (a.mVisibleFromClient) {
                if (!a.mWindowAdded) {
                    a.mWindowAdded = true;
                    // 把DecorView添加到WindowManager
                    wm.addView(decor, l);
                } 
                // ...
    }

到这里Activity的Window创建过程分析完毕

4.2 Dialog的Window创建过程

Dialog的Window创建过程和Activity类似,可以分为如下几个步骤:

  1. 创建Window:

创建Window的地方就是直接在Dialog的构造函数中:

java 复制代码
Dialog(@UiContext @NonNull Context context, @StyleRes int themeResId,
        boolean createContextThemeWrapper) {
    // ...

    mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
    
    // 创建PhoneWindow
    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);
}

可以发现这里和Activity一样,都是创建PhoneWindow,同时设置一些回调。

  1. 初始化DecorView,并将Dialog的视图添加到DecorView中。

这个步骤也和Activity类似:

java 复制代码
public void setContentView(@NonNull View view) {
    mWindow.setContentView(view);
}

这个过程也和Activity类似,都是通过Window去添加指定的布局文件。

  1. 将DecorView添加到Window中并显示

在Dialog的show方法中,会通过WindowManager将DecorView添加到Window中:

java 复制代码
public void show() {
        // ...
        // 获取DecorView
        mDecor = mWindow.getDecorView();

        // ...
        // 添加到WindowManager
        mWindowManager.addView(mDecor, l);
        // ...
        // 发送回调消息
        // 目的是在视图完成添加并可见后,通知业务逻辑。
        sendShowMessage();
}

从上面三个步骤可以发现,Dialog的Window创建和Activity的Window创建很类似,二至几乎没有上面区别。

当Dialog关闭时,它会通过WindowManager来移除DecorView,mWindowManager.removeViewImmediate(mDecor)

普通的Dialog有一个特殊之处,就是必须采用Activity的Context,如果采用Application的Context,就会报错:

java 复制代码
Caused by: android.view.WindowManager$BadTokenException: Unable to add window -- token null is not valid; is your activity running?

错误信息很明确,是没有应用Token导致的,而Token一般只有Activity拥有,所以这里只需要用Activity作为Context来显示对话框即可。

另外,系统 Window 比较特殊,它可以不需要Token,我们可以将Dialog的Window的Type修改为系统类型就可以了:

java 复制代码
Dialog dialog = new Dialog(getApplicationContext());
dialog.setContentView(R.layout.dialog_layout);
Window window = dialog.getWindow();
if (window != null) {
    // 设置系统类型 Window,使其不依赖 Activity Token
    window.setType(WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY);
}
dialog.show();

4.3 Toast的Window创建过程

Toast也是基于Window来实现的,不过它的工作过程稍微会复杂一点。由于Toast具备定时取消功能,所以在 Toast 的内部有两类 IPC 的过程,第一类是 Toast 访问 NotificationManagerService 过程。第二类是 NotificationManagerServer 回调 Toast 里的 TN 接口。下面将 NotificationManagerService 简称为 NMS。

Toast 属于系统 Window,内部视图有两种定义方式,一种是系统默认的,另一种是通过 setView 方法来指定一个 View(setView 方法在 android 11 以后已经废弃了,不会再展示自定义视图),它们都对应 Toast 的一个内部成员 mNextView

Toast提供了show和cancel方法,分别用于显示和隐藏Toast,但是它们都是一个IPC过程:

java 复制代码
public void show() {
        // ...

        INotificationManager service = getService();
        String pkg = mContext.getOpPackageName();
        TN tn = mTN;
        if (Flags.toastNoWeakref()) {
            tn.mNextView = mNextView;
        } else {
            tn.mNextViewWeakRef = new WeakReference<>(mNextView);
        }
        final boolean isUiContext = mContext.isUiContext();
        final int displayId = mContext.getDisplayId();

        boolean wasEnqueued = false;
        try {
            if (Compatibility.isChangeEnabled(CHANGE_TEXT_TOASTS_IN_THE_SYSTEM)) {
                if (mNextView != null) {
                    // It's a custom toast
                    // 调用NMS
                    wasEnqueued = service.enqueueToast(pkg, mToken, tn, mDuration, isUiContext,
                            displayId);
                } else {
                    // ...
                }
            } else {
                // ...
            }
        } catch (RemoteException e) {
            // Empty
        } finally {
            // ...
        }
    }
java 复制代码
public void cancel() {
    if (Compatibility.isChangeEnabled(CHANGE_TEXT_TOASTS_IN_THE_SYSTEM)
            && mNextView == null) {
        try {
            // 调用NMS 
            getService().cancelToast(mContext.getOpPackageName(), mToken);
        } catch (RemoteException e) {
            // Empty
        }
    } else {
        mTN.cancel();
    }
}

从上面的代码可以看出,显示和隐藏Toast都需要通过NMS来实现,由于NMS运行在系统的进程中,所以只能通过IPC来显示和隐藏Toast。

注意这里的TN类,它是一个Binder类,在Toast和NMS进行IPC的过程中,当NMS处理Toast的显示或隐藏请求时会跨进程回调TN中的方法,这个时候由于TN运行在Binder线程池中,所以需要通过Handler将其切换到当前线程中。这里的当前线程指的是发送Toast所在的线程。

不过要注意的是这里使用了Handler,所以这意味着Toast无法在没有Looper的线程中弹出,这是因为Handler需要使用Looper才能完成切换线程的功能。

既然是先和NMS通信,简单说一下NMS中的代码,代码较多,简单说一下流程:

  1. 调用NMS方法携带3个参数,分别是当前包名、tn远程回调和Toast时长。NMS先把Toast请求封装为ToastRecord对象,添加到mToastQueue队列中。
  2. mToastQueue是一个50容量的ArrayList,这样做是为了防止DOS,假如通过某个方法大量的连续弹出Toast,这将导致其他应用没机会弹出Toast。
  3. 正常情况下,是达不到存储上限的,当ToastRecord被添加到mToastQueue中,NMS会通过showNextToastLocked方法来显示当前的Toast。
  4. Toast的显示是由ToastRecord的callback来完成的,而这个callback实际上就是传递进来的tn,所以最终调用TN中的方法会是在Binder线程池中。
  5. Toast显示后,NMS还会通过scheduleTimeoutLocked方法发送一个延迟消息,同样是经过callback回调,这个就是取消Toast的动作。

所以从上面分析来看,Toast和NMS进行了多次IPC通信,但是真正去显示Toast的还是得由Toast类来完成,也就是上面所说的TN类:

java 复制代码
private static class TN extends ITransientNotification.Stub {
    // ...
    final Handler mHandler;
    // ...

    
    TN(Context context, String packageName, Binder token, List<Callback> callbacks,
            @Nullable Looper looper) {
        IAccessibilityManager accessibilityManager = IAccessibilityManager.Stub.asInterface(
                ServiceManager.getService(Context.ACCESSIBILITY_SERVICE));
        mPresenter = new ToastPresenter(context, accessibilityManager, getService(),
                packageName);
        mParams = mPresenter.getLayoutParams();
        mPackageName = packageName;
        mToken = token;
        mCallbacks = callbacks;

        mHandler = new Handler(looper, null) {
            @Override
            public void handleMessage(Message msg) {
                switch (msg.what) {
                    case SHOW: {
                        IBinder token = (IBinder) msg.obj;
                        handleShow(token);
                        break;
                    }
                    case HIDE: {
                        handleHide();
                        // Don't do this in handleHide() because it is also invoked by
                        // handleShow()
                        mNextView = null;
                        break;
                    }
                    case CANCEL: {
                        handleHide();
                        // Don't do this in handleHide() because it is also invoked by
                        // handleShow()
                        mNextView = null;
                        try {
                            getService().cancelToast(mPackageName, mToken);
                        } catch (RemoteException e) {
                        }
                        break;
                    }
                }
            }
        };
    }

   // ...
}

这里我们会发现使用了Handler,但是前面说过TN的回调其实是在Binder线程池中,即运行在子线程中。当我们在主线程中弹出Toast肯定没问题,那么在子线程中弹出Toast呢?

java 复制代码
new Thread(() -> {
    Toast.makeText(this, "你好", Toast.LENGTH_SHORT).show();
}).start();

运行后报以下错误:

java 复制代码
java.lang.NullPointerException: Can't toast on a thread that has not called Looper.prepare()
at com.android.internal.util.Preconditions.checkNotNull(Preconditions.java:174)
at android.widget.Toast.getLooper(Toast.java:188)
at android.widget.Toast.<init>(Toast.java:173)
at android.widget.Toast.makeText(Toast.java:518)
at android.widget.Toast.makeText(Toast.java:507)
at com.example.windowtest.MainActivity.lambda$onCreate$1$com-example-windowtest-MainActivity(MainActivity.java:36)
at com.example.windowtest.MainActivity$$ExternalSyntheticLambda1.run(D8$$SyntheticClass:0)

可以发现这里的错误并不是说View不能再子线程中绘制,而是说没有Looper,所以我们给子线程也获取以下Looper:

java 复制代码
new Thread(() -> {
    Looper.prepare();
    Toast.makeText(this, "你好", Toast.LENGTH_SHORT).show();
}).start();

运行效果:

是可以正常弹出Toast的,所以这里我们知道Toast不能显示在子线程其实并不是一个错误,Toast是可以在子线程中弹出的。

相关推荐
luod43 分钟前
RabbitMQ简单生产者和消费者实现
java·rabbitmq
AllBlue1 小时前
安卓调用unity中的方法
android·unity·游戏引擎
okseekw1 小时前
Java抽象类详解:从“不能生孩子”的类到模板设计模式实战
java
古城小栈1 小时前
Spring中 @Transactional 和 @Async注解 容易不消停
java·spring
q_19132846951 小时前
基于Springboot+uniapp的智慧停车场收费小程序
java·vue.js·spring boot·小程序·uni-app·毕业设计·计算机毕业设计
JessonLv1 小时前
单商户商城说明文档-支持小程序及APP,JAVA+VUE开发
java·软件开发
鲸沉梦落1 小时前
网络原理-初识
java·网络
任子菲阳1 小时前
学Java第五十二天——IO流(下)
java·开发语言·intellij-idea
ArabySide1 小时前
【Java Web】过滤器的核心原理、实现与执行顺序配置
java·spring boot·java-ee