1、初识 Window

val layoutParams = WindowManager.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
layoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE shl WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL shl  WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
windowManager.addView(imageView,layoutParams)
复制代码
  • Window 类型
  1. 系统级 WIndow:系统级别的 Window 需要声明权限才能创建,如 Toast
  2. 应用级 Window:系统的活动窗口,如:Activity
  3. 子 Window:附属在父 Window 中,如:Dialog
  • FLAG
  1. FLAG_NOT_FOCUSABLE: 表示不获取焦点,事件最终会传递到底层的 Window
  2. FLAG_NOT_TOUCH_MODEL:只处理自己区域内的点击事件,区域之外的传递给底层 Window
  3. FLAG_SHOW_WHEN_LOCKED:开启此模式可以显示在锁屏上

2、Window 工作过程

Window 是一个抽象概念,它是以 View 的形式存在,每个 Window 都对应着 View 和 ViewRootImpl,Window 和 View 之间通过 ViewRootImpl 建立联系

2.1、Window 的添加过程

Window 的整个添加过程可分为两部分执行:

  • WindowManager
  • WindowManagerService

View 的添加是从调用 windowManager.addView()开始的,其实点开 windowManager 只是一个继承 ViewManager 的接口,在活动中真正执行任务的是它的实现类 WindowMangerImpl,因此方法会执行到 WindowMangerImpl.addView(),但 WindowMangerImpl 是个聪明的类,在 addView() 中除了验证设置 LayoutParams 的合法性之外,它又将所有的工作都桥接给 WindowManagerGlobal 执行:

@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
    applyDefaultToken(params);//验证params的合法性
    mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow); // 直接交给WindowManagerGlobal处理
}
复制代码
  • WindowManagerGlobal

在具体执行方法前先介绍下 WindowManagerGlobal 中的各个集合的作用(见下面注释),在 Window 工作的整个过程他们时刻保存着 Window 和 View 的运行状态

private final ArrayList<View> mViews = new ArrayList<View>();  // 保存所有Window对应的View
private final ArrayList<ViewRootImpl> mRoots = new ArrayList<ViewRootImpl>();  // 保存所有Window对应的ViewRootImpl
private final ArrayList<WindowManager.LayoutParams> mParams =  // 保存所有Window对应的WindowManager.LayoutParams
        new ArrayList<WindowManager.LayoutParams>();
private final ArraySet<View> mDyingViews = new ArraySet<View>(); // 保存正在被删除的View
复制代码
  • WindowManagerGlobal.addView()
root = new ViewRootImpl(view.getContext(), display);
view.setLayoutParams(wparams);
mViews.add(view); 
mRoots.add(root); 
mParams.add(wparams);
root.setView(view, wparams, panelParentView);  // View的绘制
复制代码

上面是 addView()中的部分代码,它执行了以下几个操作:

  1. 创建 ViewRootImpl 的实例
  2. 设置 View 的布局参数
  3. 分别在集合中保存 view、root 和 params

在保存了相关数据后,View 真正的执行就是 setView()这一句开始,下面看看 ViewRootImpl 中是如何实现 View 的测量绘制的

  • ViewRootImpl.setView()

ViewRootImpl 是 View 中的最高层级,属于所有 View 的根(但 ViewRootImpl 不是 View,只是实现了 ViewParent 接口),实现了 View 和 WindowManager 之间的通信协议

public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
        synchronized (this) {
        ......
          requestLayout();//对View进行第一次测量和绘制

res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(), mWinFrame,
mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
mAttachInfo.mOutsets, mAttachInfo.mDisplayCutout, mInputChannel); // 调用 WindowSession 的 addTodiaplay()添加窗口
}
}

复制代码

requestLayout()内调用 scheduleTraversals(),scheduleTraversals() 中 会获取主线程的 Handler 然后发送消息执行 TraversalRunnable 实例,TraversalRunnable 是 Runnable 的实现类,在 run()方法中执行 oTraversal() ,然后方法会执行到 performTraversals()

  • performTraversals()
