android dialog,popupwindow,toast窗口的添加機制


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統一隊列管理。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM