Window源碼解析(一):與DecorView的那些事

博文出處:Window源碼解析(一):與DecorView的那些事,歡迎大家關注我的博客,謝謝!

注:本文解析的源碼基于 API 25,部分內容來自于《Android開發藝術探索》。

Header

今天我們來講講 Window ,Window 代表著一個窗口。

比如在 Activity 中,我們可以設置自定義的視圖 View ,其實 View 并不是直接附著在 Activity 上,而是 View 附著在 Window 上,Activity 又持有一個 Window 對象。可見,Window 是一個重要的角色,主要用來負責管理 View 的。而 Window 和 View 又是通過 ViewRootImpl 來建立聯系的,這在之前的《View的工作原理》中介紹過。

所以一個 Window 就對應著一個 View 和一個 ViewRootImpl 。

同理,Dialog 和 Toast 等的視圖也都是附著在 Window 上。

除此之外,相信看過《Android開發藝術探索》的同學都知道。Window 有三種類型,分別對應著:

  1. 應用 Window ,即 Activity 的 Window 。對應的 type 為1~99;
  2. 子 Window ,比如 Dialog 的 Window ,子 Window 并不能單獨存在,需要有父 Window 的支持。對應的 type 為1000~1999;
  3. 系統 Window ,需要權限聲明才可以創建,比如常用的 Toast 和狀態欄等都是系統級別的 Window。對應的 type 為2000~2999;

這三種 Window 的區分方法就是依靠 WindowManager.LayoutParams 中的 type 來決定的。type 越大,Window 就越顯示在層級頂部。

粗看有這么多知識點,所以我們確實有必要對 Window 好好深入了解一下。在這,我們先詳細介紹一下 Window 和 Activity 的那些“糾葛”,然后再深入 Window 的內部機制。

初見Window

Activity

attach(Context context, ActivityThread aThread, ...)

Window 第一次出現在 Activity 的視野中,是在 Activity 的 attach 方法中,具體代碼如下:

    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) {
        attachBaseContext(context);

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

        // 創建 window 對象
        mWindow = new PhoneWindow(this, window);
        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);
        }
        
        ...

        // 設置窗口管理器,其實是創建了 WindowManagerImpl 對象
        // WindowManager 是接口,而 WindowManagerImpl 是 WindowManger 的實現類
        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;
    }

在方法中創建了一個 PhoneWindow 對象,而 PhoneWindow 其實就是 Window 的具體實現類,Window 只是一個接口而已。之后設置了回調,這樣當 Window 接收到觸摸或者按鍵等事件后,會回調給 Activity 。

另外還給 Window 對象設置了窗口管理器,也就是我們經常用到的 WindowManager 。

WindowManager 是外界接觸 Window 的入口,也就是說,想要對 Window 進行一些操作需要用過 WindowManager 來完成。

與DecorView的那些事

在開頭中說到,Window 是用來負責管理 View 的。

現在 Window 已經創建完畢了,那么到底什么時候與 View 發生了交集了呢?

我們需要深入到 onCreate() 中一個熟悉的方法: setContentView(R.layout.activity_main)

Activity

setContentView(@LayoutRes int layoutResID)

    public void setContentView(@LayoutRes int layoutResID) {
        // 這里 getWindow 得到的正是上面創建的 PhoneWindow 對象
        getWindow().setContentView(layoutResID);
        initWindowDecorActionBar();
    }

發現它調用的是 Window 中的同名方法。

接著到 PhoneWindow 中跟進,查看具體實現的邏輯。

PhoneWindow

setContentView(int layoutResID)

    @Override
    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.
        
        // mContentParent 是放置窗口內容的父 viewgroup ,可能是 decorView 本身,也有可能是它的子 viewgroup
        // 如果 mContentParent 是空的,那么就說明 decorView 是空的
        if (mContentParent == null) {
            // 創建 decorview
            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 {
            // 將 layout 布局加入到 mContentParent 中并去解析 layout xml 文件
            mLayoutInflater.inflate(layoutResID, mContentParent);
        }
        mContentParent.requestApplyInsets();
        // 通知 activity 窗口內容已經發生變化了
        final Callback cb = getCallback();
        if (cb != null && !isDestroyed()) {
            cb.onContentChanged();
        }
        mContentParentExplicitlySet = true;
    }

setContentView(int layoutResID) 中,一開始判斷了 mContentParent 。mContentParent 其實就是我們設置的 contentView 的父視圖。

關于 mContentParent ,在 PhoneWindow 中有注釋:

// This is the view in which the window contents are placed. It is either
// mDecor itself, or a child of mDecor where the contents go.

意思就是說,當我們不需要 titlebar 的時候,mContentParent 其實就和 DecorView 一樣了;有 titlebar 的時候,DecorView 的內容就分為了 titlebar 和 mContentParent 。

所以如果 mContentParent 為空,那么可以說明還沒有創建過 DecorView 。

