作者:ScottStone
鏈接:http://www.reibang.com/p/1090d6c33dec
通過上面的分析可以看出,View是Android中的視圖呈現(xiàn)方式铝阐,但是View并不能單獨的存在址貌,需要依附在Window這個抽象的概念上,也就是說有界面的地方就有Window徘键,線面我們就通過Activity练对、Dialog跟Toast來深入的了解下Window的創(chuàng)建過程到底是怎樣的。
1. Activity中Window的創(chuàng)建過程
在介紹Activity中的Window的創(chuàng)建過程之前吹害,我們先得了解下Activity的啟動過程螟凭,后面會專門的寫文章介紹Activity的啟動過程,這里先簡單介紹下它呀,還是先上源碼:
......
Activity activity = null;
try {
java.lang.ClassLoader cl = appContext.getClassLoader();
activity = mInstrumentation.newActivity(
cl, component.getClassName(), r.intent);
StrictMode.incrementExpectedActivityCount(activity.getClass());
r.intent.setExtrasClassLoader(cl);
r.intent.prepareToEnterProcess();
if (r.state != null) {
r.state.setClassLoader(cl);
}
} catch (Exception e) {
if (!mInstrumentation.onException(activity, e)) {
throw new RuntimeException(
"Unable to instantiate activity " + component
+ ": " + e.toString(), e);
}
}
......
Window window = null;
if (r.mPendingRemoveWindow != null && r.mPreserveWindow) {
window = r.mPendingRemoveWindow;
r.mPendingRemoveWindow = null;
r.mPendingRemoveWindowManager = null;
}
appContext.setOuterContext(activity);
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 (customIntent != null) {
activity.mIntent = customIntent;
}
......
Activity的啟動過程很復(fù)雜螺男,最終是有ActivityThread中的performLaunchActivity方法來完成的棒厘,看上圖源碼可以看出performLaunchActivity是通過類加載器獲得Activity的實例的。然后調(diào)動Activity的attach方法為其關(guān)聯(lián)運行過程中所依賴的一系列上下文環(huán)境變量下隧。
在Activity的attach方法里奢人,
- 系統(tǒng)會創(chuàng)建Activity所屬的Window對象并為其設(shè)置回調(diào)接口,這里Window對象實際上是PhoneWindow淆院。
- 給Activity初始化各種參數(shù)何乎,如mUiThread等
- 給PhoneWindow設(shè)置WindowManager,實際上設(shè)置的是WindowManagerImpl:
下圖給出一部分源碼土辩,有興趣的同學(xué)還是直接看源碼宪赶。
......
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();
mMainThread = aThread;
mInstrumentation = instr;
mToken = token;
mIdent = ident;
mApplication = application;
mIntent = intent;
mReferrer = referrer;
mComponent = intent.getComponent();
mActivityInfo = info;
mTitle = title;
mParent = parent;
mEmbeddedID = id;
mLastNonConfigurationInstances = lastNonConfigurationInstances;
if (voiceInteractor != null) {
if (lastNonConfigurationInstances != null) {
mVoiceInteractor = lastNonConfigurationInstances.voiceInteractor;
} else {
mVoiceInteractor = new VoiceInteractor(voiceInteractor, this, this,
Looper.myLooper());
}
}
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);
......
由于Activity實現(xiàn)了Window的Callback接口,因此當(dāng)Window接收到外界的狀態(tài)改變時就會回調(diào)Activity的方法脯燃。Callback接口中的方法很多搂妻,但是有幾個卻是我們都非常熟悉的,比如onAttachedToWindow辕棚、onDetachedFromWindow欲主、dispatchTouchEvent,等等逝嚎。
public interface Callback {
public boolean dispatchKeyEvent(KeyEvent event);
public boolean dispatchKeyShortcutEvent(KeyEvent event);
public boolean dispatchTouchEvent(MotionEvent event);
public boolean dispatchTrackballEvent(MotionEvent event);
public boolean dispatchGenericMotionEvent(MotionEvent event);
......
到這里Window已經(jīng)創(chuàng)建完成了扁瓢,但是像之前文章說過的一樣,只有Window其實只是一個空的架子补君,還需要View才能真正是出現(xiàn)視圖引几。Activity的視圖是怎么加到Window中的呢?這里就得說道一個我們很熟悉的方法setContentView挽铁。
......
/**
* Set the activity content from a layout resource. The resource will be
* inflated, adding all top-level views to the activity.
* @param layoutResID Resource ID to be inflated.
* @see #setContentView(android.view.View)
* @see #setContentView(android.view.View, android.view.ViewGroup.LayoutParams)
*/
public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}
......
從Activity的setContentView方法我們可以清楚的看到伟桅,getWindow()返回的實際上是上面創(chuàng)建的PhoneWindow,也就是它會調(diào)用PhoneWindow的setContentView叽掘,在該方法中會創(chuàng)建DecorView并完成布局視圖的填充楣铁。下面我們看下PhoneWindow的setContentView的源碼。
@Override
public void setContentView(View view, ViewGroup.LayoutParams params) {
// 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)) {
view.setLayoutParams(params);
final Scene newScene = new Scene(mContentParent, view);
transitionTo(newScene);
} else {
mContentParent.addView(view, params);
}
mContentParent.requestApplyInsets();
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
mContentParentExplicitlySet = true;
}
通過上面的源碼我們能清楚的看到大概分為以幾個步驟:
- 如果沒有DecorView更扁,則需要創(chuàng)建盖腕,否則移除其中的mContentParent中所有的View。
- 將View添加到DecorView的mContentParent中浓镜。
- 回調(diào)Activity的onContentChanged方法通知Activity視圖已經(jīng)發(fā)生改變溃列。
經(jīng)過上面幾個步驟,DecorView就創(chuàng)建完并初始化成功了膛薛。Activity的布局文件也已經(jīng)成功添加到了DecorView的mContentParent中听隐,但是這個時候DecorView還沒有被WindowManager正式添加到Window中。這里需要正確理解Window的概念相叁,Window更多表示的是一種抽象的功能集合遵绰,雖然說早在Activity的attach方法中Window就已經(jīng)被創(chuàng)建了辽幌,但是這個時候由于DecorView并沒有被WindowManager識別,所以這個時候的Window無法提供具體功能椿访,因為它還無法接收外界的輸入信息乌企。在ActivityThread的handleResumeActivity方法中,首先會調(diào)用Activity的onResume方法成玫,接著會調(diào)用Activity的makeVisible()加酵,正是在makeVisible方法中,DecorView真正地完成了添加和顯示這兩個過程哭当,到這里Activity的視圖才能被用戶看到猪腕。
void makeVisible() {
if (!mWindowAdded) {
ViewManager wm = getWindowManager();
wm.addView(mDecor, getWindow().getAttributes());
mWindowAdded = true;
}
mDecor.setVisibility(View.VISIBLE);
}
2. Dialog中的Window的創(chuàng)建過程
Dialog的Window的創(chuàng)建過程跟Activity的很相似,大體有以下幾個步驟钦勘。
-1. 創(chuàng)建Window
Dialog的Window的創(chuàng)建同樣是PhoneWindow陋葡,這個剩下的跟Activity還是很類似的。具體看下下面的源碼彻采。
Dialog(@NonNull Context context, @StyleRes int themeResId, boolean createContextThemeWrapper) {
if (createContextThemeWrapper) {
if (themeResId == ResourceId.ID_NULL) {
final TypedValue outValue = new TypedValue();
context.getTheme().resolveAttribute(R.attr.dialogTheme, outValue, true);
themeResId = outValue.resourceId;
}
mContext = new ContextThemeWrapper(context, themeResId);
} else {
mContext = context;
}
mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
final Window w = new PhoneWindow(mContext);
mWindow = w;
w.setCallback(this);
w.setOnWindowDismissedCallback(this);
w.setOnWindowSwipeDismissedCallback(() -> {
if (mCancelable) {
cancel();
}
});
w.setWindowManager(mWindowManager, null, null);
w.setGravity(Gravity.CENTER);
mListenersHandler = new ListenersHandler(this);
}
-2. 初始化DecorView并將Dialog的界面添加到DecorView中
這個過程跟Activity也是類似的腐缤,也是通過Window去添加指定的布局。
/**
* Set the screen content from a layout resource. The resource will be
* inflated, adding all top-level views to the screen.
* @param layoutResID Resource ID to be inflated.
*/
public void setContentView(@LayoutRes int layoutResID) {
mWindow.setContentView(layoutResID);
}
-3. 將DecorView添加到Window中并顯示
Dialog的show方法中肛响,會通過WindowManager將DecorView添加到Window中岭粤,源碼如下
......
mDecor = mWindow.getDecorView();
......
mWindowManager.addView(mDecor, l);
mShowing = true;
......
其實從上面的三個步驟能看出,Dialog的Window創(chuàng)建過程跟Activity的很類似特笋,幾乎沒有多少區(qū)別剃浇。當(dāng)Dialog關(guān)閉時,會通過WindowManager來移除DecorView猎物。
普通的Dialog有個不同之處虎囚,就是必須要使用Activity的Context,如果使用Application的Context會報錯霸奕。這個地方是因為普通的Dialog需要token溜宽,而token一般是Activity才會有,這個時候如果一定要用Application的Context质帅,需要Dialog是系統(tǒng)的Window才行,這就需要一開始設(shè)置Window的type留攒,一般選擇TYPE_SYSTEM_OVERLAY指定Window的類型為系統(tǒng)Window煤惩。
3 Toast的Window創(chuàng)建過程
Toast和Dialog不同,它的工作過程就稍顯復(fù)雜炼邀。首先Toast也是基于Window來實現(xiàn)的魄揉,但是由于Toast具有定時取消這一功能,所以系統(tǒng)采用了Handler拭宁。在Toast的內(nèi)部有兩類IPC過程洛退,第一類是Toast訪問NotificationManagerService瓣俯,第二類是Notification-ManagerService回調(diào)Toast里的TN接口。關(guān)于IPC的一些知識兵怯,可以移步Android中的IPC方式彩匕。為了便于描述,下面將NotificationManagerService簡稱為NMS媒区。
Toast屬于系統(tǒng)Window驼仪,它內(nèi)部的視圖由兩種方式指定,一種是系統(tǒng)默認(rèn)的樣式袜漩,另一種是通過setView方法來指定一個自定義View绪爸,不管如何,它們都對應(yīng)Toast的一個View類型的內(nèi)部成員mNextView宙攻。Toast提供了show和cancel分別用于顯示和隱藏Toast奠货,它們的內(nèi)部是一個IPC過程,下面我們看下show方法跟cancel方法座掘。
/**
* Show the view for the specified duration.
*/
public void show() {
if (mNextView == null) {
throw new RuntimeException("setView must have been called");
}
INotificationManager service = getService();
String pkg = mContext.getOpPackageName();
TN tn = mTN;
tn.mNextView = mNextView;
try {
service.enqueueToast(pkg, tn, mDuration);
} catch (RemoteException e) {
// Empty
}
}
/**
* Close the view if it's showing, or don't show it if it isn't showing yet.
* You do not normally have to call this. Normally view will disappear on its own
* after the appropriate duration.
*/
public void cancel() {
mTN.cancel();
}
從上面的代碼可以看到仇味,顯示和隱藏Toast都需要通過NMS來實現(xiàn),由于NMS運行在系統(tǒng)的進(jìn)程中雹顺,所以只能通過遠(yuǎn)程調(diào)用的方式來顯示和隱藏Toast丹墨。需要注意的是TN這個類,它是一個Binder類嬉愧,在Toast和NMS進(jìn)行IPC的過程中贩挣,當(dāng)NMS處理Toast的顯示或隱藏請求時會跨進(jìn)程回調(diào)TN中的方法,這個時候由于TN運行在Binder線程池中没酣,所以需要通過Handler將其切換到當(dāng)前線程中忱详。這里的當(dāng)前線程是指發(fā)送Toast請求所在的線程。注意济锄,由于這里使用了Handler催训,所以這意味著Toast無法在沒有Looper的線程中彈出,這是因為Handler需要使用Looper才能完成切換線程的功能.
從上面源碼show方法我們可以看到偿衰,Toast的顯示調(diào)用了NMS的enqueueToast方法挂疆。enqueueToast方法有三個參數(shù),分別是:pkg當(dāng)前應(yīng)用包名下翎、tn遠(yuǎn)程回調(diào)和mDuration顯示時長缤言。
enqueueToast首先將Toast請求封裝為ToastRecord對象并將其添加到一個名為mToastQueue的隊列中。mToastQueue其實是一個ArrayList视事。對于非系統(tǒng)應(yīng)用來說胆萧,mToastQueue中最多能同時存在50個ToastRecord,這樣做是為了防止DOS(DenialofService)俐东。如果不這么做跌穗,試想一下订晌,如果我們通過大量的循環(huán)去連續(xù)彈出Toast,這將會導(dǎo)致其他應(yīng)用沒有機(jī)會彈出Toast蚌吸,那么對于其他應(yīng)用的Toast請求推励,系統(tǒng)的行為就是拒絕服務(wù)跌造,這就是拒絕服務(wù)攻擊的含義寝杖,這種手段常用于網(wǎng)絡(luò)攻擊中磕蒲。
// Limit the number of toasts that any given package except the android
// package can enqueue. Prevents DOS attacks and deals with leaks.
if (!isSystemToast) {
int count = 0;
final int N = mToastQueue.size();
for (int i=0; i<N; i++) {
final ToastRecord r = mToastQueue.get(i);
if (r.pkg.equals(pkg)) {
count++;
if (count >= MAX_PACKAGE_NOTIFICATIONS) {
Slog.e(TAG, "Package has already posted " + count
+ " toasts. Not showing more. Package=" + pkg);
return;
}
}
}
}
// If it's at index 0, it's the current toast. It doesn't matter if it's
// new or just been updated. Call back and tell it to show itself.
// If the callback fails, this will remove it from the list, so don't
// assume that it's valid after this.
if (index == 0) {
showNextToastLocked();
}
正常情況下许起,一個應(yīng)用不可能達(dá)到上限,當(dāng)ToastRecord被添加到mToastQueue中后伦乔,NMS就會通過showNextToastLocked方法來顯示當(dāng)前的Toast招刹。下面的代碼很好理解,需要注意的是越锈,Toast的顯示是由ToastRecord的callback來完成的,這個callback實際上就是Toast中的TN對象的遠(yuǎn)程Binder膘滨,通過callback來訪問TN中的方法是需要跨進(jìn)程來完成的甘凭,最終被調(diào)用的TN中的方法會運行在發(fā)起Toast請求的應(yīng)用的Binder線程池中。
@GuardedBy("mToastQueue")
void showNextToastLocked() {
ToastRecord record = mToastQueue.get(0);
while (record != null) {
if (DBG) Slog.d(TAG, "Show pkg=" + record.pkg + " callback=" + record.callback);
try {
record.callback.show(record.token);
scheduleTimeoutLocked(record);
return;
} catch (RemoteException e) {
Slog.w(TAG, "Object died trying to show notification " + record.callback
+ " in package " + record.pkg);
// remove it from the list and let the process die
int index = mToastQueue.indexOf(record);
if (index >= 0) {
mToastQueue.remove(index);
}
keepProcessAliveIfNeededLocked(record.pid);
if (mToastQueue.size() > 0) {
record = mToastQueue.get(0);
} else {
record = null;
}
}
}
}
從上面的源碼可以看到火邓,Toast顯示之后丹弱,通過scheduleTimeoutLocked來發(fā)送一個延時消息,時長當(dāng)然是根據(jù)一開始設(shè)置的時間贡翘。具體看下代碼:
@GuardedBy("mToastQueue")
private void scheduleTimeoutLocked(ToastRecord r)
{
mHandler.removeCallbacksAndMessages(r);
Message m = Message.obtain(mHandler, MESSAGE_TIMEOUT, r);
long delay = r.duration == Toast.LENGTH_LONG ? LONG_DELAY : SHORT_DELAY;
mHandler.sendMessageDelayed(m, delay);
}
上面LONG_DELAY是3.5s蹈矮,SHORT_DELAY是2s。延時過后鸣驱,NMS會通過cancelToastLocked來隱藏Toast并從mToastQueue中移除泛鸟,我們看下源碼就能清楚的了解這個過程,下面是cancelToastLocked方法踊东,可以看到移除Toast之后如果mToastQueue有Toast又調(diào)用了showNextToastLocked方法北滥。
@GuardedBy("mToastQueue")
void cancelToastLocked(int index) {
ToastRecord record = mToastQueue.get(index);
try {
record.callback.hide();
} catch (RemoteException e) {
Slog.w(TAG, "Object died trying to hide notification " + record.callback
+ " in package " + record.pkg);
// don't worry about this, we're about to remove it from
// the list anyway
}
ToastRecord lastToast = mToastQueue.remove(index);
mWindowManagerInternal.removeWindowToken(lastToast.token, true, DEFAULT_DISPLAY);
keepProcessAliveIfNeededLocked(record.pid);
if (mToastQueue.size() > 0) {
// Show the next one. If the callback fails, this will remove
// it from the list, so don't assume that the list hasn't changed
// after this point.
showNextToastLocked();
}
}
經(jīng)過上面的分析,我們了解到Toast的顯示和隱藏過程實際上是通過Toast中的TN這個類來實現(xiàn)的闸翅,它有兩個方法show和hide再芋,分別對應(yīng)Toast的顯示和隱藏。由于這兩個方法是被NMS以跨進(jìn)程的方式調(diào)用的坚冀,因此它們運行在Binder線程池中济赎。為了將執(zhí)行環(huán)境切換到Toast請求所在的線程,在它們的內(nèi)部使用了Handler,具體看下源碼:
......
mHandler = new Handler(looper, null) {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case SHOW: {
IBinder token = (IBinder) msg.obj;
handleShow(token);
break;
}
case HIDE: {
handleHide();
// Don't do this in handleHide() because it is also invoked by
// handleShow()
mNextView = null;
break;
}
case CANCEL: {
handleHide();
// Don't do this in handleHide() because it is also invoked by
// handleShow()
mNextView = null;
try {
getService().cancelToast(mPackageName, TN.this);
} catch (RemoteException e) {
}
break;
}
}
}
};
......
/**
* schedule handleShow into the right thread
*/
@Override
public void show(IBinder windowToken) {
if (localLOGV) Log.v(TAG, "SHOW: " + this);
mHandler.obtainMessage(SHOW, windowToken).sendToTarget();
}
/**
* schedule handleHide into the right thread
*/
@Override
public void hide() {
if (localLOGV) Log.v(TAG, "HIDE: " + this);
mHandler.obtainMessage(HIDE).sendToTarget();
}
上述代碼中司训,mShow和mHide是兩個Runnable构捡,它們內(nèi)部分別調(diào)用了handleShow和handleHide方法。由此可見壳猜,handleShow和handleHide才是真正完成顯示和隱藏Toast的地方勾徽。TN的handleShow中會將Toast的視圖添加到Window中。代碼如下统扳。
......
// Since the notification manager service cancels the token right
// after it notifies us to cancel the toast there is an inherent
// race and we may attempt to add a window after the token has been
// invalidated. Let us hedge against that.
try {
mWM.addView(mView, mParams);
trySendAccessibilityEvent();
} catch (WindowManager.BadTokenException e) {
/* ignore */
}
......
上面的handleShow代碼段喘帚,我們能清楚的看到,mWM將Toast添加了進(jìn)去咒钟。handleHide的源碼如下:
public void handleHide() {
if (localLOGV) Log.v(TAG, "HANDLE HIDE: " + this + " mView=" + mView);
if (mView != null) {
// note: checking parent() just to make sure the view has
// been added... i have seen cases where we get here when
// the view isn't yet added, so let's try not to crash.
if (mView.getParent() != null) {
if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);
mWM.removeViewImmediate(mView);
}
mView = null;
}
}
到這里Toast的Window創(chuàng)建就介紹完了吹由,相信大家看后應(yīng)該有了新的理解。
當(dāng)然還有很多其他的通過Window實現(xiàn)的組件盯腌,諸如PopWindow溉知、菜單欄和狀態(tài)欄能,這里不再一一介紹了腕够,還是那句話级乍,看源碼,里面的注釋寫的也是比較詳細(xì)的帚湘。