前言
大部分情況下模暗,我們和Window打交道的情況比較少,一般都是與Activity和View“交流”念祭。最近做了不少與Window相關(guān)的工作兑宇,梳理了下Window相關(guān)的知識(shí),在此粱坤,與大家分享下隶糕。關(guān)于系統(tǒng)源碼相關(guān)的知識(shí)如何介紹瓷产,其實(shí)是比較費(fèi)神,如果只是貼出源碼枚驻,做出解釋濒旦,其實(shí)不好理解和記憶,并且網(wǎng)上關(guān)于源碼分析的文章太多太多再登。因此尔邓,我會(huì)結(jié)合例子來演示,并把常見問題貼出來并加以分析锉矢。我相信梯嗽,以后面試的時(shí)候,你可能會(huì)碰到此類問題沽损。
知識(shí)點(diǎn)
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類是一個(gè)抽象類灯节,它定義了頂級(jí)窗體樣式和行為。一個(gè)Window實(shí)例應(yīng)作為頂級(jí)View添加到WindowManager中绵估。它提供標(biāo)準(zhǔn)的UI規(guī)則炎疆,例如背景、標(biāo)題国裳、默認(rèn)關(guān)鍵過程等形入。
Window有且僅有一個(gè)實(shí)現(xiàn)類PhoneWindow,如果我們需要Window實(shí)例化PhoneWindow即可躏救。
看到這唯笙,其實(shí)還是云里霧里,并沒有一個(gè)切身的體會(huì)盒使,所以還是要繼續(xù)分析崩掘。
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...
關(guān)于此處更詳細(xì)的講解請(qǐng)參考淺析Android的窗口,本文不做重復(fù)介紹。此文解釋的非常全面暂筝。
Window token
token這個(gè)詞我們?cè)陂_發(fā)的時(shí)候可能碰到過箩言,比如某些Dialog的異常信息就經(jīng)常會(huì)報(bào)bad Token錯(cuò)誤。那么Window token起了什么作用焕襟。大部分Window Token都是IBinder或者IInterface對(duì)象陨收,之所以用IBinder或者IInterface,因?yàn)樗鼈兡苡糜诳邕M(jìn)程間通信鸵赖,能夠用于Window的身份驗(yàn)證务漩,是Window與WMS(WindowManagerService)交流的憑證。在淺析Android的窗口"token的含義"中更詳細(xì)的介紹卫漫,可以作為參考菲饼。
WindowManager.LayoutParams類中有個(gè)token字段肾砂。
/**
* Identifier for this window. This will usually be filled in for
* you.
*/
public IBinder token = null;
父窗口和其子窗口的token對(duì)象是同一個(gè)列赎,一般都是父窗口賦給子窗口。
在Window中有個(gè)方法adjustLayoutParamsForSubWindow就是用于把父窗口的token復(fù)制給子窗口镐确。
如果Window沒有父窗口包吝,那么會(huì)由WMS為其創(chuàng)建token。
Window的顯示過程
Window顯示的過程還是挺復(fù)雜的源葫,不過其中我們只需關(guān)注幾個(gè)核心類的即可诗越,比如ViewRootImpl、WindowManagerService息堂。
另外這其中有個(gè)Session類嚷狞,該類的作用是用于單獨(dú)標(biāo)識(shí)每個(gè)進(jìn)程,與token的作用類似荣堰,只不過token是代表標(biāo)識(shí)Window床未。
創(chuàng)建一個(gè)Window試試
在Activity中,運(yùn)行下述方法振坚。
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的過程和往ViewGroup中添加View的過程相當(dāng)類似薇搁。WindowManager管理Window的添加、移除等操作渡八。
WindowManager是一個(gè)接口啃洋,應(yīng)用程序可以通過WindowManager來與系統(tǒng)Window服務(wù)交流。我們可以通過
Context.getSystemService(Context.WINDOW_SERVICE)
來獲取WindowManager實(shí)例屎鳍。
在上述代碼中宏娄,我采取了兩種不同的方式獲取WindowManager對(duì)象。一種是直接調(diào)用Activity中的getSystemService方法逮壁,另外一種是通過調(diào)用Application Context的getSystemService方法孵坚。
通過實(shí)踐可以得知:
用Application Context獲取的WindowManager實(shí)例來創(chuàng)建Window并不隨Activity的消失而消失,只有當(dāng)進(jìn)程被殺的時(shí)候才消失。
用Activity Context獲取的WindowManager實(shí)例來創(chuàng)建Window隨Activity的消失而消失十饥。
另外窟勃,上面的動(dòng)態(tài)圖演示的時(shí)候,當(dāng)按下返回鍵的時(shí)候逗堵,應(yīng)用程序打印如下錯(cuò)誤秉氧。
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)
這個(gè)報(bào)錯(cuò)我們應(yīng)該不陌生--“窗體泄露”。特別是使用Dialog的時(shí)候蜒秤,如果Dialog未dismiss汁咏,某種情況下就會(huì)導(dǎo)致窗體泄露,所以Android提供了DialogFragment來解決此類問題作媚。
通過以上演示攘滩,我們可以得出以下問題:
1、為什么獲取WindowManager的Context類型不同纸泡,導(dǎo)致Window的生命周期不同漂问。
2、什么情況下發(fā)生窗體泄露女揭。
看源碼分析問題
1.Application Context的getSystemService方法
Application Context調(diào)用的getSystemService方法是Context抽象類中的一個(gè)抽象方法蚤假,該方法的實(shí)現(xiàn)是在ContextImpl類中。
@Override
public Object getSystemService(String name) {
return SystemServiceRegistry.getSystemService(this, name);
}
在SystemServiceRegistry類中吧兔,找到注冊(cè)WINDOW_SERVICE方法如下磷仰。
registerService(Context.WINDOW_SERVICE, WindowManager.class,
new CachedServiceFetcher<WindowManager>() {
@Override
public WindowManager createService(ContextImpl ctx) {
return new WindowManagerImpl(ctx.getDisplay());
}});
從此,我們可以看到創(chuàng)建WindowManager對(duì)象境蔼,最后是調(diào)用WindowManagerImpl(Display display)構(gòu)造方法創(chuàng)建灶平。暫且分析到這,讀者有興趣的話可以追蹤源碼來分析箍土。
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,當(dāng)傳入的參數(shù)ServiceName等于WINDOW_SERVICE涮帘,直接返回mWindowManager對(duì)象拼苍。接著,分析mWindowManager是如何創(chuàng)建调缨。
在Activity的attach方法疮鲫,創(chuàng)建了mWindowManager實(shí)例。
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的實(shí)例)的setWindowManager方法中創(chuàng)建弦叶。接著來看下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方法中有個(gè)參數(shù)也是WindowManager對(duì)象(該對(duì)象就是ContextImpl中g(shù)etSystemService獲取到的),然后調(diào)用該對(duì)象的createLocalWindowManager方法創(chuàng)建Activity的mWindowManager實(shí)例伤哺。
public WindowManagerImpl createLocalWindowManager(Window parentWindow) {
return new WindowManagerImpl(mDisplay, parentWindow);
}
到此就明朗燕侠,通過調(diào)用WindowManagerImpl(Display,Window)的構(gòu)造方法創(chuàng)建mWindowManager對(duì)象者祖。
所以我們可得出Application Context和Activity Context創(chuàng)建的WindowManager對(duì)象是通過WindowManagerImpl不同的構(gòu)造方法來創(chuàng)建的。
public WindowManagerImpl(Display display) {
this(display, null);
}
private WindowManagerImpl(Display display, Window parentWindow) {
mDisplay = display;
mParentWindow = parentWindow;
}
Application Context創(chuàng)建的WindowManagerImpl對(duì)象是沒有父窗口(parentWindow)绢彤。
Activity Context創(chuàng)建的WindowManagerImpl對(duì)象會(huì)將Activity的mWindow作為父窗口七问。
通過分析和前面的演示,可以得出結(jié)論茫舶。
1.如果某個(gè)Window有parentWindow械巡,當(dāng)parentWindow remove之后,子窗口也會(huì)被remove饶氏。
2.若有子Window未remove讥耗,則會(huì)出現(xiàn)窗體泄露錯(cuò)誤信息。
有結(jié)論了疹启,接下來就驗(yàn)證下古程。
對(duì)Android源碼搜索報(bào)錯(cuò)關(guān)鍵句“that was originally added here”,發(fā)現(xiàn)在WinowManagerGlobal的closeAll方法出現(xiàn)該語句喊崖。其實(shí)從該方法的名字就可以猜出來挣磨,關(guān)閉所有窗口。
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在哪調(diào)用趋急。繼續(xù)搜索,發(fā)現(xiàn)在ActivityThread的handleDestroyActivity方法中調(diào)用了closeAll方法势誊。
private void handleDestroyActivity(IBinder token, boolean finishing,
int configChanges, boolean getNonConfigInstance) {
ActivityClientRecord r = performDestroyActivity(token, finishing,
configChanges, getNonConfigInstance);
//......
if (r.mPendingRemoveWindow == null) {
//讀者可以仔細(xì)體會(huì)此段英文
// 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時(shí)粟耻,會(huì)去檢查是否有窗體泄露。
這里我們要注意眉踱,窗體泄露只會(huì)在堆棧中打印錯(cuò)誤信息挤忙,不會(huì)導(dǎo)致應(yīng)用程序崩潰。
從Activity到Window
Activity使我們開發(fā)過程中最經(jīng)常打交道的組件谈喳,因此從Activity和Window的關(guān)系册烈,可以更好的幫助我們理解Window是啥玩意。
創(chuàng)建頁面布局婿禽,我們都是通過調(diào)用Activity的setContentView方法
public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}
從這里赏僧,我們可以知道,最終是調(diào)用Window的setContentView方法扭倾,該方法的實(shí)現(xiàn)是在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();
}
}
從該代碼中,大概做了如下操作:
- installDecor.
1).generateDecor
2).generateLayout - inflate your layout.
DecorView是一個(gè)自定義FrameLayout膛壹,它是作為Activity中Window的頂級(jí)View驾中,比如狀態(tài)欄唉堪,標(biāo)題欄等。因?yàn)槊總€(gè)非全屏Activity都顯示狀態(tài)欄肩民,所以系統(tǒng)把某些共性的View都抽象出來唠亚,避免我們做重復(fù)工作。如果各位想要更深入了解DecorView持痰,可以結(jié)合網(wǎng)上資料進(jìn)行理解趾撵。generateDecor的過程就是創(chuàng)建DecorView的過程。
每個(gè)Activity都是有自己資源ID形式的布局共啃。在填充資源layout時(shí)候占调,會(huì)根據(jù)不同的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優(yōu)化實(shí)踐有介紹,ViewStub用于懶加載actionBar纵苛,而id為@android:id/content的FrameLayout剿涮,此FrameLayout就是contentView。我們?cè)贏ctivity中調(diào)用setContentView方法攻人,設(shè)置布局取试,最終就是添加到該FrameLayout中。
從上圖中怀吻,我們很清楚的看到整個(gè)View的層次瞬浓。DecorView最為Activity Window中的頂級(jí)View。
分析到這里蓬坡,整個(gè)Activity需要顯示的View創(chuàng)建好了猿棉,那么如何顯示?
在Activity中屑咳,我并未找到顯示DecorView的方法萨赁。根據(jù)閱讀源碼經(jīng)驗(yàn),應(yīng)該會(huì)在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添加到系統(tǒng)中杖爽。
創(chuàng)建Window的實(shí)質(zhì)
從以上的分析,我們可以得出創(chuàng)建Window的過程其實(shí)就是添加View的過程紫皇。比如Activity中decorView是作為頂級(jí)View添加到系統(tǒng)中去顯示慰安,頂級(jí)View的LayoutParams必須是WindowManager.LayoutParams類型,否則會(huì)報(bào)錯(cuò)坝橡。那么為什么會(huì)有Window的存在泻帮,我們直接用添加View形式,創(chuàng)建Activity所需顯示的界面不久就了计寇。
這里就又回到最初Window的定義锣杂,Window它定義了頂級(jí)窗體樣式和行為脂倦。因?yàn)槊總€(gè)Activity都會(huì)標(biāo)題欄,狀態(tài)欄等元莫,在Window中它提供了DecorView的創(chuàng)建赖阻,省去我們不少麻煩,通過Window提供API踱蠢,我們可以很方面改變標(biāo)題欄火欧,狀態(tài)欄的樣式。同時(shí)Window也提供某些共性操作的行為茎截,比如返回鍵操作苇侵、觸摸事件傳遞,menu顯示與因此等企锌。Window最核心的內(nèi)容還是它提供的頂級(jí)View--DecorView及其相關(guān)操作榆浓。
最后
Window在我們開發(fā)過程中起著至關(guān)重要的地位。因此撕攒,深入了解Window對(duì)我們解決一系列UI問題大有裨益陡鹃。你深入了解Window后就會(huì)知道:
1.為什么Dialog的創(chuàng)建必須要用Activity Context。
2.為什么PopupWindow的生命周期與Activity綁定抖坪。
3.為什么我創(chuàng)建的Window不能顯示萍鲸、不能點(diǎn)擊等等。
如果覺得對(duì)你有幫助擦俐,請(qǐng)不要吝惜您的喜歡脊阴!謝謝!