Android 理解Window和WindowManager

概述

Window表示窗口的概念,他是一個抽象類,他的真正實現類是PhoneWindowWindowManager用來對Window進行管理,是外接訪問Window的入口,Window操做的具體實現是在WindowManagerService中,WindowMagerWindowManagerService交互是IPC的過程java

Android中全部的視圖都是附加在Window上上呈現的,無論Activity,Dialog,Toast,他們的視圖都是附加在Window上的,所以Window其實是View的直接管理者android

下面咱們來詳細的瞭解Windowapp

Window和WindowMagaer

咱們先來了解一下如何使用WindwoMagaer來添加一個Windowide

Button button = new Button(this);
        button.setText("Window");
        WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams(
                WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.WRAP_CONTENT, 0, 0, PixelFormat.TRANSPARENT);

        layoutParams.type = WindowManager.LayoutParams.TYPE_APPLICATION;

        layoutParams.flags= WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
                | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;

        layoutParams.gravity= Gravity.LEFT|Gravity.TOP;
        layoutParams.x=100;
        layoutParams.y=300;

        WindowManager windowManager = getWindowManager();

        windowManager.addView(button,layoutParams);
複製代碼

這段代碼能夠添加一個Window,位置在(100,300)處,這裏面有倆個參數比較重要分別是,typeflag,下面分別介紹一下這倆個參數oop

TYPE 窗口的屬性

type參數表示Window的類型,Window有三種類型,分別是Application Window(應用窗口),Sub Window(子窗口)和System Window(系統窗口),每一個大類型又包含多個小類型,他們都定義在WindowMager的靜態內部類LayoutParams中,下面對這三種類型進行講解佈局

Application Window(應用窗口)post

Activity就是典型的應用窗口,應用窗口包含的類型以下:ui

public static final int FIRST_APPLICATION_WINDOW = 1;
        //窗口的基礎值,其餘窗口要大於這個值
        public static final int TYPE_BASE_APPLICATION   = 1;
        // 普通應用程序的窗口
        public static final int TYPE_APPLICATION        = 2;

        public static final int TYPE_APPLICATION_STARTING = 3;

        public static final int TYPE_DRAWN_APPLICATION = 4;

        public static final int LAST_APPLICATION_WINDOW = 99;
複製代碼

應用窗口就包括了以上幾中類型,其中最上方是起始值,最下方是結束值,也就是說應用窗口的Type值的範圍是1-99,這個數值的大小涉及窗口的層級this

Sub Window(子窗口)spa

子窗口不可以獨立存在,要依附在其餘窗口上才行,PopupWindow就屬於子窗口,子窗口的定義類型以下:

//子窗口的初始值
        public static final int FIRST_SUB_WINDOW = 1000;

        public static final int TYPE_APPLICATION_PANEL = FIRST_SUB_WINDOW;

        public static final int TYPE_APPLICATION_MEDIA = FIRST_SUB_WINDOW + 1;

        public static final int TYPE_APPLICATION_SUB_PANEL = FIRST_SUB_WINDOW + 2;

        public static final int TYPE_APPLICATION_ATTACHED_DIALOG = FIRST_SUB_WINDOW + 3;

        public static final int TYPE_APPLICATION_MEDIA_OVERLAY  = FIRST_SUB_WINDOW + 4;

        public static final int TYPE_APPLICATION_ABOVE_SUB_PANEL = FIRST_SUB_WINDOW + 5;
        //子窗口的結束值
        public static final int LAST_SUB_WINDOW = 1999;
複製代碼

能夠看出子窗口的type值範圍是1000-1999

System Window (系統窗口)

Toast,輸入法窗口,系統音量條窗口,系統錯誤窗口,都屬於系統窗口,系統窗口的類型定義以下:

public static final int FIRST_SYSTEM_WINDOW     = 2000;
        public static final int TYPE_STATUS_BAR         = FIRST_SYSTEM_WINDOW;
        public static final int TYPE_SEARCH_BAR         = FIRST_SYSTEM_WINDOW+1;
        @Deprecated
        public static final int TYPE_PHONE              = FIRST_SYSTEM_WINDOW+2;
        @Deprecated
        public static final int TYPE_SYSTEM_ALERT       = FIRST_SYSTEM_WINDOW+3;
        public static final int TYPE_KEYGUARD           = FIRST_SYSTEM_WINDOW+4;
        @Deprecated
        public static final int TYPE_TOAST              = FIRST_SYSTEM_WINDOW+5;
        @Deprecated
        public static final int TYPE_SYSTEM_OVERLAY     = FIRST_SYSTEM_WINDOW+6;
        @Deprecated
        public static final int TYPE_PRIORITY_PHONE     = FIRST_SYSTEM_WINDOW+7;
        ...
        public static final int LAST_SYSTEM_WINDOW      = 2999;
