Android:關于Window少為人知的一面

前言

大部分情況下,我們和Window打交道的情況比較少,一般都是與Activity和View“交流”。最近做了不少與Window相關的工作,梳理了下Window相關的知識,在此,與大家分享下。關于系統源碼相關的知識如何介紹,其實是比較費神,如果只是貼出源碼,做出解釋,其實不好理解和記憶,并且網上關于源碼分析的文章太多太多。因此,我會結合例子來演示,并把常見問題貼出來并加以分析。我相信,以后面試的時候,你可能會碰到此類問題。

知識點

Window是什么
首先看下官方的定義:

Abstract base class for a top-level window look and behavior policy. An instance of this class should be used as the top-level view added to the window manager. It provides standard UI policies such as a background, title area, default key processing, etc.
The only existing implementation of this abstract class is PhoneWindow, which you should instantiate when needing a Window.

大概翻譯如下:
Window類是一個抽象類,它定義了頂級窗體樣式和行為。一個Window實例應作為頂級View添加到WindowManager中。它提供標準的UI規則,例如背景、標題、默認關鍵過程等。

Window有且僅有一個實現類PhoneWindow,如果我們需要Window實例化PhoneWindow即可。

看到這,其實還是云里霧里,并沒有一個切身的體會,所以還是要繼續分析。

Window的類型

在WindowManager中定義了許多Window類型,總共分為以下三類。
1、Application Window。Value from 1 to 99 .
比如:activity、dialog…
2、Sub Window。Value from 1000 to 1999.
比如:ContextMenu、PopupWindow…
3、System Window。Value from 2000 to 2999.
比如:Toast、SystemAlert、 InputMethod...
關于此處更詳細的講解請參考淺析Android的窗口,本文不做重復介紹。此文解釋的非常全面。

Window token

token這個詞我們在開發的時候可能碰到過,比如某些Dialog的異常信息就經常會報bad Token錯誤。那么Window token起了什么作用。大部分Window Token都是IBinder或者IInterface對象,之所以用IBinder或者IInterface,因為它們能用于跨進程間通信,能夠用于Window的身份驗證,是Window與WMS(WindowManagerService)交流的憑證。在淺析Android的窗口"token的含義"中更詳細的介紹,可以作為參考。
WindowManager.LayoutParams類中有個token字段。

 /**
         * Identifier for this window.  This will usually be filled in for
         * you.
         */
        public IBinder token = null;

父窗口和其子窗口的token對象是同一個,一般都是父窗口賦給子窗口。
在Window中有個方法adjustLayoutParamsForSubWindow就是用于把父窗口的token復制給子窗口。
如果Window沒有父窗口,那么會由WMS為其創建token。

Window的顯示過程

window顯示過程

Window顯示的過程還是挺復雜的,不過其中我們只需關注幾個核心類的即可,比如ViewRootImpl、WindowManagerService。
另外這其中有個Session類,該類的作用是用于單獨標識每個進程,與token的作用類似,只不過token是代表標識Window。

創建一個Window試試

在Activity中,運行下述方法。

    private void generateSystemAlert() {
        //Activity Context
        WindowManager wm = (WindowManager) getSystemService(Context.WINDOW_SERVICE);
        //Application Context
        //WindowManager wm = (WindowManager) getApplicationContext().getSystemService(Context.WINDOW_SERVICE);
        final WindowManager.LayoutParams params = new WindowManager.LayoutParams();
        params.type = WindowManager.LayoutParams.TYPE_SYSTEM_ALERT;
        params.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
        params.width = WindowManager.LayoutParams.MATCH_PARENT;
        params.height = 500;
        params.gravity = Gravity.CENTER;
        final View view = LayoutInflater.from(this).inflate(R.layout.window_system_alert, null);
        wm.addView(view, params);
    }
創建Window

添加Window的過程和往ViewGroup中添加View的過程相當類似。WindowManager管理Window的添加、移除等操作。