//调用performMeasure完成Window内视图的测量
       performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
                    int width = host.getMeasuredWidth();
                    int height = host.getMeasuredHeight();
                    boolean measureAgain = false;
                    if (lp.horizontalWeight > 0.0f) {
                        width += (int) ((mWidth - width) * lp.horizontalWeight);
                        childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(width,
                                MeasureSpec.EXACTLY);
                        measureAgain = true;
                    }
                    if (lp.verticalWeight > 0.0f) {
                        height += (int) ((mHeight - height) * lp.verticalWeight);
                        childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(height,
                                MeasureSpec.EXACTLY);
                        measureAgain = true;
                    }
                <span class="hljs-keyword">if</span> (measureAgain) {
                    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
                }

……
final boolean didLayout = layoutRequested && (!mStopped || mReportNextDraw);
boolean triggerGlobalLayoutListener = didLayout
|| mAttachInfo.mRecomputeGlobalAttributes;
if (didLayout) {
performLayout(lp, mWidth, mHeight); // 完成 View 的布局 Layout
}
……
performDraw();// 对 View 的绘制

复制代码

performTraversals 方法中,依次调用了 performMeasure、performLayout、performDraw 三个方法,这三个方法中又分别调用 View 或 ViewGroupde 的 measure、layout 和 draw 方法,完成了 View 的测量、布局和绘制;

  • WindowSession 使用 Binder 机制调用 IWindowSession 接口,内部调用 WindowManagerService.addWindow()添加,到此所有的操作就执行到了 WindowManagerService 中, 关于 WindowManagerService 的工作过程请参考Android 窗口管理分析
@Override
 public int addToDisplay(IWindow window, int seq, WindowManager.LayoutParams attrs,
         int viewVisibility, int displayId, Rect outContentInsets, Rect outStableInsets,
         Rect outOutsets, InputChannel outInputChannel) {
     return mService.addWindow(this, window, seq, attrs, viewVisibility, displayId,
             outContentInsets, outStableInsets, outOutsets, outInputChannel);
 }
复制代码

2.2、Window 删除过程

删除过程和添加一样最后执行任务的都是 WindowManagerGlobal,先看下 WindowManagerGlobal 的 removeView() 方法:

int index = findViewLocked(view, true); 
View curView = mRoots.get(index).getView(); 
removeViewLocked(index, immediate);
复制代码

removeView() 中主要执行三个步骤:

  1. 获取当前操作 View 的 index
  2. 获取 mRoots 中保存的 ViewRootImpl 实例
  3. 调用 removeViewLocked 执行删除
  • removeViewLocked():获取要删除的 View 执行删除操作
ViewRootImpl root = mRoots.get(index);
View view = root.getView();   // 获取ViewRootImpl保存的View

boolean deferred = root.die(immediate); // 调用 die()执行删除 View
mDyingViews.add(view); // 将要删除的 View 添加到 mDyingViews

复制代码
  • die():发送删除消息
boolean die(boolean immediate) {
if (immediate && !mIsInTraversal) {
    doDie(); 
    return false;
}
    mHandler.sendEmptyMessage(MSG_DIE); 
    return true;
}
复制代码

在 die()方法中根据传入的 immediate 执行同步或异步删除:

  1. 同步删除:直接调用 doDie() 方法执行删除
  2. 异步删除:发送 Handler 消息调用 doDie()
  • doDie():真正执行 View 的删除
mView.dispatchDetachedFromWindow();  
mWindowSession.remove(mWindow); 
mView.onDetachedFromWindow();
WindowManagerGlobal.getInstance().doRemoveView(this);  
复制代码

doDie 是真正发起删除的地方,执行操作如下:

  1. 调用用 mWindowSession 最终调用 WindowMangerService.removeWindow()
  2. 调用 View 的 onDetachedFromWindow()执行 View 的移除操作
  3. 移除 mRoots、mParams、mDyingView 中保存的 View 信息

2.3、Window 更新过程

  • WindowManagerGlobal.updateViewLayout()
public void updateViewLayout(View view, ViewGroup.LayoutParams params) {
final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams)params;
view.setLayoutParams(wparams);  // 设置新的LyaoutParams