複製代碼

系統窗口接近40個,這裏只列出一小部分,系統窗口的Type值在2000-2999之間

窗口的顯示次序

上面介紹的Type值越大,就意味着靠用戶越近,很顯然系統的窗口是最大的,他在應用窗口和子窗口的上方

FLAG 窗口的標誌

Flag就是窗口的標誌,用於控制Window的顯示,一樣被定義在WindowManager的內部類LayoutParams中,一共有20多個,這裏列出一些經常使用的

type 描述
FLAG_ALLOW_LOCK_WHILE_SCREEN_ON 只要窗口可見,就容許在開啓狀態的屏幕上鎖屏
FLAG_NOT_FOCUSABLE 窗口不能獲取輸入焦點,設置該標誌的同時,FLAG_NOT_TOUCH_MODAL也會被設置
FLAG_NOT_TOUCH_MODAL 將該窗口區域外的觸摸事件,傳遞給其餘Window,而本身只會處理窗口區域內的觸摸事件
FLAG_NOT_TOUCHABLE 窗口不接受任何觸摸事件
FLAG_KEEP_SCREEN_ON 只要窗口可見,就一直保持屏幕長亮
FLAG_LAYOUT_NO_LIMITS 容許窗口超出屏幕外
FLAG_FULLSCREEN 隱藏全部的屏幕裝飾窗口,好比遊戲視頻等全屏顯示
FLAG_SHOW_WHEN_LOCKED 窗口能夠在鎖屏窗口之上顯示
FLAG_IGNORE_CHEEK_PRESSES 當用戶臉貼近屏幕時(好比打電話時),不會響應此事件
FLAG_TURN_SCREEN_ON 窗口顯示時將屏幕點亮

設置Window的Flag除了上方的方式外還能夠採用下面的方式

//第一種
        Window window = getWindow();
        window.addFlags();
        
        //第二種
        Window window = getWindow();
        window.setFlags();
複製代碼

軟鍵盤模式

咱們在寫登錄界面的時候,默認彈出的軟鍵盤窗口可能會覆蓋輸入框下面的按鈕,爲了讓軟鍵盤按照指望的方式顯示,,WindowMagaer的靜態內部類LayoutParams中定義了軟鍵盤的相關模式,咱們介紹一下經常使用的

SoftInputMode 描述
SOFT_INPUT_STATE_UNSPECIFIED 沒有指定狀態,系統會選擇一個合適的狀態或依賴於主題的設置
SOFT_INPUT_STATE_UNCHANGED 不會改變軟鍵盤的狀態
SOFT_INPUT_STATE_HIDDEN 當用戶進入該窗口時,軟鍵盤默認隱藏
SOFT_INPUT_STATE_ALWAYS_HIDDEN 當窗口獲取焦點時,軟鍵盤老是隱藏
SOFT_INPUT_ADJUST_RESIZE 當軟鍵盤彈出時,窗口會調整大小
SOFT_INPUT_ADJUST_PAN 當軟鍵盤彈出時,窗口不須要調整大小,要確認輸入焦點是否可見

軟鍵盤模式能夠在AndroidManifest中設置

<activity android:name=".CameraActivity"
            android:launchMode="singleTask"
            android:windowSoftInputMode="adjustPan">

        </activity>
複製代碼

也能夠代碼設置

getWindow().setSoftInputMode();
複製代碼

WindowManager

WindowMagaer所提供的功能很簡單,只有經常使用的三個方法即,添加View,更新View,刪除View,這個三個方法定義在ViewManager中,而WindowManager繼承自ViewManager

public interface ViewManager{
    public void addView(View view, ViewGroup.LayoutParams params);
    public void updateViewLayout(View view, ViewGroup.LayoutParams params);
    public void removeView(View view);
}
複製代碼

Window的內部機制

Window是一個抽象概念,每個Window都對應一個View和一個ViewRootImplWindowView經過ViewRootImpl來創建聯繫,所以Window不是實際存在的,他是以View的形式存在的,在實際是一箇中,不能直接訪問Window,只有經過WindowManager才能訪問