WindowManager是一個接口,應用程序可以通過WindowManager來與系統Window服務交流。我們可以通過
Context.getSystemService(Context.WINDOW_SERVICE)
來獲取WindowManager實例。
在上述代碼中,我采取了兩種不同的方式獲取WindowManager對象。一種是直接調用Activity中的getSystemService方法,另外一種是通過調用Application Context的getSystemService方法。

WindowManager不同獲取方式的效果

通過實踐可以得知:
用Application Context獲取的WindowManager實例來創建Window并不隨Activity的消失而消失,只有當進程被殺的時候才消失。
用Activity Context獲取的WindowManager實例來創建Window隨Activity的消失而消失。
另外,上面的動態圖演示的時候,當按下返回鍵的時候,應用程序打印如下錯誤。

E/WindowManager: android.view.WindowLeaked: 
Activity com.kisson.windowtest.MainActivity has leaked window 
android.support.v7.widget.AppCompatTextView{1b90179 V.ED.... ......I. 0,0-1080,200 #7f0c0061 app:id/tv} that was originally added here
                         at android.view.ViewRootImpl.<init>(ViewRootImpl.java:363)
                         at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:271)
                         at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:85)
                         at com.kisson.windowtest.MainActivity.generateSystemAlert(MainActivity.java:93)
                         at com.kisson.windowtest.MainActivity.access$100(MainActivity.java:15)
                         at com.kisson.windowtest.MainActivity$2.onClick(MainActivity.java:31)
                         at android.view.View.performClick(View.java:4780)
                         at android.view.View$PerformClick.run(View.java:19866)

這個報錯我們應該不陌生--“窗體泄露”。特別是使用Dialog的時候,如果Dialog未dismiss,某種情況下就會導致窗體泄露,所以Android提供了DialogFragment來解決此類問題。

通過以上演示,我們可以得出以下問題:
1、為什么獲取WindowManager的Context類型不同,導致Window的生命周期不同。
2、什么情況下發生窗體泄露。

看源碼分析問題

1.Application Context的getSystemService方法
Application Context調用的getSystemService方法是Context抽象類中的一個抽象方法,該方法的實現是在ContextImpl類中。

    @Override
    public Object getSystemService(String name) {
        return SystemServiceRegistry.getSystemService(this, name);
    }

在SystemServiceRegistry類中,找到注冊WINDOW_SERVICE方法如下。

 registerService(Context.WINDOW_SERVICE, WindowManager.class,
                new CachedServiceFetcher<WindowManager>() {
            @Override
            public WindowManager createService(ContextImpl ctx) {
                return new WindowManagerImpl(ctx.getDisplay());
            }});

從此,我們可以看到創建WindowManager對象,最后是調用WindowManagerImpl(Display display)構造方法創建。暫且分析到這,讀者有興趣的話可以追蹤源碼來分析。
2.Activity的getSystemService方法

@Override
public Object getSystemService(@ServiceName @NonNull String name) {
        if (getBaseContext() == null) {
            throw new IllegalStateException(
                    "System services not available to Activities before onCreate()");
        }

        if (WINDOW_SERVICE.equals(name)) {
            return mWindowManager;
        } else if (SEARCH_SERVICE.equals(name)) {
            ensureSearchManager();
            return mSearchManager;
        }
        return super.getSystemService(name);
    }

從代碼可知,Activity重寫了getSystemService,當傳入的參數ServiceName等于WINDOW_SERVICE,直接返回mWindowManager對象。接著,分析mWindowManager是如何創建。
在Activity的attach方法,創建了mWindowManager實例。

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) {
        //......
        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;
    }

mWindowManager是在mWindow(mWindow是Window的實例)的setWindowManager方法中創建。接著來看下setWindowManager方法做了什么。

    public void setWindowManager(WindowManager wm, IBinder appToken, String appName,
            boolean hardwareAccelerated) {
        mAppToken = appToken;
        mAppName = appName;
        mHardwareAccelerated = hardwareAccelerated
                || SystemProperties.getBoolean(PROPERTY_HARDWARE_UI, false);
        if (wm == null) {
            wm = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE);
        }
        mWindowManager = ((WindowManagerImpl)wm).createLocalWindowManager(this);
    }