我們總結一下,在 setContentView(int layoutResID) 中主要就是這三件事:

  1. 創建 DecorView 視圖對象;
  2. 將自定義的視圖 layout_main.xml 進行解析并添加到 mContentParent 中;
  3. 去通知 activity 窗口視圖已經改變了,進行相關操作;

我們去 installDecor() 中看看究竟怎么創建 DecorView 的。

installDecor()

    private void installDecor() {
        mForceDecorInstall = false;
        if (mDecor == null) {
            // 如果 decorview 為空,調用 generateDecor 來創建 decorview
            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) {
            // 創建 mContentParent ,也就是 contentView 的父視圖
            mContentParent = generateLayout(mDecor);

            // Set up decor part of UI to ignore fitsSystemWindows if appropriate.
            mDecor.makeOptionalFitsSystemWindows();

            final DecorContentParent decorContentParent = (DecorContentParent) mDecor.findViewById(
                    R.id.decor_content_parent);

            ...

            }
        }
    }

installDecor() 中,調用了 generateDecor() 方法來創建 DecorView;

之后又調用 generateLayout(mDecor) 來創建 mContentParent 。

generateDecor(int featureId)

    protected DecorView generateDecor(int featureId) {
        // System process doesn't have application context and in that case we need to directly use
        // the context we have. Otherwise we want the application context, so we don't cling to the
        // activity.

        // 得到 context 上下文
        Context context;
        if (mUseDecorContext) {
            Context applicationContext = getContext().getApplicationContext();
            if (applicationContext == null) {
                context = getContext();
            } else {
                context = new DecorContext(applicationContext, getContext().getResources());
                if (mTheme != -1) {
                    context.setTheme(mTheme);
                }
            }
        } else {
            context = getContext();
        }
        // 創建 DecorView 對象
        return new DecorView(context, featureId, this, getAttributes());
    }

generateDecor(int featureId) 方法比較簡單,之前初始化了一下 context ,然后直接 new 了一個 DecorView 完事!

generateLayout(DecorView decor)

    protected ViewGroup generateLayout(DecorView decor) {
        // 應用當前的主題,比如設置一些 window 屬性等
        ... 

        // 根據主題設置去選擇 layoutResource
        // 這個 layoutResource 也就是 DecorView 的子 View 的布局
        int layoutResource;
        int features = getLocalFeatures();

        ...

        else if ((features & (1 << FEATURE_NO_TITLE)) == 0) {
            // If no other features and not embedded, only need a title.
            // If the window is floating, we need a dialog layout
            if (mIsFloating) {
                TypedValue res = new TypedValue();
                getContext().getTheme().resolveAttribute(
                        R.attr.dialogTitleDecorLayout, res, true);
                layoutResource = res.resourceId;
            } else if ((features & (1 << FEATURE_ACTION_BAR)) != 0) {
                layoutResource = a.getResourceId(
                        R.styleable.Window_windowActionBarFullscreenDecorLayout,
                        R.layout.screen_action_bar);
            } else {
                // 比較常見的就是這種布局
                layoutResource = R.layout.screen_title;
            }
            // System.out.println("Title!");
        } else if ((features & (1 << FEATURE_ACTION_MODE_OVERLAY)) != 0) {
            layoutResource = R.layout.screen_simple_overlay_action_mode;
        } else {
            // Embedded, so no decoration is needed.
            layoutResource = R.layout.screen_simple;
            // System.out.println("Simple!");
        }

        mDecor.startChanging();
        // 這個方法里將上面 layoutResource 的布局轉換并添加到 DecorVew 中
        mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
        // 得到 contentParent(id = android.R.id.content), 也就是我們 setContentView 的父視圖
        ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
        if (contentParent == null) {
            throw new RuntimeException("Window couldn't find content container view");
        }

        if ((features & (1 << FEATURE_INDETERMINATE_PROGRESS)) != 0) {
            ProgressBar progress = getCircularProgressBar(false);
            if (progress != null) {
                progress.setIndeterminate(true);
            }
        }

        if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {
            registerSwipeCallbacks();
        }

        // Remaining setup -- of background and title -- that only applies
        // to top-level windows.
        // 背景設置和標題設置
        if (getContainer() == null) {
            final Drawable background;
            if (mBackgroundResource != 0) {
                background = getContext().getDrawable(mBackgroundResource);
            } else {
                background = mBackgroundDrawable;
            }
            mDecor.setWindowBackground(background);

            final Drawable frame;
            if (mFrameResource != 0) {
                frame = getContext().getDrawable(mFrameResource);
            } else {
                frame = null;
            }
            mDecor.setWindowFrame(frame);

            mDecor.setElevation(mElevation);
            mDecor.setClipToOutline(mClipToOutline);

            if (mTitle != null) {
                setTitle(mTitle);
            }

            if (mTitleColor == 0) {
                mTitleColor = mTextColor;
            }
            setTitleColor(mTitleColor);
        }

        mDecor.finishChanging();
        
        return contentParent;
    }