Window的添加過程

Window的添加是經過WindowManageraddView方法實現的,咱們WindowManager##addView方法做爲入口來分析,WindowMagager是一個接口,真正的實如今WindowManagerImpl

@Override
    public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
        applyDefaultToken(params);
        mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
    }
複製代碼

咱們發現他其實把事情交給了WindowManagerGlobal

public void addView(View view, ViewGroup.LayoutParams params, Display display, Window parentWindow) {
        //註釋1
        if (view == null) {
            throw new IllegalArgumentException("view must not be null");
        }
        if (display == null) {
            throw new IllegalArgumentException("display must not be null");
        }
        if (!(params instanceof WindowManager.LayoutParams)) {
            throw new IllegalArgumentException("Params must be WindowManager.LayoutParams");
        }
            ····
            //註釋2
            root = new ViewRootImpl(view.getContext(), display);

            view.setLayoutParams(wparams);
            //註釋3
            mViews.add(view);
            mRoots.add(root);
            mParams.add(wparams);

            // do this last because it fires off messages to start doing things
            try {
                //註釋4
                root.setView(view, wparams, panelParentView);
            } catch (RuntimeException e) {
                // BadTokenException or InvalidDisplayException, clean up.
                if (index >= 0) {
                    removeViewLocked(index, true);
                }
                throw e;
            }
        }
    }
複製代碼

咱們首先了解幾個重要的變量

//儲存全部Window對應的View
    private final ArrayList<View> mViews = new ArrayList<View>();
    //儲存全部Window對應的ViewRoot
    private final ArrayList<ViewRootImpl> mRoots = new ArrayList<ViewRootImpl>();
    //佈局參數列表
    private final ArrayList<WindowManager.LayoutParams> mParams =
            new ArrayList<WindowManager.LayoutParams>();
複製代碼
  • 註釋1處,它主要是檢查參數是否合法
  • 註釋2處,在此處建立了ViewRootImpl並賦值給root變量
  • 註釋3處,將View,root和params添加到列表中
  • 註釋4處,調用ViewRootImpl來更新界面並完成Window的添加過程

ViewRootImpl有不少的職責

  • View樹的根,並管理View樹
  • 觸發View的測量,佈局和繪製
  • 輸入時間的中轉站
  • 管理Surface
  • 負責與WMS通訊

咱們繼續看一下ViewRootImplsetView方法

public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
        synchronized (this) {
        ···
             // Schedule the first layout -before- adding to the window
                // manager, to make sure we do the relayout before receiving
                // any other events from the system.
                requestLayout();
        ···
           try {
                    mOrigWindowType = mWindowAttributes.type;
                    mAttachInfo.mRecomputeGlobalAttributes = true;
                    collectViewAttributes();
                    res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
                            getHostVisibility(), mDisplay.getDisplayId(), mWinFrame,
                            mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
                            mAttachInfo.mOutsets, mAttachInfo.mDisplayCutout, mInputChannel);
                } catch (RemoteException e) {
    
        }
        }
複製代碼

這個方法首先會調用requestLayout方法來完成一部刷新請求

public void requestLayout() {
        if (!mHandlingLayoutInLayoutRequest) {
            checkThread();
            mLayoutRequested = true;
            scheduleTraversals();
        }
    }
複製代碼

scheduleTraversals實際是View繪製的入口

而後調用mWindowSession.addToDisplay方法,mWindowSession是一個IWindowSession類型的,是一個Binder對象,用於進程間通訊,也就是說addToDisplay方法實際上是運行在WMS所在的進程system_server進程

@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);
    }
複製代碼

addToDisplay方法內部調用了WMSaddWindow方法並將自身也就是Session傳入了進去,每一個應用程序都會有一個SessionWMS會用ArrayList來保存起來,這樣全部的工做都交給了WMS來作

WMS會爲這個添加的窗口分配Surface,並肯定窗口的顯示次序,負責顯示界面的是畫布Surface,而不是窗口自己,WMS會把Surface交給SurfaceFlinger處理,SurfaceFlinger會把這些Surface混合並繪製到屏幕上

Window的更新過程

