《Android开发艺术探索》第8章-理解 Window 和 WindowManager 读书笔记

1 Window 和 WindowManager

1.1 说一说对于 Window 的理解。

  • 从概念上来说,Window 表示一个窗口的概念。
  • 从类结构上来说,Window 是一个抽象类,它的具体实现是 PhoneWindow,同时也是唯一的实现。
  • 从获取方式上来说,在实际使用中无法直接访问 Window,外界访问 Window 必须要通过 WindowManager,也就是说,WindowManager 是外界访问 Window 的入口;
  • 从内部机制上来说,Window 的具体实现位于 WindowManagerService 中,WindowManagerWindowManagerService 的交互是一个 IPC 过程。
  • 从和 View 的关系上来说,Android 中所有的视图都是通过 Window 来呈现的,不管是 ActivityDialog 还是 Toast,以及 PopupWindow,菜单等,它们的视图实际上都是附加在 Window 之上的,因此 Window 实际是 View 的直接管理者。

Abstract base class for a top-level window look and behavior policy. An
instance of this class should be used as the top-level view added to the
window manager. It provides standard UI policies such as a background, title
area, default key processing, etc.

1.2 WindowManager 的常用方法有哪些?

WindowManager 是一个接口,常用的方法有三个,即添加 View,更新 View 和删除 View,这三个方法是定义在 ViewManager 接口中,WindowManager 是继承于 ViewManager 的。

«interface» ViewManager +addView(View view, ViewGroup.LayoutParams params) : void +updateViewLayout(View view, ViewGroup.LayoutParams params) : void +removeView(View view) : void «interface» WindowManager +removeViewImmediate(View view) : void +getDefaultDisplay() : Display extends

可以看到,addView(添加 View),updateViewLayout (更新 View)和 removeView(删除 View) 这三个接口方法都是针对 View 来操作的。

1.3 WindowManager 中有一个静态内部类 LayoutParams,绘制一下它的类图。

ViewGroup_LayoutParams +int width +int height LayoutParams +int x +int y +float horizontalWeight +float verticalWeight +int type +int flags +int softInputMode +int gravity +float horizontalMargin +float verticalMargin +int format +int windowAnimations +float alpha +float dimAmount +IBinder token +String packageName +int screenOrientation +float preferredRefreshRate +int systemUiVisibility -CharSequence mTitle +setTitle(CharSequence title) : void +getTitle() : CharSequence Parcelable extends implements

参考链接:·WindowManager.LayoutParams详解总结和对应实例

1.4 Window 的类型分类

WindowManager.LayoutParamstype参数表示 Window 的层级范围。

类型含义层级范围说明举例
Application windowsnormal top-level application windowsFIRST_APPLICATION_WINDOW(1)~LAST_APPLICATION_WINDOW(99)For these types of windows, the token must be set to the token of the activity they are a part of (this will normally be done for you if token is null比如 Activity的窗口类型是 TYPE_BASE_APPLICATION(1)
Sub-windowsassociated with another top-level windowFIRST_SUB_WINDOW(1000)~ LAST_SUB_WINDOW(1999)For these types of windows, the token must be the token of the window it is attached to
System windowsspecial types of windows for use by the system for specific purposesFIRST_SYSTEM_WINDOW(2000)~ LAST_SYSTEM_WINDOW(2999)They should not normally be used by applications, and a special permission is required to use them状态栏窗口的类型是TYPE_STATUS_BAR(2000),瞬态通知(如 Toast)的窗口类型是 TYPE_TOAST(2005)

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

应该注意的是,在有些手机上,需要手动打开 android.permission.SYSTEM_ALERT_WINDOW 才可以。

Window 是分层的,每个 Window 都有对应的 z-ordered,层级大的会覆盖在层级小的 Window 之上。

1.5 说一说实际开发中 Window 的应用

  1. 在应用接管通话页面时,通话页退到后台了,需要显示一个悬浮窗来显示当前通话的时长;

  2. 在通过辅助权限来做权限申请时,显示一个 Window 在最顶层,防止用户再去操作页面;

  3. 有的应用的锁屏页面就是通过 Window 来实现的。

2 Window 的内部机制

2.1 Window 和 View 的关系

Window 是一个抽象的概念,每一个 Window 都对应着一个 ViewViewRootImplWindowView 通过 ViewRootImpl 来建立联系。所以,Window 并不是实际存在的,它是以 View 的形式存在的。View 才是 Window 存在的实体。

2.2 Window 的添加过程

public class WindowActivity extends Activity implements View.OnTouchListener {

    private WindowManager mWindowManager;
    private Button mFloatingButton;
    private WindowManager.LayoutParams mLayoutParams;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_window);
        mWindowManager = (WindowManager) getSystemService(Context.WINDOW_SERVICE);
    }

    public void button(View view) {
        mFloatingButton = new Button(this);
        mFloatingButton.setText("click me");
        mLayoutParams = new WindowManager.LayoutParams(WindowManager.LayoutParams.WRAP_CONTENT,
                WindowManager.LayoutParams.WRAP_CONTENT, 0, 0, PixelFormat.TRANSPARENT);
        // 如果不写下面这行,那么锁屏后竟然无法解锁了。
        mLayoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL // 设置了这个标记,即便当前 Window 具有焦点,
                // 也允许将当前 Window 区域以外的单击事件传递给底层的 Window;否则,当前窗口将消费所有的点击事件,不管点击事件是否在窗口之内。
                | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE // 表示 Window 不需要获取焦点,也不需要接收各种输入事件,
                // 此标记会同时启用 FLAG_NOT_TOUCH_MODAL,最终事件会直接传递给下层的具有焦点的 Window
                | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED; // 让 Window 显示在锁屏的界面上。
        mLayoutParams.type = WindowManager.LayoutParams.TYPE_SYSTEM_ERROR;
        mLayoutParams.gravity = Gravity.LEFT | Gravity.TOP;
        mLayoutParams.x = 100;
        mLayoutParams.y = 300;
        mFloatingButton.setOnTouchListener(this);
        // 添加
        mWindowManager.addView(mFloatingButton, mLayoutParams);
    }
}