在setWindowManager方法中有個參數也是WindowManager對象(該對象就是ContextImpl中getSystemService獲取到的),然后調用該對象的createLocalWindowManager方法創建Activity的mWindowManager實例。

    public WindowManagerImpl createLocalWindowManager(Window parentWindow) {
        return new WindowManagerImpl(mDisplay, parentWindow);
    }

到此就明朗,通過調用WindowManagerImpl(Display,Window)的構造方法創建mWindowManager對象。

所以我們可得出Application Context和Activity Context創建的WindowManager對象是通過WindowManagerImpl不同的構造方法來創建的。

    public WindowManagerImpl(Display display) {
        this(display, null);
    }

    private WindowManagerImpl(Display display, Window parentWindow) {
        mDisplay = display;
        mParentWindow = parentWindow;
    }

Application Context創建的WindowManagerImpl對象是沒有父窗口(parentWindow)。
Activity Context創建的WindowManagerImpl對象會將Activity的mWindow作為父窗口。
通過分析和前面的演示,可以得出結論。
1.如果某個Window有parentWindow,當parentWindow remove之后,子窗口也會被remove。
2.若有子Window未remove,則會出現窗體泄露錯誤信息。

有結論了,接下來就驗證下。
對Android源碼搜索報錯關鍵句“that was originally added here”,發現在WinowManagerGlobal的closeAll方法出現該語句。其實從該方法的名字就可以猜出來,關閉所有窗口。

    public void closeAll(IBinder token, String who, String what) {
        synchronized (mLock) {
            int count = mViews.size();
            //Log.i("foo", "Closing all windows of " + token);
            for (int i = 0; i < count; i++) {
                //Log.i("foo", "@ " + i + " token " + mParams[i].token
                //        + " view " + mRoots[i].getView());
                if (token == null || mParams.get(i).token == token) {
                    ViewRootImpl root = mRoots.get(i);

                    //Log.i("foo", "Force closing " + root);
                    if (who != null) {
                        WindowLeaked leak = new WindowLeaked(
                                what + " " + who + " has leaked window "
                                + root.getView() + " that was originally added here");
                        leak.setStackTrace(root.getLocation().getStackTrace());
                        Log.e(TAG, "", leak);
                    }

                    removeViewLocked(i, false);
                }
            }
        }
    }

接著,看下closeAll在哪調用。繼續搜索,發現在ActivityThread的handleDestroyActivity方法中調用了closeAll方法。

private void handleDestroyActivity(IBinder token, boolean finishing,
            int configChanges, boolean getNonConfigInstance) {
        ActivityClientRecord r = performDestroyActivity(token, finishing,
                configChanges, getNonConfigInstance);
        //......
            if (r.mPendingRemoveWindow == null) {
                //讀者可以仔細體會此段英文
                // If we are delaying the removal of the activity window, then
                // we can't clean up all windows here.  Note that we can't do
                // so later either, which means any windows that aren't closed
                // by the app will leak.  Well we try to warning them a lot
                // about leaking windows, because that is a bug, so if they are
                // using this recreate facility then they get to live with leaks.
                WindowManagerGlobal.getInstance().closeAll(token,
                        r.activity.getClass().getName(), "Activity");
            }
    //......
    }

到此,可以知道,在Activity destroy時,會去檢查是否有窗體泄露。

這里我們要注意,窗體泄露只會在堆棧中打印錯誤信息,不會導致應用程序崩潰。

從Activity到Window

Activity使我們開發過程中最經常打交道的組件,因此從Activity和Window的關系,可以更好的幫助我們理解Window是啥玩意。
創建頁面布局,我們都是通過調用Activity的setContentView方法

    public void setContentView(@LayoutRes int layoutResID) {
        getWindow().setContentView(layoutResID);
        initWindowDecorActionBar();
    }

