Android知識進階樹——Window & ViewRootImpl

一、初識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:開啓此模式能夠顯示在鎖屏上

二、Window工做過程

Window是一個抽象概念,它是以View的形式存在,每一個Window都對應着View和ViewRootImpl,Window和View之間經過ViewRootImpl創建聯繫android

2.一、Window的添加過程

Window的整個添加過程可分爲兩部分執行:bash

  • 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的運行狀態app

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的測量繪製的ide

  • ViewRootImpl.setView()

ViewRootImpl是View中的最高層級,屬於全部View的根(但ViewRootImpl不是View,只是實現了ViewParent接口),實現了View和WindowManager之間的通訊協議oop

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的測量、佈局和繪製;ui

  • 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.二、Window刪除過程

刪除過程和添加同樣最後執行任務的都是WindowManagerGlobal,先看下WindowManagerGlobal的removeView()方法:this

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.三、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.一、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.二、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.三、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的添加和操做的?

相關文章
相關標籤/搜索