認識Android中Window(三) 之 Window的建立過程
前面文章有提到,Android中所有的View呈現都是通過Window做到的,像懸浮窗、Activity、Dialog、Toast都是通過Window來呈現的。因為View不能單獨存在,它必須附著在Window這個抽象的概念上面,所以有View的地方就有Window。
Activity 的 Window 建立過程
我們在文章《Android應用程式啟動詳解(一)》中有分析過Activity從startActivity()方法後的啟動過程有介紹過ActivityThread.performLaunchActivity()方法。在這個方法內部做了3件大事,第1是通過反射機制建立了 Activity 的例項物件,第3是onCreate的回撥,而第2是呼叫Acitivty.attach 方法為Activity初始化關聯的一系列屬性。Window 建立就在這裡發生的,系統會建立 Activity 所屬的 Window 物件併為其設定回撥介面,請看原始碼:
Activity.java
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) { attachBaseContext(context); mFragments.attachActivity(this, mContainer, null); // 關鍵程式碼 mWindow = PolicyManager.makeNewWindow(this); mWindow.setCallback(this); mWindow.setOnWindowDismissedCallback(this); mWindow.getLayoutInflater().setPrivateFactory(this); if (info.softInputMode != WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED) { mWindow.setSoftInputMode(info.softInputMode); } …… 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; }
可以看一以,Window物件的建立是通過PolicyManager.makeNewWindow()方法來實現。在建立完成後,設定了回撥setCallback並傳入this,所以當Window接到外界的狀態改變時就會回撥Activity方法。我們重點關注一下PolicyManager.makeNewWindow(),從名字可以看出PolicyManager是一個策略類,它實現了幾個工廠方法全部的策略介面在IPolicy中宣告,真正實現的類是Policy,請看它們的程式碼:
IPolicy.java
public interface IPolicy { Window makeNewWindow(Context var1); LayoutInflater makeNewLayoutInflater(Context var1); WindowManagerPolicy makeNewWindowManager(); FallbackEventHandler makeNewFallbackEventHandler(Context var1); }
Policy.java
public class Policy implements IPolicy {
⋯⋯
public Window makeNewWindow(Context context) {
return new PhoneWindow(context);
}
⋯⋯
}
這裡就能說明,Window的具體且唯一實現的確是PhoneWindow。Window建立完成後就要來看看Activity 的View是如何附屬到 Window 上的,而 Activity 的View由 setContentView() 提供,所以從 setContentView 入手,它的原始碼如下:
PhoneWindow.java
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) {
// 關鍵程式碼1,如果沒有 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 {
// 關鍵程式碼2:將 View 新增到 DecorView 的 mContentParent 中
mLayoutInflater.inflate(layoutResID, mContentParent);
}
// 關鍵程式碼3:回撥 Activity 的 onContentChanged 方法通知 Activity View已經發生改變
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
}
方法中,重點做了3件事情,關鍵程式碼1中,判斷mContentParent就否存在,mContentParent就是DecorView的內容部分。。如果不存在就建立DecorView;關鍵程式碼2中,直接將 Activity 的View新增到 DecorView 的 mContentParent,由此可以理解 Activity 的 setContentView 這個方法名的來歷,為什麼不叫 setView 呢?因為 Activity 的佈局檔案只是被新增到 DecorView 的 mContentParent 中,因此叫 setContentView;關鍵程式碼3中,就是處理Activity 的onContentChanged回撥,因為從前面的setCallback()知道傳入的是Activity,所以這裡的getCallback()返回的就是Activity。
到目前為止,DecorView 已經被建立並初始化完畢,Activity 的佈局檔案也已經成功新增到了 DecorView 的 mContentParent 中,但是DecorView 還沒有被 WindowManager 正式新增到 Window 中。在 ActivityThread 的 handleResumeActivity 方法中,首先會呼叫Acitivy 的 onResume 方法,接著會呼叫 Acitivy 的 makeVisible() 方法,正是在 makeVisible 方法中,DecorView 才真正的完成了顯示過程,到這裡 Activity 的檢視才能被使用者看到,如下:
Activity.java
void makeVisible() {
if (!mWindowAdded) {
ViewManager wm = getWindowManager();
wm.addView(mDecor, getWindow().getAttributes());
mWindowAdded = true;
}
mDecor.setVisibility(View.VISIBLE);
}
Dialog 的 Window 建立過程
Dialog 的 Window 的建立過程與 Activity 非常相似,請看Dialog類的建構函式:
Dialog.java
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);
}
註釋的關鍵程式碼中的new PhoneWindow再明顯不過了。接著將Dialog的View新增到DecorView中,也是setContentView()方法:
public void setContentView(@NonNull View view) {
mWindow.setContentView(view);
}
mWindow就是PhoneWindow,所以跟Activity是一樣的,最後再看看是如何將DecorView新增到Window中去顯示,那就是show()方法了:
public void show() {
⋯⋯
// 關鍵程式碼
mWindowManager.addView(mDecor, l);
mShowing = true;
sendShowMessage();
}
有顯示就有關閉,所以在dismissDialog()方法中會將DecorView移除,請看程式碼:
void dismissDialog() {
⋯⋯
try {
mWindowManager.removeViewImmediate(mDecor);
} finally {
⋯⋯
}
}
我們知道,普通的Dialog有一個特殊之處,就是必須採用Activity的Context,如果是採用Application的Context的話就會報錯。這是因為沒有應用 token導致的,而應用 token 一般只有 Activity 擁有,另外,系統 Window 比較特殊,可以不需要 token。WindowManager.LayoutParams中的type表示Window的型別,而系統Window就是層級範圍是2000~2999範圍的,例如:TYPE_SYSTEM_ERROR,使用如下:
dialog.getWindow().setType(LayoutParams.TYPE_SYSTEM_ERROR); ,另外別忘記要在AndroidManifest中宣告android.permission.SYSTEM_ALERT_WINDOW許可權。
token
token是用來表示視窗的一個令牌,只有符合條件的token才能被WindowManagerService通過新增到應用上。還記得上面Activity.attach 方法為Activity初始化關聯的一列屬性有這樣一行程式碼嗎:
Activity.java
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) {
⋯⋯
mToken = token;
⋯⋯
mWindow.setWindowManager(
(WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
mToken, mComponent.flattenToString(),
(info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
⋯⋯
}
可以看到token傳遞到mWindow.setWindowManager中去,再來看看下面做了什麼事情:
Window.java
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中,appToken賦值到Window上,同時在當前Window上建立了WindowManager。
在一系列的傳遞中和跨程序通訊中,token會到達WindowManagerServer中去,請看addWindow()方法:
WindowManagerServer.java
public int addWindow(Session session, IWindow client, int seq,
WindowManager.LayoutParams attrs, int viewVisibility, int displayId,
Rect outContentInsets, Rect outStableInsets, Rect outOutsets,
InputChannel outInputChannel) {
⋯⋯
if (token == null) {
if (rootType >= FIRST_APPLICATION_WINDOW && rootType <= LAST_APPLICATION_WINDOW) {
Slog.w(TAG_WM, "Attempted to add application window with unknown token "
+ attrs.token + ". Aborting.");
return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
}
if (rootType == TYPE_INPUT_METHOD) {
Slog.w(TAG_WM, "Attempted to add input method window with unknown token "
+ attrs.token + ". Aborting.");
return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
}
if (rootType == TYPE_VOICE_INTERACTION) {
Slog.w(TAG_WM, "Attempted to add voice interaction window with unknown token "
+ attrs.token + ". Aborting.");
return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
}
if (rootType == TYPE_WALLPAPER) {
Slog.w(TAG_WM, "Attempted to add wallpaper window with unknown token "
+ attrs.token + ". Aborting.");
return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
}
if (rootType == TYPE_DREAM) {
Slog.w(TAG_WM, "Attempted to add Dream window with unknown token "
+ attrs.token + ". Aborting.");
return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
}
if (rootType == TYPE_QS_DIALOG) {
Slog.w(TAG_WM, "Attempted to add QS dialog window with unknown token "
+ attrs.token + ". Aborting.");
return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
}
if (rootType == TYPE_ACCESSIBILITY_OVERLAY) {
Slog.w(TAG_WM, "Attempted to add Accessibility overlay window with unknown token "
+ attrs.token + ". Aborting.");
return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
}
if (type == TYPE_TOAST) {
// Apps targeting SDK above N MR1 cannot arbitrary add toast windows.
if (doesAddToastWindowRequireToken(attrs.packageName, callingUid,
parentWindow)) {
Slog.w(TAG_WM, "Attempted to add a toast window with unknown token "
+ attrs.token + ". Aborting.");
return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
}
}
final IBinder binder = attrs.token != null ? attrs.token : client.asBinder();
token = new WindowToken(this, binder, type, false, displayContent,
session.mCanAddInternalSystemWindow);
} else if (rootType >= FIRST_APPLICATION_WINDOW && rootType <= LAST_APPLICATION_WINDOW) {
atoken = token.asAppWindowToken();
if (atoken == null) {
Slog.w(TAG_WM, "Attempted to add window with non-application token "
+ token + ". Aborting.");
return WindowManagerGlobal.ADD_NOT_APP_TOKEN;
} else if (atoken.removed) {
Slog.w(TAG_WM, "Attempted to add window with exiting application token "
+ token + ". Aborting.");
return WindowManagerGlobal.ADD_APP_EXITING;
}
} else if (rootType == TYPE_INPUT_METHOD) {
if (token.windowType != TYPE_INPUT_METHOD) {
Slog.w(TAG_WM, "Attempted to add input method window with bad token "
+ attrs.token + ". Aborting.");
return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
}
} else if (rootType == TYPE_VOICE_INTERACTION) {
if (token.windowType != TYPE_VOICE_INTERACTION) {
Slog.w(TAG_WM, "Attempted to add voice interaction window with bad token "
+ attrs.token + ". Aborting.");
return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
}
} else if (rootType == TYPE_WALLPAPER) {
if (token.windowType != TYPE_WALLPAPER) {
Slog.w(TAG_WM, "Attempted to add wallpaper window with bad token "
+ attrs.token + ". Aborting.");
return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
}
} else if (rootType == TYPE_DREAM) {
if (token.windowType != TYPE_DREAM) {
Slog.w(TAG_WM, "Attempted to add Dream window with bad token "
+ attrs.token + ". Aborting.");
return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
}
} else if (rootType == TYPE_ACCESSIBILITY_OVERLAY) {
if (token.windowType != TYPE_ACCESSIBILITY_OVERLAY) {
Slog.w(TAG_WM, "Attempted to add Accessibility overlay window with bad token "
+ attrs.token + ". Aborting.");
return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
}
} else if (type == TYPE_TOAST) {
// Apps targeting SDK above N MR1 cannot arbitrary add toast windows.
addToastWindowRequiresToken = doesAddToastWindowRequireToken(attrs.packageName,
callingUid, parentWindow);
if (addToastWindowRequiresToken && token.windowType != TYPE_TOAST) {
Slog.w(TAG_WM, "Attempted to add a toast window with bad token "
+ attrs.token + ". Aborting.");
return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
}
} else if (type == TYPE_QS_DIALOG) {
if (token.windowType != TYPE_QS_DIALOG) {
Slog.w(TAG_WM, "Attempted to add QS dialog window with bad token "
+ attrs.token + ". Aborting.");
return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
}
} else if (token.asAppWindowToken() != null) {
Slog.w(TAG_WM, "Non-null appWindowToken for system window of rootType=" + rootType);
// It is not valid to use an app token with other system types; we will
// instead make a new token for it (as if null had been passed in for the token).
attrs.token = null;
token = new WindowToken(this, client.asBinder(), type, false, displayContent,
session.mCanAddInternalSystemWindow);
}
final WindowState win = new WindowState(this, session, client, token, parentWindow,
appOp[0], seq, attrs, viewVisibility, session.mUid,
session.mCanAddInternalSystemWindow);
if (win.mDeathRecipient == null) {
// Client has apparently died, so there is no reason to
// continue.
Slog.w(TAG_WM, "Adding window client " + client.asBinder()
+ " that is dead, aborting.");
return WindowManagerGlobal.ADD_APP_EXITING;
}
if (win.getDisplayContent() == null) {
Slog.w(TAG_WM, "Adding window to Display that has been removed.");
return WindowManagerGlobal.ADD_INVALID_DISPLAY;
}
mPolicy.adjustWindowParamsLw(win.mAttrs);
win.setShowToOwnerOnlyLocked(mPolicy.checkShowToOwnerOnly(attrs));
res = mPolicy.prepareAddWindowLw(win, attrs);
if (res != WindowManagerGlobal.ADD_OKAY) {
return res;
}
⋯⋯
return res;
}
可以看到方法內根據token做了很多的判斷,return對應不同的值。都沒問題則開始新增Window。而在ViewRootImpl的setView()方法則有判斷對應的返回值來報錯:
ViewRootImpl.java
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
synchronized (this) {
if (mView == null) {
⋯⋯
if (res < WindowManagerGlobal.ADD_OKAY) {
mAttachInfo.mRootView = null;
mAdded = false;
mFallbackEventHandler.setView(null);
unscheduleTraversals();
setAccessibilityFocus(null, null);
switch (res) {
case WindowManagerGlobal.ADD_BAD_APP_TOKEN:
case WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN:
throw new WindowManager.BadTokenException(
"Unable to add window -- token " + attrs.token
+ " is not valid; is your activity running?");
case WindowManagerGlobal.ADD_NOT_APP_TOKEN:
throw new WindowManager.BadTokenException(
"Unable to add window -- token " + attrs.token
+ " is not for an application");
case WindowManagerGlobal.ADD_APP_EXITING:
throw new WindowManager.BadTokenException(
"Unable to add window -- app for token " + attrs.token
+ " is exiting");
case WindowManagerGlobal.ADD_DUPLICATE_ADD:
throw new WindowManager.BadTokenException(
"Unable to add window -- window " + mWindow
+ " has already been added");
case WindowManagerGlobal.ADD_STARTING_NOT_NEEDED:
// Silently ignore -- we would have just removed it
// right away, anyway.
return;
case WindowManagerGlobal.ADD_MULTIPLE_SINGLETON:
throw new WindowManager.BadTokenException("Unable to add window "
+ mWindow + " -- another window of type "
+ mWindowAttributes.type + " already exists");
case WindowManagerGlobal.ADD_PERMISSION_DENIED:
throw new WindowManager.BadTokenException("Unable to add window "
+ mWindow + " -- permission denied for window type "
+ mWindowAttributes.type);
case WindowManagerGlobal.ADD_INVALID_DISPLAY:
throw new WindowManager.InvalidDisplayException("Unable to add window "
+ mWindow + " -- the specified display can not be found");
case WindowManagerGlobal.ADD_INVALID_TYPE:
throw new WindowManager.InvalidDisplayException("Unable to add window "
+ mWindow + " -- the specified window type "
+ mWindowAttributes.type + " is not valid");
}
throw new RuntimeException(
"Unable to add window -- unknown error code " + res);
}
⋯⋯
}
}
}
Toast 的 Window 建立過程
Toast也是基本Window來實現的,它屬於系統Window,其內部通過IPC訪問系統服務NotificationManagerService來完成。Toast內部的檢視可以是系統預設樣式也可以通過 setView() 方法自定義 View,不管如何,它們都對應 Toast 的內部成員mNextView,Toast 提供 show() 和 cancal() 用於顯示和取消顯示Toast,它內部是跟NotificationManagerService的IPC 過程,程式碼如下:
Toast.java
public class Toast {
public void setView(View view) {
mNextView = view;
}
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
}
}
public void cancel() {
mTN.cancel();
}
TN 是一個 Binder 類,當 NotificationManagerService 處理 Toast 的顯示或隱藏請求時會跨程序回撥 TN 中的方法。由於 TN 執行在 Binder 執行緒池中,所以需要通過 Handler 將其切換到傳送 Toast 請求所在的執行緒:
private static class TN extends ITransientNotification.Stub {
public void cancel() {
if (localLOGV) Log.v(TAG, "CANCEL: " + this);
mHandler.obtainMessage(CANCEL).sendToTarget();
}
// 關鍵程式碼1:後面介紹
@Override
public void show(IBinder windowToken) {
if (localLOGV) Log.v(TAG, "SHOW: " + this);
mHandler.obtainMessage(SHOW, windowToken).sendToTarget();
}
// 關鍵程式碼2:後面介紹
@Override
public void hide() {
if (localLOGV) Log.v(TAG, "HIDE: " + this);
mHandler.obtainMessage(HIDE).sendToTarget();
}
⋯⋯
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 {
// 關鍵程式碼3
getService().cancelToast(mPackageName, TN.this);
} catch (RemoteException e) {
}
break;
}
}
}
};
}
}
來看看上面show()方法中呼叫了NotificationManagerService的 enqueueToast 方法:
NotificationManagerService.java
private final IBinder mService = new INotificationManager.Stub() {
@Override
public void enqueueToast(String pkg, ITransientNotification callback, int duration) {
⋯⋯
synchronized (mToastQueue) {
int callingPid = Binder.getCallingPid();
long callingId = Binder.clearCallingIdentity();
try {
ToastRecord record;
int index = indexOfToastLocked(pkg, callback);
if (index >= 0) {
record = mToastQueue.get(index);
record.update(duration);
} else {
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;
}
}
}
}
Binder token = new Binder();
mWindowManagerInternal.addWindowToken(token, TYPE_TOAST, DEFAULT_DISPLAY);
record = new ToastRecord(callingPid, pkg, callback, duration, token);
// 關鍵程式碼1
mToastQueue.add(record);
index = mToastQueue.size() - 1;
keepProcessAliveIfNeededLocked(callingPid);
}
if (index == 0) {
// 關鍵程式碼2
showNextToastLocked();
}
} finally {
Binder.restoreCallingIdentity(callingId);
}
}
}
⋯⋯
}
上面方法內部將 Toast 請求封裝為 ToastRecord 物件並將其新增到一個名為 mToastQueue 的佇列中,對於非系統應用來說,mToastQueue 中最多同時存在 50 個 ToastRecord,用於防止 DOS (Denial of Service 拒絕服務)。當 ToastRecord 新增到 mToastQueue 中後,NotificationManagerService 就會通過showNextToastLocked 方法來順序顯示 Toast,但是 Toast 真正的顯示並不是在 NotificationManagerService 中完成的,而是由ToastRecord 的 callback 來完成的,繼續看下showNextToastLocked()方法原始碼:
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) {
⋯⋯
}
}
}
程式碼中的callback實際上就是Toast中的TN物件的遠端Binder,通過callback來訪問TN中的方法是需要跨程序來完成,最終被呼叫的TN中的方法會執行在發起Toast請求的應用的Binder執行緒池中。
Toast 顯示以後,NotificationManagerService 還呼叫了 sheduleTimeoutLocked() 方法傳送一個延時訊息,具體的延時時長取決於 Toast 的顯示時長:
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);
}
MESSAGE_TIMEOUT訊息會呼叫到handleTimeout()方法:
private void handleTimeout(ToastRecord record) {
if (DBG) Slog.d(TAG, "Timeout pkg=" + record.pkg + " callback=" + record.callback);
synchronized (mToastQueue) {
int index = indexOfToastLocked(record.pkg, record.callback);
if (index >= 0) {
// 關鍵程式碼
cancelToastLocked(index);
}
}
}
handleTimeout()會通過 cancelToastLocked()方法來隱藏 Toast 並將它從 mToastQueue 中移除,這時如果 mToastQueue 中還有其他 Toast,那麼 NotificationManagerService 就繼續顯示其他 Toast。Toast 的隱藏也是通過 ToastRecord 的 callback 來完成的,同樣也是一次 IPC 過程。實際上,Toast 發起的取消顯示cancelToast()方法內部也是呼叫了cancelToastLocked()方法:
void cancelToastLocked(int index) {
ToastRecord record = mToastQueue.get(index);
try {
// 關鍵程式碼
record.callback.hide();
} catch (RemoteException e) {
⋯⋯
}
ToastRecord lastToast = mToastQueue.remove(index);
mWindowManagerInternal.removeWindowToken(lastToast.token, true, DEFAULT_DISPLAY);
keepProcessAliveIfNeededLocked(record.pid);
if (mToastQueue.size() > 0) {
showNextToastLocked();
}
}
以上顯示和隱藏提到的關鍵程式碼中:record.callback.show(record.token);和record.callback.hide();最終是通過 Toast 的 TN 類來實現的,TN 類的兩個方法 show() 和 hide(),這兩個方法的原始碼我們在上面介紹Handler時已經貼過,它們的實現就是往Handler傳送 SHOW 和 HIDE 訊息,然後執行handleShow()和handleHide()方法:
private static class TN extends ITransientNotification.Stub {
public void handleShow(IBinder windowToken) {
⋯⋯
if (mView != mNextView) {
⋯⋯
mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
⋯⋯
if (mView.getParent() != null) {
mWM.removeView(mView);
}
⋯⋯
try {
// 關鍵程式碼
mWM.addView(mView, mParams);
trySendAccessibilityEvent();
} catch (WindowManager.BadTokenException e) {
/* ignore */
}
}
}
public void handleHide() {
if (localLOGV) Log.v(TAG, "HANDLE HIDE: " + this + " mView=" + mView);
if (mView != null) {
if (mView.getParent() != null) {
// 關鍵程式碼
mWM.removeViewImmediate(mView);
}
mView = null;
}
}
}
完...
——本文部分內容參考自《Android開發藝術探索》