从上面的代码中,可以看到:Window 的添加过程需要通过 WindowManageraddView 来实现。但是,WindowManager 是一个接口,那么它的实现类是谁呢?

其实在 ContextImpl 里有一个静态代码块,包含了初始化 WindowManager 的代码:

static {
	 registerService(WINDOW_SERVICE, new ServiceFetcher() {
	                Display mDefaultDisplay;
	                public Object getService(ContextImpl ctx) {
	                    Display display = ctx.mDisplay;
	                    if (display == null) {
	                        if (mDefaultDisplay == null) {
	                            DisplayManager dm = (DisplayManager)ctx.getOuterContext().
	                                    getSystemService(Context.DISPLAY_SERVICE);
	                            mDefaultDisplay = dm.getDisplay(Display.DEFAULT_DISPLAY);
	                        }
	                        display = mDefaultDisplay;
	                    }
	                    // 构造了 WindowManagerImpl 的实例
	                    return new WindowManagerImpl(display);
	                }});
}
private static final HashMap<String, ServiceFetcher> SYSTEM_SERVICE_MAP =
            new HashMap<String, ServiceFetcher>();

private static void registerService(String serviceName, ServiceFetcher fetcher) {
        if (!(fetcher instanceof StaticServiceFetcher)) {
            fetcher.mContextCacheIndex = sNextPerContextServiceCacheIndex++;
        }
        SYSTEM_SERVICE_MAP.put(serviceName, fetcher);
    }

再看一下 ContextImplgetSystemService 方法:

public Object getSystemService(String name) {
   	ServiceFetcher fetcher = SYSTEM_SERVICE_MAP.get(name);
    return fetcher == null ? null : fetcher.getService(this);
}

这里获取的正是 WindowManagerImpl 的实例。所以,WindowManager 接口的真正实现是 WindowManagerImpl 类。

我们看一下 WindowManagerImpl 类如何实现添加 Window 的:

public final class WindowManagerImpl implements WindowManager {
    private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();
    private final Display mDisplay;
    private final Window mParentWindow;

    public WindowManagerImpl(Display display) {
        this(display, null);
    }

    @Override
    public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
        applyDefaultToken(params);
        mGlobal.addView(view, params, mDisplay, 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);
    }

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

可以看到,WindowManagerImpl 并没有直接实现 Window 的添加,更新以及删除,而是全部交给了 WindowManagerGlobal 来处理。

WindowManagerGlobal单例类,这一点需要特别注意一下。

WindowManagerImplWindowManagerGlobal 的关系用到了桥接模式,如下:

WindowManagerImpl -WindowManagerGlobal mGlobal +addView(View view, ViewGroup.LayoutParams params) : void +updateViewLayout(View view, ViewGroup.LayoutParams params) : void +removeView(View view) : void +removeViewImmediate(View view) : void WindowManagerGlobal +addView(View view, ViewGroup.LayoutParams params, Display display, Window parentWindow) : void +updateViewLayout(View view, ViewGroup.LayoutParams params) : void +removeView(View view, boolean immediate) Aggregation

先介绍 WindowManagerGlobal 里几个重要的列表:

// 存储的是所有 Window 对应的 View 对象
private final ArrayList<View> mViews = new ArrayList<View>();
// 存储的是所有 Window 对应的 ViewRootImpl 对象
private final ArrayList<ViewRootImpl> mRoots = new ArrayList<ViewRootImpl>();
// 存储的是所有 Window 对应的布局参数
private final ArrayList<WindowManager.LayoutParams> mParams =
       new ArrayList<WindowManager.LayoutParams>();
// 存储的是那些正在被删除的 View 对象,或者说是那些已经调用 removeView 方法
// 但是删除操作还未完成的 View 对象
private final ArraySet<View> mDyingViews = new ArraySet<View>();