從這里,我們可以知道,最終是調用Window的setContentView方法,該方法的實現是在PhoneWindow中。

@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.
        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();
        }
    }

從該代碼中,大概做了如下操作:

  1. installDecor.
    1).generateDecor
    2).generateLayout
  2. inflate your layout.

DecorView是一個自定義FrameLayout,它是作為Activity中Window的頂級View,比如狀態欄,標題欄等。因為每個非全屏Activity都顯示狀態欄,所以系統把某些共性的View都抽象出來,避免我們做重復工作。如果各位想要更深入了解DecorView,可以結合網上資料進行理解。generateDecor的過程就是創建DecorView的過程。

每個Activity都是有自己資源ID形式的布局。在填充資源layout時候,會根據不同的feature來選擇不同的布局。
大概有如下幾種。

R.layout.screen_title_icons
R.layout.screen_progress
R.layout.screen_custom_title
R.layout.screen_action_bar
R.layout.screen_simple_overlay_action_mode;
R.layout.screen_simple

接著我們來來看R.layout.screen_simple布局是什么樣子。

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    android:orientation="vertical">
    <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:id="@android:id/content"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         android:foregroundInsidePadding="false"
         android:foregroundGravity="fill_horizontal|top"
         android:foreground="?android:attr/windowContentOverlay" />
</LinearLayout>

AndroidUI優化實踐有介紹,ViewStub用于懶加載actionBar,而id為@android:id/content的FrameLayout,此FrameLayout就是contentView。我們在Activity中調用setContentView方法,設置布局,最終就是添加到該FrameLayout中。

Activity中View層級

從上圖中,我們很清楚的看到整個View的層次。DecorView最為Activity Window中的頂級View。

分析到這里,整個Activity需要顯示的View創建好了,那么如何顯示?
在Activity中,我并未找到顯示DecorView的方法。根據閱讀源碼經驗,應該會在ActivityThread中。
在ActivityThread的handleResumeActivity中

if (r.window == null && !a.mFinished && willBeVisible) {
                r.window = r.activity.getWindow();
                View decor = r.window.getDecorView();
                decor.setVisibility(View.INVISIBLE);
                ViewManager wm = a.getWindowManager();
                WindowManager.LayoutParams l = r.window.getAttributes();
                a.mDecor = decor;
                l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
                l.softInputMode |= forwardBit;
                if (a.mVisibleFromClient) {
                    a.mWindowAdded = true;
                    wm.addView(decor, l);
                }

我們可以看到DecorView通過wm添加到系統中。

創建Window的實質

從以上的分析,我們可以得出創建Window的過程其實就是添加View的過程。比如Activity中decorView是作為頂級View添加到系統中去顯示,頂級View的LayoutParams必須是WindowManager.LayoutParams類型,否則會報錯。那么為什么會有Window的存在,我們直接用添加View形式,創建Activity所需顯示的界面不久就了。
這里就又回到最初Window的定義,Window它定義了頂級窗體樣式和行為。因為每個Activity都會標題欄,狀態欄等,在Window中它提供了DecorView的創建,省去我們不少麻煩,通過Window提供API,我們可以很方面改變標題欄,狀態欄的樣式。同時Window也提供某些共性操作的行為,比如返回鍵操作、觸摸事件傳遞,menu顯示與因此等。Window最核心的內容還是它提供的頂級View--DecorView及其相關操作。

最后

Window在我們開發過程中起著至關重要的地位。因此,深入了解Window對我們解決一系列UI問題大有裨益。你深入了解Window后就會知道:
1.為什么Dialog的創建必須要用Activity Context。
2.為什么PopupWindow的生命周期與Activity綁定。
3.為什么我創建的Window不能顯示、不能點擊等等。

如果覺得對你有幫助,請不要吝惜您的喜歡!謝謝!

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

推薦閱讀更多精彩內容