Window的更新過程和添加過程是相似的,須要調用WindowManagerupdateViewLayout方法,而後會繼續進入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;
        //註釋1
        view.setLayoutParams(wparams);

        synchronized (mLock) {
            //註釋2
            int index = findViewLocked(view, true);
            //註釋3
            ViewRootImpl root = mRoots.get(index);
            //註釋4
            mParams.remove(index);
            //註釋5
            mParams.add(index, wparams);
            //註釋6
            root.setLayoutParams(wparams, false);
        }
    }
複製代碼
  • 註釋1,將更新的參數設置到View中
  • 註釋2,獲得要更新的窗口在View列表中的索引
  • 註釋3,根據索引獲取窗口的ViewRoot
  • 註釋4 5,用於更新佈局參數列表
  • 註釋6,調用ViewRootsetLayoutParams方法,將更新的參數設置到ViewRootImpl中,setLayoutParams方法最終會調用ViewRootImplscheduleTraversals方法,咱們看下這個方法
void scheduleTraversals() {
        if (!mTraversalScheduled) {
            mTraversalScheduled = true;
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
            //註釋1
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
            if (!mUnbufferedInputDispatch) {
                scheduleConsumeBatchedInput();
            }
            notifyRendererOfFramePending();
            pokeDrawLockIfNeeded();
        }
    }
複製代碼

咱們看下注釋1,mChoreographer翻譯爲編舞者,用於接受系統的VSync信號,在下一個幀渲染時控制一些操做,mChoreographerpostCallback方法用於添加回調,這個添加的回調,將在下一幀渲染時執行,這個添加的回調指的是TraversalRunnable類型的mTraversalRunnable,以下:

final class TraversalRunnable implements Runnable {
        @Override
        public void run() {
            doTraversal();
        }
    }
複製代碼

這個方法內部調用了doTraversal

void doTraversal() {
        if (mTraversalScheduled) {
            mTraversalScheduled = false;
            mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);

            if (mProfile) {
                Debug.startMethodTracing("ViewAncestor");
            }

            performTraversals();

            if (mProfile) {
                Debug.stopMethodTracing();
                mProfile = false;
            }
        }
    }
複製代碼

這個方法又調用了performTraversals,這個方法中更新了Window的視圖,而且完成了View的繪製流程,measure,layout,draw,這樣就完成了View的更新

Activity的Window的建立過程

這個須要瞭解App的啓動過程,這個我就再也不重複說了,不瞭解的能夠看我以前的文章Android App啓動過程,他最後會調用performLaunchActivity方法來完成整個啓動過程,這個方法內部會經過類加載器建立Activity的實例對象,並調用了attach方法,爲其關聯運行中所依賴的一系列變量

private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
    ...
    Activity activity = null;
    try {
        java.lang.ClassLoader cl = appContext.getClassLoader();
        activity = mInstrumentation.newActivity(
                cl, component.getClassName(), r.intent);
        ...
    } catch (Exception e) {
        ...
    }

    try {
        // 返回以前建立過的 application 對象
        Application app = r.packageInfo.makeApplication(false, mInstrumentation);
        ...
        if (activity != null) {
            ...
            // attach 到 window 上
            activity.attach(appContext, this, getInstrumentation(), r.token,
                    r.ident, app, r.intent, r.activityInfo, title, r.parent,
                    r.embeddedID, r.lastNonConfigurationInstances, config,
                    r.referrer, r.voiceInteractor, window, r.configCallback);
            ...
            if (r.isPersistable()) {
                mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState);
            } else {
                mInstrumentation.callActivityOnCreate(activity, r.state);
            }
            ...
        }
    } catch (Exception e) {
        ...
    }
    return activity;
}
複製代碼
final void attach(Context context, ActivityThread aThread, Instrumentation instr, IBinder token, int ident, Application application, Intent intent, ActivityInfo info, CharSequence title, Activity parent, String id, NonConfigurationInstances lastNonConfigurationInstances, Configuration config, String referrer, IVoiceInteractor voiceInteractor, Window window, ActivityConfigCallback activityConfigCallback) {
        attachBaseContext(context);

        mFragments.attachHost(null /*parent*/);

        mWindow = new PhoneWindow(this, window, activityConfigCallback);
        mWindow.setWindowControllerCallback(this);
        mWindow.setCallback(this);
        mWindow.setOnWindowDismissedCallback(this);
        mWindow.getLayoutInflater().setPrivateFactory(this);
        if (info.softInputMode != WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED) {
            mWindow.setSoftInputMode(info.softInputMode);
        }
        if (info.uiOptions != 0) {
            mWindow.setUiOptions(info.uiOptions);
        }
        mUiThread = Thread.currentThread();

       ...

        mWindow.setWindowManager(
                (WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
                mToken, mComponent.flattenToString(),
                (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
        if (mParent != null) {
            mWindow.setContainer(mParent.getWindow());
        }
        mWindowManager = mWindow.getWindowManager();
        mCurrentConfig = config;

        mWindow.setColorMode(info.colorMode);

        setAutofillCompatibilityEnabled(application.isAutofillCompatibilityEnabled());
        enableAutofillCompatibilityIfNeeded();
    }
複製代碼

這個方法,會建立Activity所屬的Window對象併爲其設置回調接口,到這裏Window已經建立完成了,下面咱們分析一下Activity的視圖是怎麼依附到Window上的,因爲Activity的視圖是從setContentView方法提供,咱們從setContentView方法開始分析

getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
複製代碼

咱們點進去發現他其實調用了PhoneWindow的setContentView方法,咱們看下這個方法

public void setContentView(int layoutResID) {
        // Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
        // decor, when theme attributes and the like are crystalized. Do not check the feature
        // before this happens.
        if (mContentParent == null) {
            installDecor();
        } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            mContentParent.removeAllViews();
        }

        if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
                    getContext());
            transitionTo(newScene);
        } else {
            mLayoutInflater.inflate(layoutResID, mContentParent);
        }
        mContentParent.requestApplyInsets();
        final Callback cb = getCallback();
        if (cb != null && !isDestroyed()) {
            cb.onContentChanged();
        }
        mContentParentExplicitlySet = true;
    }