WindowManagerGlobaladdView 方法的执行流程:

  1. 检查参数合法性,如果是子 Window (存在父 Window 的就是子 Window)就调整一些布局参数;如果不存在父 Window 并且版本大于等于 21,会默认设置窗口的硬件加速属性。

  2. 检查 View 是否在 mViews 集合中,是否在 mDyingViews 集合中。如果已经在 mView 里,是不可以添加的,会抛出异常;如果已经在 mDyingViews 集合里,需要立马销毁掉对应的 View

    int index = findViewLocked(view, false);
    if (index >= 0) {
        if (mDyingViews.contains(view)) {
            // Don't wait for MSG_DIE to make it's way through root's queue.
            mRoots.get(index).doDie();
        } else {
            throw new IllegalStateException("View " + view
                    + " has already been added to the window manager.");
        }
        // The previous removeView() had not completed executing. Now it has.
    }
    
  3. 创建 ViewRootImpl 对象,并将 VIew 添加到列表中

    相关的代码如下:

    // 创建 ViewRootImpl 对象
    root = new ViewRootImpl(view.getContext(), display);
    view.setLayoutParams(wparams);
    // 添加到列表中
    mViews.add(view);
    mRoots.add(root);
    mParams.add(wparams);
    
  4. 通过 VIewRootImpl 来更新界面完成 Window 的添加过程

    root.setView(view, wparams, panelParentView);
    

    4.1 在 setView 方法内部,先通过 requestLayout() 来完成异步刷新请求:实际完成的 View 的布局,测量以及绘制过程

    public void requestLayout() {
        if (!mHandlingLayoutInLayoutRequest) {
        	// 检查是否是 UI 线程
            checkThread();
            mLayoutRequested = true;
            // 开启异步刷新请求(这里面用到了同步屏障消息和异步消息)
            scheduleTraversals();
        }
    }
    

    4.2 接着通过 WindowSession 最终来完成 Window 的添加过程。

    // 这个返回值,就是平常开发中遇到的 `BadTokenException`,`InvalidDisplayException` 的来源。
    res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
        getHostVisibility(), mDisplay.getDisplayId(),
        mAttachInfo.mContentInsets, mInputChannel);
    

    mWindowSession 的类型是 IWindowSession,它是一个 Binder 对象,真正的实现类是 Session,这说明 Window 的添加过程需要一次 IPC 调用。

    注意,mWindowViewRootImpl 的构造方法里完成初始化:mWindow = new W(this);,它是 ViewRootImpl 对象的包装。

    4.3 在 Session 内部通过 WindowManagerService 来实现 Window 的添加。

    public int addToDisplay(IWindow window, int seq, WindowManager.LayoutParams attrs,
            int viewVisibility, int displayId, Rect outContentInsets, Rect outStableInsets,
            InputChannel outInputChannel) {
        return mService.addWindow(this, window, seq, attrs, viewVisibility, displayId,
                outContentInsets, outStableInsets, outInputChannel);
    }
    

2.3 Window 的删除过程

