作者:ScottStone
鏈接:http://www.lxweimin.com/p/1090d6c33dec
通過上面的分析可以看出,View是Android中的視圖呈現方式,但是View并不能單獨的存在,需要依附在Window這個抽象的概念上,也就是說有界面的地方就有Window,線面我們就通過Activity、Dialog跟Toast來深入的了解下Window的創建過程到底是怎樣的。
1. Activity中Window的創建過程
在介紹Activity中的Window的創建過程之前,我們先得了解下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的啟動過程很復雜,最終是有ActivityThread中的performLaunchActivity方法來完成的,看上圖源碼可以看出performLaunchActivity是通過類加載器獲得Activity的實例的。然后調動Activity的attach方法為其關聯運行過程中所依賴的一系列上下文環境變量。
在Activity的attach方法里,
- 系統會創建Activity所屬的Window對象并為其設置回調接口,這里Window對象實際上是PhoneWindow。
- 給Activity初始化各種參數,如mUiThread等
- 給PhoneWindow設置WindowManager,實際上設置的是WindowManagerImpl:
下圖給出一部分源碼,有興趣的同學還是直接看源碼。
......
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實現了Window的Callback接口,因此當Window接收到外界的狀態改變時就會回調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已經創建完成了,但是像之前文章說過的一樣,只有Window其實只是一個空的架子,還需要View才能真正是出現視圖。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()返回的實際上是上面創建的PhoneWindow,也就是它會調用PhoneWindow的setContentView,在該方法中會創建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,則需要創建,否則移除其中的mContentParent中所有的View。
- 將View添加到DecorView的mContentParent中。
- 回調Activity的onContentChanged方法通知Activity視圖已經發生改變。
經過上面幾個步驟,DecorView就創建完并初始化成功了。Activity的布局文件也已經成功添加到了DecorView的mContentParent中,但是這個時候DecorView還沒有被WindowManager正式添加到Window中。這里需要正確理解Window的概念,Window更多表示的是一種抽象的功能集合,雖然說早在Activity的attach方法中Window就已經被創建了,但是這個時候由于DecorView并沒有被WindowManager識別,所以這個時候的Window無法提供具體功能,因為它還無法接收外界的輸入信息。在ActivityThread的handleResumeActivity方法中,首先會調用Activity的onResume方法,接著會調用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的創建過程
Dialog的Window的創建過程跟Activity的很相似,大體有以下幾個步驟。
-1. 創建Window
Dialog的Window的創建同樣是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創建過程跟Activity的很類似,幾乎沒有多少區別。當Dialog關閉時,會通過WindowManager來移除DecorView。
普通的Dialog有個不同之處,就是必須要使用Activity的Context,如果使用Application的Context會報錯。這個地方是因為普通的Dialog需要token,而token一般是Activity才會有,這個時候如果一定要用Application的Context,需要Dialog是系統的Window才行,這就需要一開始設置Window的type,一般選擇TYPE_SYSTEM_OVERLAY指定Window的類型為系統Window。
3 Toast的Window創建過程
Toast和Dialog不同,它的工作過程就稍顯復雜。首先Toast也是基于Window來實現的,但是由于Toast具有定時取消這一功能,所以系統采用了Handler。在Toast的內部有兩類IPC過程,第一類是Toast訪問NotificationManagerService,第二類是Notification-ManagerService回調Toast里的TN接口。關于IPC的一些知識,可以移步Android中的IPC方式。為了便于描述,下面將NotificationManagerService簡稱為NMS。
Toast屬于系統Window,它內部的視圖由兩種方式指定,一種是系統默認的樣式,另一種是通過setView方法來指定一個自定義View,不管如何,它們都對應Toast的一個View類型的內部成員mNextView。Toast提供了show和cancel分別用于顯示和隱藏Toast,它們的內部是一個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來實現,由于NMS運行在系統的進程中,所以只能通過遠程調用的方式來顯示和隱藏Toast。需要注意的是TN這個類,它是一個Binder類,在Toast和NMS進行IPC的過程中,當NMS處理Toast的顯示或隱藏請求時會跨進程回調TN中的方法,這個時候由于TN運行在Binder線程池中,所以需要通過Handler將其切換到當前線程中。這里的當前線程是指發送Toast請求所在的線程。注意,由于這里使用了Handler,所以這意味著Toast無法在沒有Looper的線程中彈出,這是因為Handler需要使用Looper才能完成切換線程的功能.
從上面源碼show方法我們可以看到,Toast的顯示調用了NMS的enqueueToast方法。enqueueToast方法有三個參數,分別是:pkg當前應用包名、tn遠程回調和mDuration顯示時長。
enqueueToast首先將Toast請求封裝為ToastRecord對象并將其添加到一個名為mToastQueue的隊列中。mToastQueue其實是一個ArrayList。對于非系統應用來說,mToastQueue中最多能同時存在50個ToastRecord,這樣做是為了防止DOS(DenialofService)。如果不這么做,試想一下,如果我們通過大量的循環去連續彈出Toast,這將會導致其他應用沒有機會彈出Toast,那么對于其他應用的Toast請求,系統的行為就是拒絕服務,這就是拒絕服務攻擊的含義,這種手段常用于網絡攻擊中。
// 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();
}
正常情況下,一個應用不可能達到上限,當ToastRecord被添加到mToastQueue中后,NMS就會通過showNextToastLocked方法來顯示當前的Toast。下面的代碼很好理解,需要注意的是,Toast的顯示是由ToastRecord的callback來完成的,這個callback實際上就是Toast中的TN對象的遠程Binder,通過callback來訪問TN中的方法是需要跨進程來完成的,最終被調用的TN中的方法會運行在發起Toast請求的應用的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來發送一個延時消息,時長當然是根據一開始設置的時間。具體看下代碼:
@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又調用了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();
}
}
經過上面的分析,我們了解到Toast的顯示和隱藏過程實際上是通過Toast中的TN這個類來實現的,它有兩個方法show和hide,分別對應Toast的顯示和隱藏。由于這兩個方法是被NMS以跨進程的方式調用的,因此它們運行在Binder線程池中。為了將執行環境切換到Toast請求所在的線程,在它們的內部使用了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,它們內部分別調用了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添加了進去。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創建就介紹完了,相信大家看后應該有了新的理解。
當然還有很多其他的通過Window實現的組件,諸如PopWindow、菜單欄和狀態欄能,這里不再一一介紹了,還是那句話,看源碼,里面的注釋寫的也是比較詳細的。