Android知識進階樹——Window & ViewRootImpl

Alex@W發表於2019-01-28

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的新增和操作的?

相關文章