• 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统一队列管理。

  • 相关阅读:
    docker volume
    Nginx 安装配置
    Shell test 命令,Shell 输入/输出重定向
    Shell 变量,Shell echo命令
    MongoDB数据库
    linux yum 命令
    Linux 磁盘管理,Linux vi/vim
    Python----Paramiko模块和堡垒机实战
    Linux 文件与目录管理,Linux系统用户组的管理
    Linux 忘记密码解决方法,Linux 远程登录
  • 原文地址:https://www.cnblogs.com/mingfeng002/p/10962244.html
Copyright © 2020-2023  润新知