synchronized (mLock) {
    int index = findViewLocked(view, <span class="hljs-literal">true</span>);
    ViewRootImpl root = mRoots.get(index);  //更新root,mParams集合中的数据
    mParams.remove(index);
    mParams.add(index, wparams);//替换mParams中保存的wparams
    root.setLayoutParams(wparams, <span class="hljs-literal">false</span>); // 更新View
}

}

复制代码

执行过程见上面注释,在 root.setLayoutParams 中会触发 ViewRootImpl 的 scheduleTraversals 实现 View 的测量、布局、绘制;

3、实例

3.1、Activity 中 Window 的添加

在这里插入图片描述 借用网络上的一幅图展示 Activity 的层次关系:

  1. PhoneWindow:Activity 的活动窗口
  2. DecorView:所有视图的根 View,其中包含标题栏和 content
  3. ContentView:布局容器设置的 layout 文件被加载到其中
  • window 的创建
  1. 在 Activity.attach()方法中使用 PolicyManager.makeNewWindow()创建 PhoneWIndow
 mWindow = new PhoneWindow(this, window, activityConfigCallback);
        mWindow.setWindowControllerCallback(this);
        mWindow.setCallback(this);
        mWindow.setOnWindowDismissedCallback(this);
        mWindow.getLayoutInflater().setPrivateFactory(this);
复制代码
  • window 设置视图
//Activity中
public void setContentView(@LayoutRes int layoutResID) {
    getWindow().setContentView(layoutResID);  //调用PhoneWindow的setContentView()
    initWindowDecorActionBar();
}
复制代码

此处的 getWindow()得到的就是前面创建的 PhoneWindow ,所以 setContentView()最终是在 PhoneWindow 中执行的

public void setContentView(int layoutResID) {
      if (mContentParent == null) {
            installDecor();
        } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            mContentParent.removeAllViews();
        }
}
复制代码

setContentView()方法中,首先判断 contentParent 是否空, 如果为空则执行 installDecor(),installDecor()中有两处代码比较明显,分别是初始化 DecorView 和 mContentParent,下面分别看看这两个方法

  • generateDecor():创建 DecorView 实例
protected DecorView generateDecor(int featureId) {
    return new DecorView(context, featureId, this, getAttributes()); //初始化DecorView,此时只是一个FrameLayout
}
复制代码
  • generateLayout():加载布局文件并初始化 mContentParent
mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
//根据加载后的布局查找content
ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);  // 加载DecorView布局中的content容器
public static final int ID_ANDROID_CONTENT = com.android.internal.R.id.content;  //content的id

//mDecor.onResourcesLoaded()
final View root = inflater.inflate(layoutResource, null); // 加载原始布局文件:包含标题栏和 content
addView(root,new ViewGroup.MarginLayoutParams(MATCH_PARENT, MATCH_PARENT)); // 将加载的布局添加到 DecorView 中

复制代码

在 generateLayout 中完成了布局 layout 文件的加载,具体细节如下:

  1. 加载 getWindowStyle 中的属性值
  2. 根据设置的 style 初始化 Layout 的 WindowManager.LayoutParams 和选择系统的布局资源 layoutResource
  3. 设置 DecorView 的背景、标题、颜色等状态
  4. 然后调用 mDecor.onResourcesLoaded() 加载 layoutResource 到 DecorView 中
  5. 根据资源 id 获取布局中的 contentParent 容器
  • 将 View 添加到 DecorView 的 contentParent 容器中
@Override
public void setContentView(int layoutResID) {
     mLayoutInflater.inflate(layoutResID, mContentParent); //加载布局到mContentParent中
}
复制代码
  • 加载完布局后回调 onContentChange(),通知 Activity 加载完毕
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
    cb.onContentChanged();
}
复制代码

到此 DecorView 和 contentParent 初始化已经完成,DecorView 中加载了一个具有 TitleView 和 ContentView 的布局,并且加载的 layoutResID 也已加载到 ContentView 中,所以关于 DecorView 内部的工作已经完成,但 DecorView 未被添加到 Window 中,所以此时界面仍是不可见

  • DecorView 添加到 Window()

