Android知识进阶树——Window & ViewRootImpl

·  阅读 1386

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

                    if (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, true);
        ViewRootImpl root = mRoots.get(index);  //更新root,mParams集合中的数据
        mParams.remove(index);
        mParams.add(index, wparams);//替换mParams中保存的wparams
        root.setLayoutParams(wparams, false); // 更新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
标签:
分类:
Android
标签: