Dialog 窗口添加機制
代碼示例
首先舉兩個例子:
例子1 在Activity中
@OnClick(R.id.but) void onClick() { Log.d("LiaBin", "activity window token:" + this.getWindow().getAttributes().token); Dialog dialog = new ProgressDialog(this); dialog.show(); Log.d("LiaBin", "dialog window token:" + dialog.getWindow().getAttributes().token); }
輸出結果: 11-21 03:24:38.038 2040-2040/lbb.demo.first D/LiaBin: activity window token:android.os.BinderProxy@18421fac 11-21 03:24:38.054 2040-2040/lbb.demo.first D/LiaBin: dialog window token:null
例子2
@OnClick(R.id.but) void onClick() { Dialog dialog = new ProgressDialog(getApplicationContext()); dialog.show(); }
例子3
public class WindowService extends Service { @Override public IBinder onBind(Intent intent) { return null; } @Override public void onCreate() { super.onCreate(); //重點關注構造函數的參數 Dialog dialog = new ProgressDialog(this); dialog.setTitle("TestDialogContext"); dialog.show(); } }
輸出結果都是: E/AndroidRuntime: android.view.WindowManager$BadTokenException: Unable to add window – token null is not for an application E/AndroidRuntime: at android.view.ViewRootImpl.setView(ViewRootImpl.java:566) E/AndroidRuntime: at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:282) E/AndroidRuntime: at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:85) E/AndroidRuntime: at android.app.Dialog.show(Dialog.java:298)
為什么會出現以上兩種輸出結果,看以下分析。
Dialog源碼分析
Dialog是一系列XXXDialog的基類,我們可以new任意Dialog或者通過Activity提供的onCreateDialog(……)、onPrepareDialog(……)和showDialog(……)等方法來管理我們的Dialog,但是究其實質都是來源於Dialog基類,所以我們對於各種XXXDialog來說只用分析Dialog的窗口加載就可以了。
public class Dialog implements DialogInterface, Window.Callback, KeyEvent.Callback, OnCreateContextMenuListener, Window.OnWindowDismissedCallback { ...... public Dialog(Context context) { this(context, 0, true); } //構造函數最終都調運了這個默認的構造函數 Dialog(Context context, int theme, boolean createContextThemeWrapper) { //默認構造函數的createContextThemeWrapper為true if (createContextThemeWrapper) { //默認構造函數的theme為0 if (theme == 0) { TypedValue outValue = new TypedValue(); context.getTheme().resolveAttribute(com.android.internal.R.attr.dialogTheme, outValue, true); theme = outValue.resourceId; } mContext = new ContextThemeWrapper(context, theme); } else { mContext = context; } //mContext已經從外部傳入的context對象獲得值(一般是個Activity)!!!非常重要,先記住!!! //獲取WindowManager對象 mWindowManager = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE); //為Dialog創建新的Window Window w = PolicyManager.makeNewWindow(mContext); mWindow = w; //Dialog能夠接受到按鍵事件的原因 w.setCallback(this); w.setOnWindowDismissedCallback(this); //關聯WindowManager與新Window,特別注意第二個參數token為null,也就是說Dialog沒有自己的token //一個Window屬於Dialog的話,那么該Window的mAppToken對象是null w.setWindowManager(mWindowManager, null, null); w.setGravity(Gravity.CENTER); mListenersHandler = new ListenersHandler(this); } ...... }
Dialog構造函數首先把外部傳入的參數context對象賦值給了當前類的成員(我們的Dialog一般都是在Activity中啟動的,所以這個context一般是個Activity),然后調用context.getSystemService(Context.WINDOW_SERVICE)獲取WindowManager,這個WindowManager是哪來的呢?先按照上面說的context一般是個Activity來看待,可以發現這句實質就是Activity的getSystemService方法,我們看下源碼,如下:
http://androidxref.com/6.0.1_r10/xref/frameworks/base/core/java/android/app/Activity.java
@Override public Object getSystemService(@ServiceName @NonNull String name) { if (getBaseContext() == null) { throw new IllegalStateException( "System services not available to Activities before onCreate()"); } //我們Dialog中獲得的WindowManager對象就是這個分支 if (WINDOW_SERVICE.equals(name)) { //Activity的WindowManager return mWindowManager; } else if (SEARCH_SERVICE.equals(name)) { ensureSearchManager(); return mSearchManager; } return super.getSystemService(name); }
看見沒有,Dialog中的WindowManager成員實質和Activity里面是一樣的,也就是共用了一個WindowManager。
回到Dialog的構造函數繼續分析,在得到了WindowManager之后,程序又新建了一個Window對象(類型是PhoneWindow類型,和Activity的Window新建過程類似);接着通過w.setCallback(this)設置Dialog為當前window的回調接口,這樣Dialog就能夠接收事件處理了;接着把從Activity拿到的WindowManager對象關聯到新創建的Window中。
總結如下:
1.dialog使用有自己的window,不同於activity的window
2.dialog的mWindowManager變量其實就是activity對象的mWindowManager變量,此時注意因為window通過setWindowManager方法也會復制自己的mWindowManager,但這個mWindowManager是通過createLocalWindowManager返回的。不同於dialog的mWindowManager變量。不要混淆
//Window 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 mWindowManager = ((WindowManagerImpl)wm).createLocalWindowManager(this); } //在WindowManagerImpl類中 public WindowManagerImpl createLocalWindowManager(Window parentWindow) { return new WindowManagerImpl(mContext, parentWindow); }
Activity的getSystemService根本沒有創建WindowManager。類似於PhoneWindow和Window的關系,WindowManager是一個接口,具體的實現是WindowManagerImpl。
Application 的getSystemService()源碼其實是在ContextImpl中:有興趣的可以看看APP啟動時Context的創建:
@Override public Object getSystemService(String name) { return SystemServiceRegistry.getSystemService(this, name); }
SystemServiceRegistry類用靜態字段及方法中封裝了一些服務的代理,其中就包括WindowManagerService
public static Object getSystemService(ContextImpl ctx, String name) { ServiceFetcher<?> fetcher = SYSTEM_SERVICE_FETCHERS.get(name); return fetcher != null ? fetcher.getService(ctx) : null; } static { ... registerService(Context.WINDOW_SERVICE, WindowManager.class, new CachedServiceFetcher<WindowManager>() { @Override public WindowManager createService(ContextImpl ctx) { return new WindowManagerImpl(ctx.getDisplay()); }}); ... }
http://androidxref.com/6.0.1_r10/xref/frameworks/base/core/java/android/view/WindowManagerImpl.java
public WindowManagerImpl(Display display) { this(display, null); } private WindowManagerImpl(Display display, Window parentWindow) { mDisplay = display; mParentWindow = parentWindow; }
因此context.getApplicationContext().getSystemService()最終可以簡化為new WindowManagerImpl(ctx.getDisplay())。
3.activity覆蓋了context的getSystemService方法,如果WINDOW_SERVICE,那么返回的是當前activity的mWindowManager對象
至此Dialog的創建過程Window處理已經完畢,很簡單,所以接下來我們繼續看看Dialog的show與cancel方法,如下:
public void show() { ...... if (!mCreated) { //回調Dialog的onCreate方法 dispatchOnCreate(null); } //回調Dialog的onStart方法 onStart(); //類似於Activity,獲取當前新Window的DecorView對象,所以有一種自定義Dialog布局的方式就是重寫Dialog的onCreate方法,使用setContentView傳入布局,就像前面文章分析Activity類似 mDecor = mWindow.getDecorView(); ...... //獲取新Window的WindowManager.LayoutParams參數,和上面分析的Activity一樣type為TYPE_APPLICATION WindowManager.LayoutParams l = mWindow.getAttributes(); ...... try { //把一個View添加到Activity共用的windowManager里面去 mWindowManager.addView(mDecor, l); ...... } finally { } }
可以看見Dialog的新Window與Activity的Window的type同樣都為TYPE_APPLICATION,上面介紹WindowManager.LayoutParams時TYPE_APPLICATION的注釋明確說過,普通應用程序窗口TYPE_APPLICATION的token必須設置為Activity的token來指定窗口屬於誰。所以可以看見,既然Dialog和Activity共享同一個WindowManager(也就是上面分析的WindowManagerImpl),而WindowManagerImpl里面有個Window類型的mParentWindow變量,這個變量在Activity的attach中創建WindowManagerImpl時傳入的為當前Activity的Window,而當前Activity的Window里面的mAppToken值又為當前Activity的token,所以Activity與Dialog共享了同一個mAppToken值,只是Dialog和Activity的Window對象不同。
然后這句話是重點,有木有跟Activity窗口添加的時候很像,沒錯
mWindowManager.addView(mDecor, l);
Dialog機制大概就這些了,現在來分析一下,上面兩個代碼示例
第一個問題:
Dialog dialog = new ProgressDialog(this);//為什么這樣是正常的?
所以此時dialog的mWindowManager變量其實就是activity對象的mWindowManager變量。
還記得嗎?在WindowManager.addView實際上執行的是WindowManagerImpl的addView
http://androidxref.com/6.0.1_r10/xref/frameworks/base/core/java/android/view/WindowManagerImpl.java
public final class WindowManagerImpl implements WindowManager { //繼承自Object的單例類 private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance(); private final Window mParentWindow; public WindowManagerImpl(Display display) { this(display, null); } private WindowManagerImpl(Display display, Window parentWindow) { mDisplay = display; mParentWindow = parentWindow; } public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) { applyDefaultToken(params); //mParentWindow是上面分析的在Activity中獲取WindowManagerImpl實例化時傳入的當前Window //view是Activity中最頂層的mDecor mGlobal.addView(view, params, mDisplay, mParentWindow); } ...... }
所以此時mParentWindow其實就是Activity的PhoneWindow對象,雖然dialog有自己的PhoneWindow,但是這兩者完全是兩碼事,不要混淆
所以在WindowManagerGlobal.addView方法中調用
public void addView(View view, ViewGroup.LayoutParams params, Display display, Window parentWindow) { //... final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params; if (parentWindow != null) { //依據當前Activity的Window調節sub Window的LayoutParams parentWindow.adjustLayoutParamsForSubWindow(wparams); } else { // If there's no parent, then hardware acceleration for this view is // set from the application's hardware acceleration setting. final Context context = view.getContext(); if (context != null && (context.getApplicationInfo().flags & ApplicationInfo.FLAG_HARDWARE_ACCELERATED) != 0) { wparams.flags |= WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED; } } ViewRootImpl root; View panelParentView = null; synchronized (mLock) { //... root = new ViewRootImpl(view.getContext(), display); view.setLayoutParams(wparams); mViews.add(view); mRoots.add(root); mParams.add(wparams); } // do this last because it fires off messages to start doing things try { root.setView(view, wparams, panelParentView); } catch (RuntimeException e) { //... } }
http://androidxref.com/6.0.1_r10/xref/frameworks/base/core/java/android/view/Window.java
adjustLayoutParamsForSubWindow方法中
void adjustLayoutParamsForSubWindow(WindowManager.LayoutParams wp) { CharSequence curTitle = wp.getTitle(); if (wp.type >= WindowManager.LayoutParams.FIRST_SUB_WINDOW && wp.type <= WindowManager.LayoutParams.LAST_SUB_WINDOW) { //... } else if (wp.type >= WindowManager.LayoutParams.FIRST_SYSTEM_WINDOW && //... } else { if (wp.token == null) { wp.token = mContainer == null ? mAppToken : mContainer.mAppToken; } if ((curTitle == null || curTitle.length() == 0) && mAppName != null) { wp.setTitle(mAppName); } } if (wp.packageName == null) { wp.packageName = mContext.getPackageName(); } if (mHardwareAccelerated) { wp.flags |= WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED; } }
調整wp的時候,所以最后wp.token拿到的就是Activity的mToken,就不為null,所以在最后WindowManagerService的addWindow方法中就不會讓ViewRootImpl中拋異常了,所以一切OK
第二個問題:
Log.d(“LiaBin”, “dialog window token:” + dialog.getWindow().getAttributes().token); 打印的為什么是null,而不是activity的token
現在就很好理解了,首先dialog.getWindow(),那么獲取的就是dialog的PhoneWindow,而Dialog的window的mWindowAttributes的token值初始化就為null
雖然調用了adjustLayoutParamsForSubWindow方法,但是並沒有調整Dialog的window的mWindowAttributes的token值,因為以下代碼行就把兩者關系斷了,調整的是另外一個對象
final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams)params;
第三個問題:
Dialog dialog = new ProgressDialog(getApplicationContext());為什么會拋異常BadTokenException: Unable to add window – token null is not for an application
因為mContext賦值為了getApplicationContext(),那么
//獲取WindowManager對象 mWindowManager = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
那么此時的mWindowManager就是全局唯一的mWindowManager了,而不是activity的mWindowManager。可以看上一篇的分析。調用的其實是ContextImpl的getSystemService方法
所以在WindowManagerGlobal.addView方法中parentWindow就為null了,所以就不會去調用adjustLayoutParamsForSubWindow方法了,所以最后params的token就為null了
在最后WindowManagerService的addWindow方法,把param的token取出來一看是null,就return WindowManagerGlobal.ADD_NOT_APP_TOKEN;返回給ViewRootImpl的setView方法
再來看ViewRootImpl的setView方法,片段
case WindowManagerGlobal.ADD_NOT_APP_TOKEN:throw new WindowManager.BadTokenException(“Unable to add window – token ” + attrs.token + ” is not for an application”);
所以最后拋BadTokenException異常啦
第四個問題:
在服務中調用Dialog dialog = new ProgressDialog(this);為什么要會拋異常
因為service中並沒有跟activity做同樣的處理,調用的其實是ContextImpl的getSystemService方法,所以此時的mWindowManager就是全局唯一的mWindowManager了,
另外一種情況:
在Activity中使用Dialog的時候,為什么有時候會報錯“Unable to add window – token is not valid; is your activity running?”?這種情況一般發生在什么時候?一般發生在Activity進入后台,Dialog沒有主動Dismiss掉,然后從后台再次進入App的時候。
Dialog窗口加載總結
從圖中可以看出,Activity和Dialog共用了一個Token對象,Dialog必須依賴於Activity而顯示(通過別的context搞完之后token都為null,最終會在ViewRootImpl的setView方法中加載時因為token為null拋出異常),所以Dialog的Context傳入參數一般是一個存在的Activity,如果Dialog彈出來之前Activity已經被銷毀了,則這個Dialog在彈出的時候就會拋出異常,因為token不可用了。在Dialog的構造函數中我們關聯了新Window的callback事件監聽處理,所以當Dialog顯示時Activity無法消費當前的事件。
PopWindow 窗口添加機制
http://androidxref.com/6.0.1_r10/xref/frameworks/base/core/java/android/widget/PopupWindow.java
public class PopupWindow { private int mWindowLayoutType = WindowManager.LayoutParams.TYPE_APPLICATION_PANEL; ...... //我們只分析最常用的一種構造函數 public PopupWindow(View contentView, int width, int height, boolean focusable) { if (contentView != null) { //獲取mContext,contentView實質是View,View的mContext都是構造函數傳入的,View又層級傳遞,所以最終這個mContext實質是Activity!!!很重要 mContext = contentView.getContext(); //獲取Activity的getSystemService的WindowManager mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE); } //進行一些Window類的成員變量初始化賦值操作 setContentView(contentView); setWidth(width); setHeight(height); setFocusable(focusable); } ...... }
其中注意,view創建的時候都會把一個context參數傳遞進去,context就是當前的activity了,所以其實contentView.getContext();返回的是該Activity,所以mWindowManager共享當前Activity的mWindowManager變量。同時因為popupwindow構造函數的參數是view,並不是context,所以並不用擔心getApplicationContext造成的問題
public void showAsDropDown(View anchor, int xoff, int yoff, int gravity) { ...... //anchor是Activity中PopWindow准備依附的View,這個View的token實質也是Activity的Window中的token,也即Activity的token //第一步 初始化WindowManager.LayoutParams WindowManager.LayoutParams p = createPopupLayout(anchor.getWindowToken()); //第二步 preparePopup(p); ...... //第三步 invokePopup(p); }
createPopupLayout
private WindowManager.LayoutParams createPopupLayout(IBinder token) { //實例化一個默認的WindowManager.LayoutParams,其中type=TYPE_APPLICATION WindowManager.LayoutParams p = new WindowManager.LayoutParams(); //設置Gravity p.gravity = Gravity.START | Gravity.TOP; //設置寬高 p.width = mLastWidth = mWidth; p.height = mLastHeight = mHeight; //依據背景設置format if (mBackground != null) { p.format = mBackground.getOpacity(); } else { p.format = PixelFormat.TRANSLUCENT; } //設置flags p.flags = computeFlags(p.flags); //修改type=WindowManager.LayoutParams.TYPE_APPLICATION_PANEL,mWindowLayoutType有初始值,type類型為子窗口 p.type = mWindowLayoutType; //設置token為Activity的token p.token = token; ...... return p; }
private void preparePopup(WindowManager.LayoutParams p) { ...... //有無設置PopWindow的background區別 if (mBackground != null) { ...... //如果有背景則創建一個PopupViewContainer對象的ViewGroup PopupViewContainer popupViewContainer = new PopupViewContainer(mContext); PopupViewContainer.LayoutParams listParams = new PopupViewContainer.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, height ); //把背景設置給PopupViewContainer的ViewGroup popupViewContainer.setBackground(mBackground); //把我們構造函數傳入的View添加到這個ViewGroup popupViewContainer.addView(mContentView, listParams); //返回這個ViewGroup mPopupView = popupViewContainer; } else { //如果沒有通過PopWindow的setBackgroundDrawable設置背景則直接賦值當前傳入的View為PopWindow的View mPopupView = mContentView; } ...... }
可以看見preparePopup方法的作用就是判斷設置View,如果有背景則會在傳入的contentView外面包一層PopupViewContainer(實質是一個重寫了事件處理的FrameLayout)之后作為mPopupView,如果沒有背景則直接用contentView作為mPopupView。我們再來看下這里的PopupViewContainer類,如下源碼:
private class PopupViewContainer extends FrameLayout { ...... @Override protected int[] onCreateDrawableState(int extraSpace) { ...... } @Override public boolean dispatchKeyEvent(KeyEvent event) { ...... } @Override public boolean dispatchTouchEvent(MotionEvent ev) { if (mTouchInterceptor != null && mTouchInterceptor.onTouch(this, ev)) { return true; } return super.dispatchTouchEvent(ev); } @Override public boolean onTouchEvent(MotionEvent event) { ...... if(xxx) { dismiss(); } ...... } @Override public void sendAccessibilityEvent(int eventType) { ...... } }
可以看見,這個PopupViewContainer是一個PopWindow的內部私有類,它繼承了FrameLayout,在其中重寫了Key和Touch事件的分發處理邏輯。同時查閱PopupView可以發現,PopupView類自身沒有重寫Key和Touch事件的處理,所以如果沒有將傳入的View對象放入封裝的ViewGroup中,則點擊Back鍵或者PopWindow以外的區域PopWindow是不會消失的(其實PopWindow中沒有向Activity及Dialog一樣new新的Window,所以不會有新的callback設置,也就沒法處理事件消費了)。
private void invokePopup(WindowManager.LayoutParams p) { if (mContext != null) { p.packageName = mContext.getPackageName(); } mPopupView.setFitsSystemWindows(mLayoutInsetDecor); setLayoutDirectionFromAnchor(); mWindowManager.addView(mPopupView, p); }
這里使用了Activity的WindowManager將我們的PopWindow進行了顯示。
到此可以發現,PopWindow的實質無非也是使用WindowManager的addView、updateViewLayout、removeView進行一些操作展示。與Dialog不同的地方是沒有新new Window而已(也就沒法設置callback,無法消費事件,也就是前面說的PopupWindow彈出后可以繼續與依賴的Activity進行交互的原因)。
到此PopWindw的窗口加載顯示機制就分析完畢了,接下來進行總結與應用開發技巧提示。
可以看見preparePopup方法的作用就是判斷設置View,如果有背景則會在傳入的contentView外面包一層PopupViewContainer(實質是一個重寫了事件處理的FrameLayout)之后作為mPopupView,如果沒有背景則直接用contentView作為mPopupView
PopupViewContainer是一個PopWindow的內部私有類,它繼承了FrameLayout,在其中重寫了Key和Touch事件的分發處理邏輯。同時查閱PopupView可以發現,PopupView類自身沒有重寫Key和Touch事件的處理,所以如果沒有將傳入的View對象放入封裝的ViewGroup中,則點擊Back鍵或者PopWindow以外的區域PopWindow是不會消失的(其實PopWindow中沒有向Activity及Dialog一樣new新的Window,所以不會有新的callback設置,也就沒法處理事件消費了)。
1.與Dialog不同的地方是沒有新new Window而已(也就沒法設置callback,無法消費事件,也就是前面說的PopupWindow彈出后可以繼續與依賴的Activity進行交互的原因)。
2.如果設置了PopupWindow的background,則點擊Back鍵或者點擊PopupWindow以外的區域時PopupWindow就會dismiss;如果不設置PopupWindow的background,則點擊Back鍵或者點擊PopupWindow以外的區域PopupWindow不會消失。
另一方面,如果需要全屏的popupwindow,那么可以使用一下代碼
view.showAtLocation(mActivity.getWindow().getDecorView(), Gravity.CENTER, 0, 0);
getWindow().getDecorView()就是獲取頂層的DecorView
Toast 窗口添加機制
我們常用的Toast窗口其實和前面分析的Activity、Dialog、PopWindow都是不同的,因為它和輸入法、牆紙類似,都是系統窗口。
public static Toast makeText(Context context, CharSequence text, @Duration int duration) { //new一個Toast對象 Toast result = new Toast(context); //獲取前面有篇文章分析的LayoutInflater LayoutInflater inflate = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); //加載解析Toast的布局,實質transient_notification.xml是一個LinearLayout中套了一個@android:id/message的TextView而已 View v = inflate.inflate(com.android.internal.R.layout.transient_notification, null); //取出布局中的TextView TextView tv = (TextView)v.findViewById(com.android.internal.R.id.message); //把我們的文字設置到TextView上 tv.setText(text); //設置一些屬性 result.mNextView = v; result.mDuration = duration; //返回新建的Toast return result; } public void show() { ...... INotificationManager service = getService(); String pkg = mContext.getOpPackageName(); TN tn = mTN; tn.mNextView = mNextView; try { //把TN對象和一些參數傳遞到遠程NotificationManagerService中去 service.enqueueToast(pkg, tn, mDuration); } catch (RemoteException e) { // Empty } }
這里使用了IBinder機制,其實是通過遠程NotificationManagerService服務來管理toast的
private static class TN extends ITransientNotification.Stub { private final WindowManager.LayoutParams mParams = new WindowManager.LayoutParams(); params.type = WindowManager.LayoutParams.TYPE_TOAST; ...... //僅僅是實例化了一個Handler,非常重要!!!!!!!! final Handler mHandler = new Handler(); ...... final Runnable mShow = new Runnable() { @Override public void run() { handleShow(); } }; final Runnable mHide = new Runnable() { @Override public void run() { handleHide(); // Don't do this in handleHide() because it is also invoked by handleShow() mNextView = null; } }; ...... //實現了AIDL的show與hide方法 @Override public void show() { if (localLOGV) Log.v(TAG, "SHOW: " + this); mHandler.post(mShow); } @Override public void hide() { if (localLOGV) Log.v(TAG, "HIDE: " + this); mHandler.post(mHide); } ...... }
此時說明toast的type是TYPE_TOAST,這里直接new了一個handler,所以如果在子線程中直接顯示一個taost,就會報異常,除非在子線程中調用Looper的prepare和looper方法,才可以在線程中顯示toast。接下來重點分析handleShow方法
public void handleShow() { if (localLOGV) Log.v(TAG, "HANDLE SHOW: " + this + " mView=" + mView + " mNextView=" + mNextView); if (mView != mNextView) { // remove the old view if necessary //如果有必要就通過WindowManager的remove刪掉舊的 handleHide(); mView = mNextView; Context context = mView.getContext().getApplicationContext(); String packageName = mView.getContext().getOpPackageName(); if (context == null) { context = mView.getContext(); } //通過得到的context(一般是ContextImpl的context)獲取WindowManager對象(上一篇文章分析的單例的WindowManager) mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE); ...... //在把Toast的View添加之前發現Toast的View已經被添加過(有partent)則刪掉 if (mView.getParent() != null) { ...... mWM.removeView(mView); } ...... //把Toast的View添加到窗口,其中mParams.type在構造函數中賦值為TYPE_TOAST!!!!!!特別重要 mWM.addView(mView, mParams); ...... } }
mWM此時是全局單例的WindowManager,調用的是ContextImpl的getSystemService方法獲取
最后總結一下:
通過分析TN類的handler可以發現,如果想在非UI線程使用Toast需要自行聲明Looper,否則運行會拋出Looper相關的異常;UI線程不需要,因為系統已經幫忙聲明。
1.在使用Toast時context參數盡量使用getApplicationContext(),可以有效的防止靜態引用導致的內存泄漏。 因為首先toast構造函數中拿到了toast,所以如果在當前activity中彈出一個toast,然后finish掉該toast,toast並不依賴activity,是系統級的窗口,當然也不會隨着activity的finish就消失,只是隨着設置時間的到來而消失,所以如果此時設置toast顯示的時間足夠長,那么因為toast持有該activity的引用,那么該activty就一直不能被回收,一直到toast消失,造成內存泄漏,所以最好使用getApplicationContext()
2.有時候我們會發現Toast彈出過多就會延遲顯示,因為上面源碼分析可以看見Toast.makeText是一個靜態工廠方法,每次調用這個方法都會產生一個新的Toast對象,當我們在這個新new的對象上調用show方法就會使這個對象加入到NotificationManagerService管理的mToastQueue消息顯示隊列里排隊等候顯示;所以如果我們不每次都產生一個新的Toast對象(使用單例來處理)就不需要排隊,也就能及時更新了。
3.Toast的顯示交由遠程的NotificationManagerService管理是因為Toast是每個應用程序都會彈出的,而且位置和UI風格都差不多,所以如果我們不統一管理就會出現覆蓋疊加現象,同時導致不好控制,所以Google把Toast設計成為了系統級的窗口類型,由NotificationManagerService統一隊列管理。