這個方法中大致的邏輯就是,根據主題的設置情況來選擇 DecorView 子 View 的 layoutResource 。在這,我們就看看最常用的一種布局 R.layout.screen_title (位于 /frameworks/base/core/res/res/layout/screen_title.xml ):

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:fitsSystemWindows="true">
    <!-- Popout bar for action modes -->
    <ViewStub android:id="@+id/action_mode_bar_stub"
              android:inflatedId="@+id/action_mode_bar"
              android:layout="@layout/action_mode_bar"
              android:layout_width="match_parent"
              android:layout_height="wrap_content"
              android:theme="?attr/actionBarTheme" />
    <FrameLayout
        android:layout_width="match_parent" 
        android:layout_height="?android:attr/windowTitleSize"
        style="?android:attr/windowTitleBackgroundStyle">
        <TextView android:id="@android:id/title" 
            style="?android:attr/windowTitleStyle"
            android:background="@null"
            android:fadingEdge="horizontal"
            android:gravity="center_vertical"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />
    </FrameLayout>
    <FrameLayout android:id="@android:id/content"
        android:layout_width="match_parent" 
        android:layout_height="0dip"
        android:layout_weight="1"
        android:foregroundGravity="fill_horizontal|top"
        android:foreground="?android:attr/windowContentOverlay" />
</LinearLayout>

我們可以看到,DecorView 的子 View 其實是一個 LinearLayout ,而 LinearLayout 中有分為 titlebar 和 id 為 android:id/content 的 FrameLayout(其實就是 mContentParent)。

之后將這個視圖創建出來并添加到 DecorView 中。

具體的代碼可以深入 DecorView 的 onResourcesLoaded(LayoutInflater inflater, int layoutResource) 中去看:

    void onResourcesLoaded(LayoutInflater inflater, int layoutResource) {
        mStackId = getStackId();

        if (mBackdropFrameRenderer != null) {
            loadBackgroundDrawablesIfNeeded();
            mBackdropFrameRenderer.onResourcesLoaded(
                    this, mResizingBackgroundDrawable, mCaptionBackgroundDrawable,
                    mUserCaptionBackgroundDrawable, getCurrentColor(mStatusColorViewState),
                    getCurrentColor(mNavigationColorViewState));
        }

        mDecorCaptionView = createDecorCaptionView(inflater);
        // 解析之前選擇出來的 layoutResource ,該 root 也就是 DecorView 的直接子 View
        final View root = inflater.inflate(layoutResource, null);
        if (mDecorCaptionView != null) {
            if (mDecorCaptionView.getParent() == null) {
                addView(mDecorCaptionView,
                        new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
            }
            mDecorCaptionView.addView(root,
                    new ViewGroup.MarginLayoutParams(MATCH_PARENT, MATCH_PARENT));
        } else {
            // Put it below the color views.
            // 將 root 視圖添加到 DecorView 中
            addView(root, 0, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
        }
        // 可以看出成員變量 mContentRoot 就是 DecorView 的直接子 View
        // 也就是 mContentParent 的父視圖
        mContentRoot = (ViewGroup) root;
        initializeElevation();
    }

看到這,我們可以畫一張圖出來了,把 PhoneWindow 、DecorView 和 mContentParent 都理清楚:

View層級

然后進行標題設置之類的工作。最后得到并返回 mContentParent 。

到了這里,基本上把 Window 、DecorView 和 Activity 三者之間的關系整理清楚了,但是事情并沒有結束。這時候的 DecorView 并沒有真正添加到 Window 上去,只是創建出對象了并解析了視圖而已。DecorView 還沒有被 WindowManager 識別,Window 也還無法接受外界的輸入信息。

那么,到底 DecorView 是什么時候附著到 Window 上去的?

這個答案需要我們到 ActivityThread 的 handleResumeActivity() 中找找了。回調 Activity 的 onResume() 生命周期后,又調用了 Activity 的 makeVisible() 方法。

Activity

makeVisible()

    void makeVisible() {
        if (!mWindowAdded) {
            // WindowManager 是 ViewManager 的實現類
            ViewManager wm = getWindowManager();
            // 將 decorview 添加到 window 中
            wm.addView(mDecor, getWindow().getAttributes());
            mWindowAdded = true;
        }
        // 設置 decorview 可見
        mDecor.setVisibility(View.VISIBLE);
    }

走完這步,DecorView 才完成添加和顯示出來,Activity 的視圖才能被用戶看到。

整個 Window 創建的流程也結束了。

Footer

Window 和 Decor 的“愛恨情仇”到這里就告一段落了,但是 Window 的內部機制我們還可以好好敘一敘。

注意到上面 WindowManager 的 addView 方法了吧?

Window 是怎么添加上去的,究竟在這里面發生了什么事呢?

只能留到下一篇再詳細講講了。

bye bye !

References

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,563評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,694評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,672評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,965評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,690評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,019評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,013評論 3 449
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,188評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,718評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,438評論 3 360
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,667評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,149評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,845評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,252評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,590評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,384評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,635評論 2 380

推薦閱讀更多精彩內容