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

posted on 2019-06-03 10:39  mingfeng002  阅读(2340)  评论(1编辑  收藏  举报