mWindowManager.removeView(mFloatingButton);
  1. 调用 WindowManagerremoveView 方法,是调用它的实现类 WindowManagerImplremoveView 方法;再通过桥接调用 WindowManagerGolbal 对象的 removeView(View view, boolean immediate) 方法(第二个参数取值为 false)如下:

    public void removeView(View view, boolean immediate) {
        if (view == null) {
            throw new IllegalArgumentException("view must not be null");
        }
        synchronized (mLock) {
        	// 先在 mViews 集合里查找索引,找不到会抛出异常
            int index = findViewLocked(view, true);
            // 从 mRoots 里根据索引找到 ViewRootImpl 对象,获取对应的 `View` 对象
            View curView = mRoots.get(index).getView();
            // 最后移除 View 对象
            removeViewLocked(index, immediate);
            if (curView == view) {
                return;
            }
            throw new IllegalStateException("Calling with view " + view
                    + " but the ViewAncestor is attached to " + curView);
        }
    }
    
  2. removeViewLocked 方法:

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

    这里就是调用了 ViewRootImpldie 方法。

    这里传给 die 的实参是 false,表示异步删除,由 WindowManagerImplremoveView 方法调用而来;传 true 时,表示同步删除,由WindowManagerImplremoveViewImmediate 方法调用而来。一般不要使用同步删除方式来删除 Window 以免发生意外的错误。

  3. ViewRootImpldie 方法:

    boolean die(boolean immediate) {
    // Make sure we do execute immediately if we are in the middle of a traversal o
    // done by dispatchDetachedFromWindow will cause havoc on return.
    if (immediate && !mIsInTraversal) {
        doDie();
        return false;
    }
    if (!mIsDrawing) {
        destroyHardwareRenderer();
    } else {
        Log.e(TAG, "Attempting to destroy the window while drawing!\n" +
                "  window=" + this + ", title=" + mWindowAttributes.getTitle());
    }
    mHandler.sendEmptyMessage(MSG_DIE);
    return true;
    

    如果 immediate 取值为 true,即同步删除,那么会直接调用 doDie() 方法,die 方法返回 false,表示已经完成删除;

    如果 immediate 取值为 false,即异步删除,那么发送一个请求删除的消息(MSG_DIE),die 方法返回 true,表示请求在排队中(消息被处理后,依然调用的是 doDie() 方法),再回到 removeViewLocked 方法中,会把 view 添加到 mDyingViews 集合(表示存那些正在被删除的 View 对象集合)。

  4. ViewRootImpldoDie() 方法:

    void doDie() {
        checkThread();
        if (LOCAL_LOGV) Log.v(TAG, "DIE in " + this + " of " + mSurface);
        synchronized (this) {
            if (mRemoved) {
                return;
            }
            mRemoved = true;
            if (mAdded) {
                dispatchDetachedFromWindow();
            }
            mAdded = false;
        }
        // 刷新几个列表的数据
        WindowManagerGlobal.getInstance().doRemoveView(this);
    }
    
  5. ViewRootImpldispatchDetachedFromWindow() 方法:

    void dispatchDetachedFromWindow() {
        if (mView != null && mView.mAttachInfo != null) {
            mAttachInfo.mTreeObserver.dispatchOnWindowAttachedChange(false);
            // 回调 View 的 onDetachedFromWindow 方法
            mView.dispatchDetachedFromWindow();
        }
        mView.assignParent(null);
        mView = null;
        try {
        	// Session 的 remove 方法移除 Window,最终会调用 WMS 的 removeWindow 方法
            mWindowSession.remove(mWindow);
        } catch (RemoteException e) {
        }
        // Dispose the input channel after removing the window so the Window Manager
        // doesn't interpret the input channel being closed as an abnormal termination.
        if (mInputChannel != null) {
            mInputChannel.dispose();
            mInputChannel = null;
        }
        mDisplayManager.unregisterDisplayListener(mDisplayListener);
        unscheduleTraversals();
    }
    
  6. WindowManagerGlobaldoRemoveView 方法刷新数据:

    void doRemoveView(ViewRootImpl root) {
        synchronized (mLock) {
            final int index = mRoots.indexOf(root);
            if (index >= 0) {
                mRoots.remove(index);
                mParams.remove(index);
                final View view = mViews.remove(index);
                mDyingViews.remove(view);
            }
        }
        if (HardwareRenderer.sTrimForeground && HardwareRenderer.isAvailable()) {
            doTrimForeground();
        }
    }
    

2.4 Window 的更新过程

直接看 WindowManagerGlobalupdateViewLayout 方法:

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

首先是对参数的一些检查,接着把布局参数设置给 viewint index = findViewLocked(view, true); 检查 view 是否还在 mViews 集合里,不在的话会抛出异常;最后调用 ViewRootImpl 对象的 setLayoutParams 方法更新 Window

查看 ViewRootImplsetLayoutParams 方法:

void setLayoutParams(WindowManager.LayoutParams attrs, boolean newView) {
    synchronized (this) {
        final int oldInsetLeft = mWindowAttributes.surfaceInsets.left;
        final int oldInsetTop = mWindowAttributes.surfaceInsets.top;
        final int oldInsetRight = mWindowAttributes.surfaceInsets.right;
        final int oldInsetBottom = mWindowAttributes.surfaceInsets.bottom;
        final int oldSoftInputMode = mWindowAttributes.softInputMode;
        // Keep track of the actual window flags supplied by the client.
        mClientWindowLayoutFlags = attrs.flags;
        // Preserve compatible window flag if exists.
        final int compatibleWindowFlag = mWindowAttributes.privateFlags
                & WindowManager.LayoutParams.PRIVATE_FLAG_COMPATIBLE_WINDOW;
        // Transfer over system UI visibility values as they carry current state.
        attrs.systemUiVisibility = mWindowAttributes.systemUiVisibility;
        attrs.subtreeSystemUiVisibility = mWindowAttributes.subtreeSystemUiVisibility;
        mWindowAttributesChangesFlag = mWindowAttributes.copyFrom(attrs);
        if ((mWindowAttributesChangesFlag
                & WindowManager.LayoutParams.TRANSLUCENT_FLAGS_CHANGED) != 0) {
            // Recompute system ui visibility.
            mAttachInfo.mRecomputeGlobalAttributes = true;
        }
        if (mWindowAttributes.packageName == null) {
            mWindowAttributes.packageName = mBasePackageName;
        }
        mWindowAttributes.privateFlags |= compatibleWindowFlag;
        // Restore old surface insets.
        mWindowAttributes.surfaceInsets.set(
                oldInsetLeft, oldInsetTop, oldInsetRight, oldInsetBottom);
        applyKeepScreenOnFlag(mWindowAttributes);
        if (newView) {
            mSoftInputMode = attrs.softInputMode;
            requestLayout();
        }
        // Don't lose the mode we last auto-computed.
        if ((attrs.softInputMode & WindowManager.LayoutParams.SOFT_INPUT_MASK_ADJUST)
                == WindowManager.LayoutParams.SOFT_INPUT_ADJUST_UNSPECIFIED) {
            mWindowAttributes.softInputMode = (mWindowAttributes.softInputMode
                    & ~WindowManager.LayoutParams.SOFT_INPUT_MASK_ADJUST)
                    | (oldSoftInputMode & WindowManager.LayoutParams.SOFT_INPUT_MASK_ADJUST);
        }
        // 标记 mWindowAttributesChanged 为 true
        mWindowAttributesChanged = true;
        scheduleTraversals();
    }
}

ViewRootImpl 里会通过 scheduleTraversals 方法来对 View 重新布局,包括测量,布局,重绘三个过程。除了 View 本身的重绘外,ViewRootImpl 还会通过 WindowSession 来更新 Window 的视图,这个过程最终是 WMS 的 relayoutWindow() 来实现的,它同样是一个 IPC 过程。

3 Window 的创建过程

3.1 Activity 的 Window 创建过程

  1. ActivityThreadhandleLaunchActivity 方法里先后调用 performLaunchActivity 方法和 handleResumeActivity 方法

    private void handleLaunchActivity(ActivityClientRecord r, Intent customIntent) {
       // performLaunchActivity 方法完成创建 Activity 的实例对象,
       // 并调用其 attach 方法关联其运行过程中所依赖的一系列上下文环境变量,
       // 创建 Window,回调 onCreate 方法(在 onCreate 方法中通过 setContentView 
       // 方法完成 DecorView 的创建和初始化)。      
       Activity a = performLaunchActivity(r, customIntent);
       if (a != null) {
           r.createdConfig = new Configuration(mConfiguration);
           Bundle oldState = r.state;
           // handleResumeActivity 方法会回调 onResume 方法完成顶层 View 的添加和显示
           handleResumeActivity(r.token, false, r.isForward,
                   !r.activity.mFinished && !r.startsNotResumed);
    }
    
  2. ActivityThreadperformLaunchActivity 方法完成创建 Activity 的实例对象,并调用其 attach 方法关联其运行过程中所依赖的一系列上下文环境变量,回调 onCreate 方法(在 onCreate 方法中通过 setContentView 方法完成 DecorView 的创建和初始化)

    private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
    	Activity activity = null;
    	    java.lang.ClassLoader cl = r.packageInfo.getClassLoader();
    	    // 通过 Instrumentation 对象创建 Activity 的实例
    	    activity = mInstrumentation.newActivity(
    	            cl, component.getClassName(), r.intent);
    	    StrictMode.incrementExpectedActivityCount(activity.getClass());
    	    r.intent.setExtrasClassLoader(cl);
    	    r.intent.prepareToEnterProcess();
    	    if (r.state != null) {
    	        r.state.setClassLoader(cl);
    	    }
    	// 调用 Activity 对象的 attach 方法关联其运行过程中所依赖的一系列上下文环境变量
    	activity.attach(appContext, this, getInstrumentation(), r.token,
    	        r.ident, app, r.intent, r.activityInfo, title, r.parent,
    	        r.embeddedID, r.lastNonConfigurationInstances, config,
    	        r.voiceInteractor);
        // 回调 Activity 的 onCreate 方法
    	mInstrumentation.callActivityOnCreate(activity, r.state);
    }
    
  3. Activityattach 方法里创建 Window 对象并设置回调接口

    // 创建 PhoneWindow 对象
    mWindow = new PhoneWindow(this);
    // 设置回调接口,目的是 Window 接收到外界的状态改变时就会回调到 Activity 实现的方法。
    mWindow.setCallback(this);
    mWindow.setOnWindowDismissedCallback(this);
    // 创建并设置 Window 的 WindowManager 对象,实际上是一个 WindowManagerImpl 对象。
    mWindow.setWindowManager(
                    (WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
                    mToken, mComponent.flattenToString(),
                    (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
    mWindowManager = mWindow.getWindowManager();
    

    目前我们创建了 PhoneWindow 对象,类包含图如下:
    在这里插入图片描述

  4. ActivityonCreate 方法中调用 setContentView 方法设置页面布局:

    ActivitysetContentView 方法如下:

    public void setContentView(int layoutResID) {
        // getWindow() 返回的是 mWindow 对象,即是在 attach 方法里创建的 PhoneWindow 对象。
        getWindow().setContentView(layoutResID);
        initWindowDecorActionBar();
    }
    

    PhoneWindowsetContentView 方法:

    public void setContentView(int layoutResID) {
        // 如果 mContentParent 为 null,就调用 installDecor() 方法
        if (mContentParent == null) {
            installDecor();
        } 
        // 把布局填充到 mContentParent 里面
        mLayoutInflater.inflate(layoutResID, mContentParent);
       // 调用回调方法,通知 Activity 视图已经发生改变了。
        final Callback cb = getCallback();
        if (cb != null && !isDestroyed()) {
            cb.onContentChanged();
        }
    }
    

    这里留个小疑问:mContentParent 的含义是什么?

    这里面我们需要去看一下 installDecor() 方法:

    private void installDecor() {
        // 如果 DecorView 对象不存在,就调用 generateDecor() 方法给它赋值。
        if (mDecor == null) {
            mDecor = generateDecor();
        }
        // 如果 mContentParent 不存在,就调用 generateLayout 方法给它赋值。
        if (mContentParent == null) {
            mContentParent = generateLayout(mDecor);
        }
    }
    

    generateDecor() 方法中创建 DecorView

    protected DecorView generateDecor() {
        return new DecorView(getContext(), -1);
    }
    

    这里创建了 DecorView 对象,DecorView 是什么呢?它是 PhoneWindow 的内部类,它是 FrameLayout 的子类。

    public class PhoneWindow extends Window {
    	private final class DecorView extends FrameLayout {
    	}
    }
    

    现在的类包含图如下:
    在这里插入图片描述
    接着看 generateLayout(DecorView decor) 方法,这个方法有些长,但是里面的注释可以说明代码的意图:

    public static final int ID_ANDROID_CONTENT = com.android.internal.R.id.content;
    protected ViewGroup generateLayout(DecorView decor) {
        // Apply data from current theme. 开始的大段代码的作用是应用来自当前主题的数据
        // Inflate the window decor. 需要关注的是填充 window decor
        // 首先查找要把哪个布局填充给 window decor
        // 需要说明的是,布局资源的共同点是要有一个 id 为 @android:id/content 的 FrameLayout 节点。
        int layoutResource;
        // Embedded, so no decoration is needed.
        layoutResource = R.layout.screen_simple;
        // 使用布局填充器把 xml 的布局资源转为 View,
        // 返回的是 xml 布局的根节点 View 对象,并且对象上拥有根节点上的布局参数。
        View in = mLayoutInflater.inflate(layoutResource, null);
        // 把布局资源的根 View 添加到 DecorView 里面
        decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
        // 把 xml 布局的根节点 View 对象赋值给 mCotentRoot.
        mContentRoot = (ViewGroup) in;
        // 获取 id 为 @android:id/content 的节点对象,赋值给 contentParent.
        ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
        // 一定要有 contentParent,否则抛出异常:Window 不能找到 content 的容器 View 对象。
        if (contentParent == null) {
            throw new RuntimeException("Window couldn't find content container view");
        }
        return contentParent;
    }
    

    generateLayout() 方法结束后,会返回 contentParent 对象,在 installDecor() 方法中把返回值赋值给 mContentParent 成员变量。

    到这里,我们就知道了 mContentParent 就是 id 为 @android:id/content 的节点对象。

    类包含图如下:
    在这里插入图片描述
    回到PhoneWindowsetContentView 方法:

    public void setContentView(int layoutResID) {
        // 如果 mContentParent 为 null,就调用 installDecor() 方法
        if (mContentParent == null) {
            installDecor();
        } 
        // 把布局填充到 mContentParent 里面
        mLayoutInflater.inflate(layoutResID, mContentParent);
       // 调用回调方法,通知 Activity 视图已经发生改变了。
        final Callback cb = getCallback();
        if (cb != null && !isDestroyed()) {
            cb.onContentChanged();
        }
    }
    

    mLayoutInflater.inflate(layoutResID, mContentParent); 就是把通过 setContentView 方法传递过来的布局资源(我们这里称为 Activity 的视图),填充到 mContentParent 里面。
    在这里插入图片描述

  5. ActivityThread 类的 handleResumeActivity 方法中添加和显示 DecorView

    final void handleResumeActivity(IBinder token,
        boolean clearHide, boolean isForward, boolean reallyResume) {
    	// 调用 performResumeActivity 获取了 ActivityClientRecord 对象,
    	// 并回调了 Activity 的 onResume 方法			
        ActivityClientRecord r = performResumeActivity(token, clearHide);
        if (r != null) {
            final Activity a = r.activity;
            // 添加 DecorView,但是还不会显示
            if (r.window == null && !a.mFinished && willBeVisible) {
                r.window = r.activity.getWindow();
                View decor = r.window.getDecorView();
                // 设置 DecorView 对象不可见
                decor.setVisibility(View.INVISIBLE);
                ViewManager wm = a.getWindowManager();
                WindowManager.LayoutParams l = r.window.getAttributes();
                a.mDecor = decor;
                l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
                l.softInputMode |= forwardBit;
                if (a.mVisibleFromClient) {
                    a.mWindowAdded = true;
                    // 把 DecorView 对象添加到 WindowManager。
                    wm.addView(decor, l);
                }
            } 
            if (!r.activity.mFinished && willBeVisible
                    && r.activity.mDecor != null && !r.hideForNow) {
    			// 显示 DecorView	
                if (r.activity.mVisibleFromClient) {
                    r.activity.makeVisible();
                }
            }
        } 
    }
    

    Activity 类的 makeVisble() 方法:

    void makeVisible() {
        if (!mWindowAdded) {
            ViewManager wm = getWindowManager();
            wm.addView(mDecor, getWindow().getAttributes());
            mWindowAdded = true;
        }
        // 设置 DecorView 对象可见
        mDecor.setVisibility(View.VISIBLE);
    }
    

3.2 Dialog 的 Window 创建过程

  1. Dialog 的构造方法中创建 Window 对象并设置回调接口
    Dialog(@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();
            }
        });
        // 创建 WindowManagerImpl 对象
        w.setWindowManager(mWindowManager, null, null);
        w.setGravity(Gravity.CENTER);
    }
    
  2. 初始化 DecorView 对象并将 Dialog 的视图添加到 DecorView
    public void setContentView(@LayoutRes int layoutResID) {
        mWindow.setContentView(layoutResID);
    }	
    
    这部分和 Activity 的对应部分是一致的。
  3. Dialogshow 方法中将 DecorView 对象添加到 Window 中显示
    public void show() {
        // 从 PhoneWindow 对象中获取 DecorView 对象
        mDecor = mWindow.getDecorView();
        WindowManager.LayoutParams l = mWindow.getAttributes();
        // 把 DecorView 添加到 Window 中显示
        mWindowManager.addView(mDecor, l);
    }
    

3.3 Dialog 对应的 Context 必须是 Activity 的 Context 吗?

对于普通的 Dialog 而言,确实必须传递 ActivityContext,否则会报错:

Dialog dialog = new Dialog(getApplicationContext());
dialog.setTitle("Title");
dialog.setContentView(R.layout.dialog_content);
dialog.show();

报错信息如下:

Caused by: android.view.WindowManager$BadTokenException: Unable to add window -- token null is not valid; is your activity running?
   at android.view.ViewRootImpl.setView(ViewRootImpl.java:1141)
   at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:409)
   at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:111)
   at android.app.Dialog.show(Dialog.java:342)
   at com.wzc.chapter_8.MainActivity.dialog(MainActivity.java:44)
   at java.lang.reflect.Method.invoke(Native Method) 
   at android.view.View$DeclaredOnClickListener.onClick(View.java:6324) 
   at android.view.View.performClick(View.java:7509) 
   at android.view.View.performClickInternal(View.java:7486) 
   at android.view.View.access$3600(View.java:841) 
   at android.view.View$PerformClick.run(View.java:28720) 
   at android.os.Handler.handleCallback(Handler.java:938) 
   at android.os.Handler.dispatchMessage(Handler.java:99) 
   at android.os.Looper.loop(Looper.java:236) 
   at android.app.ActivityThread.main(ActivityThread.java:8059) 
   at java.lang.reflect.Method.invoke(Native Method) 
   at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:656) 
   at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:967) 