ActivityThread 的 handleResumeActivity()中调用 Activity 的 makeVisible()方法,makeVisible 中调用 WindowManager.addView() 将 DecorView 添加到 PhoneWindow 中,到此布局资源展示在屏幕上

//handleResumeActivity
if (r.activity.mVisibleFromClient) {
    r.activity.makeVisible();
}

void makeVisible() {
if (!mWindowAdded) {
ViewManager wm = getWindowManager();
wm.addView(mDecor, getWindow().getAttributes()); // 将 DecorView 添加到 PhoneWindow 中
mWindowAdded = true;
}
mDecor.setVisibility(View.VISIBLE); // 设置 DecorView 可见
}

复制代码

3.2、Dialog 中 Window 的添加

  • Dialog 使用
val dialog = Dialog(this,R.style.Base_ThemeOverlay_AppCompat_Dialog)
dialog.setContentView(R.layout.dialog)
dialog.show()
dialog.cancel()
复制代码
  • 创建 Window

从上面使用可以看出,dialog 设置布局时和 Activity 都是使用 setContentView,所以其执行初始化的过程和 Activity 一致,只是在将 DecorView 添加到 Window 时有所不同

  • 将 DecorView 添加到 Window
public void show() { //在Dialog显示时添加到Window中
  mWindowManager.addView(mDecor, l); // 添加DecorView
}
复制代码
  • dialog 关闭时通过 WindowManager 移除 DectorView
mWindowManager.removeViewImmediate(mDecor);
复制代码

3.3、Toast 中 Window 的创建

  • 使用
Toast.makeText(this,"Toast",Toast.LENGTH_SHORT).show()
复制代码
  • makeText():makeText 执行了 Toast 的文件加载和和设置
Toast result = new Toast(context, looper); //创建Toast实例,并传入队列Loop
LayoutInflater inflate = (LayoutInflater)
        context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View v = inflate.inflate(com.android.internal.R.layout.transient_notification, null);  //加载Toast布局并设置View
TextView tv = (TextView)v.findViewById(com.android.internal.R.id.message);
tv.setText(text); //设置Toast的信息
result.mNextView = v; // 复制给mNextView
result.mDuration = duration;  //设置Toast的弹出时长
return result;
复制代码
  • Toast 的显示
INotificationManager service = getService();
String pkg = mContext.getOpPackageName();
TN tn = mTN;
tn.mNextView = mNextView;

service.enqueueToast(pkg, tn, mDuration);

复制代码

针对上面方法中做几点说明:

  1. service 是 INotificationManager 的代理类,此处是 IPC 通信;
  2. TN 是 ITransientNotification 的代理类
  3. mNextView 是本次 Toast 加载的 View
  4. service.enqueueToast() 将 Toast 加入消息队列

Toast 最终回调 TN 中的 show 方法,show()中发送 Message 到 Handle,然后调用 handleShow()

public void handleShow(IBinder windowToken) {
handleHide();
mParams.x = mX;
mParams.y = mY;
mParams.verticalMargin = mVerticalMargin;
mParams.horizontalMargin = mHorizontalMargin;
mParams.packageName = packageName;
mParams.hideTimeoutMilliseconds = mDuration ==
    Toast.LENGTH_LONG ? LONG_DURATION_TIMEOUT : SHORT_DURATION_TIMEOUT;
mParams.token = windowToken;

mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
mWM.addView(mView, mParams);
}

复制代码

handleShow()中执行以下操作:

  1. 调用 handleHide() 隐藏前一个 Toast
  2. 设置 Toast 的 mParams 参数,如:坐标、mDuration
  3. 调用 WindowManager 的 addView()添加 View

WindowManagerService 是如何执行 Window 的添加和操作的?

  • Android

    开放手机联盟(一个由 30 多家科技公司和手机公司组成的团体)已开发出 Android,Android 是第一个完整、开放、免费的手机平台。

    293 引用
感谢    赞同    分享    收藏    关注    反对    举报    ...