複製代碼

這個方法主要作了如下幾件事

  • 若是DecorView不存在則建立DecorView,DecorViewActivity中的頂級View,通常來講他包括標題欄內容欄,這個會隨着主題的改變而改變,反正內容欄必定存在,而且他有固定的idandroid.R.id.content, 建立DecorViewinstallDecor方法完成,內部會經過generateDecor方法建立,這個時候DecorView仍是一個空白的Framlayout
private void installDecor() {
        mForceDecorInstall = false;
        if (mDecor == null) {
            //建立DecroView
            mDecor = generateDecor(-1);
            mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
            mDecor.setIsRootNamespace(true);
            if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
                mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
            }
        } else {
            mDecor.setWindow(this);
        }
        if (mContentParent == null) {
            //向DecorView添加內容
            mContentParent = generateLayout(mDecor);
        }
        ...
    }
複製代碼
protected DecorView generateDecor(int featureId) {
        ...
        return new DecorView(context, featureId, this, getAttributes());
    }
複製代碼
  • 初始化DecorView的結構,經過generateLayout方法加載具體的佈局文件到DecorView中,併爲內容欄變量賦值
ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
複製代碼
  • 而後將setContentViewView添加到內容欄中, mLayoutInflater.inflate(layoutResID, mContentParent);這時Activity的佈局文件就已經添加到了DecorView的內容欄中
  • 最後回調onContentChanged方法,通知Activity視圖已經改變

經過上方的步驟,如今DecorView已經建立並初始化完成,Activity的佈局也添加到DecorView的內容欄中,可是這個時候DecorView尚未被WindowManager添加到Window

ActivityThreadhandleResumeActivity會調用ActivityonResume方法,而且會調用ViewManageraddView方法把DecorView添加到Window

@Override
    public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward, String reason) {

    
        //調用Activity的onResume方法
        final ActivityClientRecord r = performResumeActivity(token, finalStateRequest, reason);
        ...
        if (r.window == null && !a.mFinished && willBeVisible) {
            r.window = r.activity.getWindow();
            View decor = r.window.getDecorView();
            decor.setVisibility(View.INVISIBLE);
            //獲取WindowMagaer
            ViewManager wm = a.getWindowManager();
            WindowManager.LayoutParams l = r.window.getAttributes();
            a.mDecor = decor;
            l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
         
            if (a.mVisibleFromClient) {
                if (!a.mWindowAdded) {
                    a.mWindowAdded = true;
                    //把DecorView添加到Window
                    wm.addView(decor, l);
                } 
                ...
            
    }
複製代碼

到這裏Activity的Window建立過程分析完畢

參考:《Android開發藝術探索》《Android進階解密》

相關文章
相關標籤/搜索