查看日志是因为没有应用 token 导致的。这时只要传入 ActivityContext 来显示对话框就可以了,应用 token 一般只有 Activity 才有。

Dialog dialog = new Dialog(MainActivity.this);

如果非要传入非 Activity 的 Context 来显示对话框,就需要使用系统 Window,系统 Window 可以不需要 token。需要做的是:

  • 在 AndroidManifest 文件中声明权限使应用可以使用系统 Window:
      <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
    
  • 指定 Dialog 的 Window 类型为系统类型(FIRST_SYSTEM_WINDOW(2000)~ LAST_SYSTEM_WINDOW(2999))
    dialog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ERROR);
    
  • 如果是小米手机,对话框不显示时,需要手动打开显示悬浮窗的开关。

3.4 Toast 的 Window 创建过程

  1. 创建 Toast 对象,并对 Toast 对象的 mNextViewmDuration 进行赋值:

    public Toast(Context context) {
        mContext = context;
        mTN = new TN();
        mTN.mY = context.getResources().getDimensionPixelSize(
                com.android.internal.R.dimen.toast_y_offset);
        mTN.mGravity = context.getResources().getInteger(
                com.android.internal.R.integer.config_toastDefaultGravity);
    }
    
    result.mNextView = v;
    result.mDuration = duration;
    

    可以看到通过构造方法传入了一个 Context对象,并且把它赋值给 mContext 成员变量。在构造方法里面,还初始化了一个 TN 类型的成员变量。

  2. 创建 TN对象

    private static class TN extends ITransientNotification.Stub {
        final Runnable mShow = new Runnable() {
            @Override
            public void run() {
                handleShow();
            }
        };
        final Runnable mHide = new Runnable() {
            @Override
            public void run() {
                handleHide();
                mNextView = null;
            }
        };
        private final WindowManager.LayoutParams mParams = new WindowManager.LayoutParams();
        final Handler mHandler = new Handler();    
        View mView;
        View mNextView;
        WindowManager mWM;
        TN() {
            final WindowManager.LayoutParams params = mParams;
            params.height = WindowManager.LayoutParams.WRAP_CONTENT;
            params.width = WindowManager.LayoutParams.WRAP_CONTENT;
            params.format = PixelFormat.TRANSLUCENT;
            params.windowAnimations = com.android.internal.R.style.Animation_Toast;
            params.type = WindowManager.LayoutParams.TYPE_TOAST;
            params.setTitle("Toast");
            params.flags = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
                    | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
                    | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
        }
        
        @Override
        public void show() {
            mHandler.post(mShow);
        }
        
        @Override
        public void hide() {
            mHandler.post(mHide);
        }
        public void handleShow() {
            if (mView != mNextView) {
                handleHide();
                mView = mNextView;
                Context context = mView.getContext().getApplicationContext();
                if (context == null) {
                    context = mView.getContext();
                }
                mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
                if (mView.getParent() != null) {
                    mWM.removeView(mView);
                }
                mWM.addView(mView, mParams);
            }
        }   
        public void handleHide() {
            if (mView != null) {
                if (mView.getParent() != null) {
                    mWM.removeView(mView);
                }
                mView = null;
            }
        }
    }
    

    可以看到,TN继承于 ITransientNotification.Stub,这说明 TN 是一个 Binder 类,它实现了 show()hide() 方法。

    TN 的构造方法里,主要是完成了对 mParams 的属性赋值操作,比如对窗口层级属性 type 赋值为 WindowManager.LayoutParams.TYPE_TOAST(2005),这属于系统 Window 了(FIRST_SYSTEM_WINDOW(2000)~ LAST_SYSTEM_WINDOW(2999))。

    show()hide() 方法内部通过 Handler 对象 mHandler 把要执行的任务切换到当前线程执行。那么,mHandler是在哪里创建的?mHandler 是作为 TN 对象的成员变量被显式初始化的。

    接着看具体任务执行的内容,即 handleShow()handleHide() 方法。可以看到,在 handleShow() 方法内部完成了Window的添加:mWM.addView(mView, mParams);,在 handleHide()方法内部完成了Window的删除:mWM.removeView(mView);

    但是,我们目前还不知道 show()hide() 方法在哪里调用的。

  3. 调用 Toastshow() 方法来显示 Toast

    public void show() {
        INotificationManager service = getService();
        String pkg = mContext.getOpPackageName();
        TN tn = mTN;
        tn.mNextView = mNextView;
        try {
            service.enqueueToast(pkg, tn, mDuration);
        } catch (RemoteException e) {
        }
    }
    

    getService() 方法如下:

    private static INotificationManager sService;
    static private INotificationManager getService() {
        if (sService != null) {
            return sService;
        }
        sService = INotificationManager.Stub.asInterface(ServiceManager.getService("notification"));
        return sService;
    }
    

    这里 getService() 方法获取的是客户端需要的 INotificationManager 类型的对象,因为 NotificationManagerService 作为服务端运行在系统的进程中,所以这里获取到的是 INotificationManager.Stub.Proxy 对象。这一点可以通过反射获取 sService 静态变量的值看出来。
    在这里插入图片描述
    那么,通过调用 service.enqueueToast(pkg, tn, mDuration) 就是发起一次 IPC 通信过程了,传递的第一个参数是当前应用的包名,第二个参数是 TN 对象,它其实是作为远程回调传入的,第三个参数是 Toast 的时长类型。最终会调用到运行在 Binder 线程池里的 enqueueToast(String pkg, ITransientNotification callback, int duration) 方法。可以看到第二个形参的名字是 callback ,这也说明了 TN 的作用是回调接口对象。

  4. 调用 NotificationManagerService 里实现的 enqueueToast 方法

    需要说明的是, enqueueToast 并不是 NotificationManagerService 的一个成员方法,而是它内部的一个匿名内部类里的方法。

    public class NotificationManagerService extends SystemService {
    	private final IBinder mService = new INotificationManager.Stub() {
    		public void enqueueToast(String pkg, ITransientNotification callback, int duration) {
    			// 把参数信息封装在 ToastRecord 对象里面
    			record = new ToastRecord(callingPid, pkg, callback, duration);
    			// 显示 Toast
    			showNextToastLocked();
    		}
    	}
    	// showNextToastLocked 方法是 NotificationManagerService 的成员方法
    	void showNextToastLocked() {
    	    ToastRecord record = mToastQueue.get(0);
    	    while (record != null) {
    	        try {
    	        	// 调用 TN 对象这个远程 Binder 的 show 方法 
    	            record.callback.show();
    	            // 定时取消 Toast
    	            scheduleTimeoutLocked(record);
    	            return;
    	        } catch (RemoteException e) {
    	            Slog.w(TAG, "Object died trying to show notification " + record.callback
    	                    + " in package " + record.pkg);
    	            // remove it from the list and let the process die
    	            int index = mToastQueue.indexOf(record);
    	            if (index >= 0) {
    	                mToastQueue.remove(index);
    	            }
    	            keepProcessAliveLocked(record.pid);
    	            if (mToastQueue.size() > 0) {
    	                record = mToastQueue.get(0);
    	            } else {
    	                record = null;
    	            }
    	        }
    	    }
    	}
    }
    

    通过调用 record.callback.show(); 方法,会通过跨进程调用 Toast 对象的 mTNshow() 方法,将 Toast 显示出来。

  5. 定时取消 Toast

    查看 NMS 的 scheduleTimeoutLocked 方法

    static final int LONG_DELAY = 3500; // 3.5 seconds
    static final int SHORT_DELAY = 2000; // 2 seconds
    
    private void scheduleTimeoutLocked(ToastRecord r)
    {
        mHandler.removeCallbacksAndMessages(r);
        Message m = Message.obtain(mHandler, MESSAGE_TIMEOUT, r);
        long delay = r.duration == Toast.LENGTH_LONG ? LONG_DELAY : SHORT_DELAY;
        mHandler.sendMessageDelayed(m, delay);
    }
    

    通过 Handler 发送延时消息,延时消息执行时,会调用

    void cancelToastLocked(int index) {
        ToastRecord record = mToastQueue.get(index);
        // 调用 TN 对象这个远程 Binder 的 hide 方法
        record.callback.hide();
        mToastQueue.remove(index);
        keepProcessAliveLocked(record.pid);
    }
    

    通过调用 record.callback.hide(); 方法,会通过跨进程调用 Toast 对象的 mTNhide() 方法,将 Toast 隐藏掉。

参考

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

willwaywang6

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值