Android之Window和弹窗问题

目录介绍

  • 10.0.0.1 Window是什么?如何经过WindowManager添加Window(代码实现)?WindowManager的主要功能是什么?
  • 10.0.0.2 Window概念解析?WindowSession的建立过程是怎样的?WindowSession的做用?Token的使用场景?
  • 10.0.0.3 Activity、View、Window三者之间的关系,Window有哪几种类型?
  • 10.0.0.5 Activity的启动过程是怎样的?Activity建立和Dialog建立过程的异同?
  • 10.0.0.6 如何处理快速连续点击了屡次按钮时Toast就触发了屡次而关闭不掉?
  • 10.0.0.7 DecorView什么时候才被WindowManager真正添加到Window中?Window的addView源码分析?
  • 10.0.0.8 Dialog的Window建立过程?为何Dialog不能用Application的Context?
  • 10.0.0.9 什么是DecorView?如何获取到DecorView?DecorView的职责是什么?DecorView如何被加载到Window中?
  • 10.0.1.0 DecorView如何显示出来,为何setContentView()设置的界面,为何在onResume()以后才对用户可见呢?
  • 10.0.1.1 什么是ViewRoot?ViewRoot属于View树的一份子吗?ViewRoot的工做流程是怎么样的?
  • 10.0.1.2 吐司为什么会出现内存泄漏?在Toast构造方法中建立NT对象是干什么用的?Toast是怎么show出来的?
  • 10.0.1.3 连续吐司是如何肯定吐司的前后顺序?为何Toast执行show后过了一下子就自动销毁?
  • 10.0.1.4 如何理解普通应用的Toast显示数量是有限制的?为何要判断是不是系统吐司?为什么Activity销毁后Toast仍会显示?
  • 10.0.1.5 为何说Toast尽可能用全局上下文?说一下Toast的显示和隐藏重点逻辑,说下你的理解?
  • 10.0.1.6 Toast报错Unable to add window是什么意思?Toast运行在子线程会问题,在子线程或者service中能运行吗?
  • 10.0.1.7 为何建议用DialogFragment替代Dialog?如何定义DialogFragment样式?使用dialogFragment有何好处?
  • 10.0.1.8 Dialog的Window建立过程是怎样的?为何Dialog不能用Application的Context,说一下缘由?
  • 10.0.1.9 Dialog和Window有什么关系?Dialog的dismiss和cancel()方法均可销毁弹窗,它们有什么区别?
  • 10.0.2.0 PopupWindow中不设置为何必须设置宽高?PopupWindow和Dialog有什么区别?说下建立和销毁的大概流程?
  • 10.0.2.1 Snackbar与吐司有何区别在哪里?Snackbar控件show时为什么从下往上移出来?为何显示在最下面?
  • 10.0.2.2 说一下Snackbar和SnackbarManager类的设计有哪些奥妙的地方,如何处理消息的显示顺序?

好消息

  • 博客笔记大汇总【15年10月到至今】,包括Java基础及深刻知识点,Android技术博客,Python学习笔记等等,还包括平时开发中遇到的bug汇总,固然也在工做之余收集了大量的面试题,长期更新维护而且修正,持续完善……开源的文件是markdown格式的!同时也开源了生活博客,从12年起,积累共计500篇[近100万字],将会陆续发表到网上,转载请注明出处,谢谢!
  • 连接地址:https://github.com/yangchong2...
  • 若是以为好,能够star一下,谢谢!固然也欢迎提出建议,万事起于忽微,量变引发质变!

弹窗博客笔记汇总

  • 02.Toast源码深度分析php

    • 最简单的建立,简单改造避免重复建立,show()方法源码分析,scheduleTimeoutLocked吐司如何自动销毁的,TN类中的消息机制是如何执行的,普通应用的Toast显示数量是有限制的,用代码解释为什么Activity销毁后Toast仍会显示,Toast偶尔报错Unable to add window是如何产生的,Toast运行在子线程问题,Toast如何添加系统窗口的权限等等
  • 03.DialogFragment源码分析android

    • 最简单的使用方法,onCreate(@Nullable Bundle savedInstanceState)源码分析,重点分析弹窗展现和销毁源码,使用中show()方法遇到的IllegalStateException分析
  • 04.Dialog源码分析git

    • AlertDialog源码分析,经过AlertDialog.Builder对象设置属性,Dialog生命周期,Dialog中show方法展现弹窗分析,Dialog的dismiss销毁弹窗,Dialog弹窗问题分析等等
  • 05.PopupWindow源码分析github

    • 显示PopupWindow,注意问题宽和高属性,showAsDropDown()源码,dismiss()源码分析,PopupWindow和Dialog有什么区别?为什么弹窗点击一下就dismiss呢?
  • 06.Snackbar源码分析面试

    • 最简单的建立,Snackbar的make方法源码分析,Snackbar的show显示与点击消失源码分析,显示和隐藏中动画源码分析,Snackbar的设计思路,为何Snackbar老是显示在最下面
  • 07.弹窗常见问题segmentfault

    • DialogFragment使用中show()方法遇到的IllegalStateException,什么常见产生的?Toast偶尔报错Unable to add window,Toast运行在子线程致使崩溃如何解决?
  • 08.Builder模式markdown

    • 你会发现,在这个弹窗封装库中,不少地方用到了builder模式,那么能够先了解下Builder模式使用场景,简单案例,Builder模式实际案例Demo展现,看看AlertDialog.Builder源代码如何实现,为何AlertDialog要使用builder模式呢?builder模式优缺点分析。

10.0.0.1 Window是什么?如何经过WindowManager添加Window(代码实现)?WindowManager的主要功能是什么?

  • Window是什么?app

    • 表示一个窗口的概念,是全部View的直接管理者,任何视图都经过Window呈现(点击事件由Window->DecorView->View; Activity的setContentView底层经过Window完成)
    • Window是一个抽象类,具体实现是PhoneWindow
    • 建立Window须要经过WindowManager建立
    • WindowManager是外界访问Window的入口
    • Window具体实现位于WindowManagerService中
    • WindowManager和WindowManagerService的交互是经过IPC完成
  • 如何经过WindowManager添加Window(代码实现)?异步

    • 以下所示ide

      //1. 控件 
      Button button = new Button(this); 
      button.setText("Window Button"); 
      //2. 布局参数 
      WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams(WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.WRAP_CONTENT, 0, 0, PixelFormat.TRANSPARENT); 
      layoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED; 
      layoutParams.gravity = Gravity.LEFT | Gravity.TOP; 
      layoutParams.x = 100; 
      layoutParams.y = 300; 
      // 必需要有type否则会异常: the specified window type 0 is not valid 
      layoutParams.type = WindowManager.LayoutParams.TYPE_SYSTEM_ERROR; 
      //3. 获取WindowManager并添加控件到Window中 
      WindowManager windowManager = getWindowManager(); 
      windowManager.addView(button, layoutParams);
  • WindowManager的主要功能是什么?

    • 添加、更新、删除View

      public interface ViewManager{ 
          public void addView(View view, ViewGroup.LayoutParams params); 
          //添加View 
          public void updateViewLayout(View view, ViewGroup.LayoutParams params); 
          //更新View 
          public void removeView(View view); 
          //删除View 
      }

10.0.0.2 Window概念解析?WindowSession的建立过程是怎样的?WindowSession的做用?Token的使用场景?

  • Window概念解析?

    • Window和View经过ViewRootImpl创建联系
    • Window并非实际存在的,而是以View的形式存在
    • WindowManager的三个接口方法也是针对View的
    • 实际使用中没法直接访问Window,必须经过WindowManager
    • View是视图的呈现方式,可是不能单独存在,必须依附在Window这个抽象的概念上
    • WMS把全部的用户消息发给View/ViewGroup,可是在View/ViewGroup处理消息的过程当中,有一些操做是公共的, Window把这些公共行为抽象出来, 这就是Window。
  • WindowSession的建立过程是怎样的?

    • 在WindowManager的addView中会建立ViewRootImpl,内部会经过WMS去获取WindowSession
    • WindowSession的类型是IWindowSession,自己是Binder对象,真正实现类是Session
    • image
  • WindowSession的做用?博客

    • 表示一个Active Client Session
    • 每一个进程通常都有一个Session对象
    • 用于WindowManager交互
  • Token的使用场景?

    • Popupwindow的showAtLocation第一个参数须要传入View,这个View就是用来获取Token的。
    • Android 5.0新增空间SnackBar同理也须要一个View来获取Token
  • Token是什么?

    • 类型为IBinder,是一个Binder对象。
    • 主要分两种Token:

      • 指向Window的token: 主要是实现WmS和应用所在进程通讯。
      • 指向ActivityRecord的token: 主要是实现WmS和AmS通讯的。
  • Activity中的Token

    • ActivityRecord是AmS中用来保存一个Activity信息的辅助类。
    • AMS中须要根据Token去找到对应的ActivityRecord。

10.0.0.3 Activity、View、Window三者之间的关系,Window有哪几种类型?

  • Activity、View、Window三者之间的关系

    • 在Activity启动过程其中的attach()方法中初始化了PhoneWindow,而PhoneWindow是Window的惟一实现类,而后Activity经过setContentView将View设置到了PhoneWindow上,而View经过WindowManager的addView()、removeView()、updateViewLayout()对View进行管理。
  • Window有哪几种类型

    • 应用Window:对应一个Activity。
    • 子Window:不能单独存在,需附属特定的父Window。如Dialog。
    • 系统Window: 需申明权限才能建立。如Toast。
  • Activity 与 PhoneWindow 与 DecorView 关系图

    • image

10.0.0.5 Activity的启动过程是怎样的?Activity的视图加载的源码分析?Activity建立和Dialog建立过程的异同?

  • Activity的启动过程是怎样的?

    • 最终会由ActivityThread中的performLauchActivity来完成整个启动过程
    • performLauchActivity内部会经过类加载器建立Activity的实例对象
    • 并为Activity的实例对象调用attach方法,为其关联运行过程当中因此来的上下文环境变量
    • attch方法中,系统会建立Activity所属的Window对象,并为其设置回调接口
    • Window对象的建立是经过PolicyManager的makeNewWindow方法实现。博客
    • Activity实现了window的callback接口,所以外界状态改变时会回调Activity的方法(onAttachedToWindow、dispatchTouchEvent等等)
  • Activity的视图加载的源码分析

    • image
  • Dialog的Window建立过程

    • 建立WindowDialog。和Activity相似,一样是经过PolicyManager.makeNewWindow() 来实现。
    • 初始化DecorView并将Dialog的视图添加到DecorView中去。和Activity相似,一样是经过Window.setContentView() 来实现。
    • 将DecorView添加到Window中显示。和Activity同样,都是在自身要出如今前台时才会将添加Window。

      • Dialog.show() 方法:完成DecorView的显示。
      • WindowManager.remoteViewImmediate() 方法:当Dialog被dismiss时移除DecorView。

10.0.0.6 如何处理快速连续点击了屡次按钮时Toast就触发了屡次而关闭不掉?

  • 使用中遇到的问题

    • 例如:当点击有些按钮,须要吐司进行提示时;快速连续点击了屡次按钮,Toast就触发了屡次。可能致使Toast就长时间关闭不掉了。又或者咱们其实已在进行其余操做了,应该弹出新的Toast提示,而上一个Toast却还没显示结束。博客
  • 解决的办法

    建立工具类:
    /**
    * 吐司工具类    避免点击屡次致使吐司屡次,最后致使Toast就长时间关闭不掉了
    * @param context
*/
private static Toast toast;
public static void showToast(Context context, String content) {
    if (toast == null) {
        toast = Toast.makeText(context.getApplicationContext(), content, Toast.LENGTH_SHORT);
    } else {
        toast.setText(content);
    }
    toast.show();
}
```
  • 这样用的原理

    • 先判断Toast对象是否为空,若是是空的状况下才会调用makeText()方法来去生成一个Toast对象,不然就直接调用setText()方法来设置显示的内容,最后再调用show()方法将Toast显示出来。因为不会每次调用的时候都生成新的Toast对象,所以刚才咱们遇到的问题在这里就不会出现

10.0.0.7 DecorView什么时候才被WindowManager真正添加到Window中?Window的addView源码分析?

  • DecorView什么时候才被WindowManager真正添加到Window中?

    • 即便Activity的布局已经成功添加到DecorView中,DecorView此时尚未添加到Window中
    • ActivityThread的handleResumeActivity方法中,首先会调用Activity的onResume方法,接着调用Activity的makeVisible()方法
    • makeVisible()中完成了DecorView的添加和显示两个过程
    • image
  • Window的addView源码分析?

    • WindowManager是一个接口,真正实现类是WindowManagerImpl,并最终以代理模式交给WindowManagerGlobal实现。
    • addView: 1-建立ViewRootImpl;2-将ViewRoor、DecorView、布局参数保存到WM的内部列表中;3-ViewRoot.setView()创建ViewRoot和DecorView的联系。
    • setView:1-进行View绘制三大流程;2-会经过WindowSession完成Window的添加过程(一次IPC调用)
    • requestLayout:内部调用scheduleTraversals(), 底层经过mChoreographer去监听下一帧的刷新信号。
    • mWindowSession.addToDisplay: 执行WindowManangerService的addWindow
    • addWindow: 检查参数等设置;检查Token;将Token、Window保存到WMS中;将WindowState保存到Session中。
  • Window的remove源码与解析

    • WindowManager中提供了两种删除接口:removeView异步删除、removeViewImmediate同步删除(不建议使用)
    • 调用WMGlobal的removeView
    • 调用到WMGlobal的removeViewLocked进行真正的移除
    • 执行ViewRoot的die方法(): 1-同步方法直接调用doDie 2-异步方法直接发送Message
    • doDie(): 调用dispatchDetachedFromWindow()和WindowManagerGlobal.getInstance().doRemoveView(this)
    • dispatchDetachedFromWindow:博客

      • 1回调onDetachedFromeWindow;
      • 2垃圾回收相关操做;
      • 3经过Session的remove()在WMS中删除Window;
      • 4经过Choreographer移除监听器

10.0.0.8 Dialog的Window建立过程?为何Dialog不能用Application的Context?

  • Dialog的Window建立过程?

    • 建立Window——一样是经过PolicyManager的makeNewWindow方法完成,与Activity建立过程一致
    • 初始化DecorView并将Dialog的视图添加到DecorView中——和Activity一致(setContentView)
    • 将DecorView添加到Window中并显示——在Dialog的show方法中,经过WindowManager将DecorView添加到Window中(mWindowManager.addView(mDecor, 1))
    • Dialog关闭时会经过WindowManager来移除DecorView:mWindowManager.removeViewImmediate(mDecor)
    • Dialog必须采用Activity的Context,由于有应用token(Application的Context没有应用token),也能够将Dialog的Window经过type设置为系统Window就再也不须要token。
  • 为何Dialog不能用Application的Context?

    • Dialog自己的Token为null,在初始化时若是是使用Application或者Service的Context,在获取到WindowManager时,获取到的token依然是null。
    • Dialog若是采用Activity的Context,获取到的WindowManager是在activity.attach()方法中建立,token指向了activity的token。
    • 由于经过Application和Service的Context将没法获取到Token从而致使失败。

10.0.0.9 什么是DecorView?如何获取到DecorView?DecorView的职责是什么?DecorView如何被加载到Window中?

  • 什么是DecorView

    • DecorView是FrameLayout的子类,它能够被认为是Android视图树的根节点视图。
    • DecorView做为顶级View,通常状况下它内部包含一个竖直方向的LinearLayout,在这个LinearLayout里面有上下三个部分,上面是个ViewStub,延迟加载的视图(应该是设置ActionBar,根据Theme设置),中间的是标题栏(根据Theme设置,有的布局没有),下面的是内容栏。
  • 如何获取到DecorView

    • 在Activity中经过setContentView所设置的布局文件其实就是被加到内容栏之中的,成为其惟一子View,就是上面的id为content的FrameLayout中,在代码中能够经过content来获得对应加载的布局。博客
    ViewGroup content = (ViewGroup)findViewById(android.R.id.content);
    ViewGroup rootView = (ViewGroup) content.getChildAt(0);
  • DecorView的职责是什么

    • 经过源码了解能够知道,Activity就像个控制器,不负责视图部分。Window像个承载器,装着内部视图。DecorView就是个顶层视图,是全部View的最外层布局。ViewRoot像个链接器,负责沟通,经过硬件的感知来通知视图,进行用户之间的交互。
  • DecorView如何被加载到Window中?博客

    • 从Activity中的setContentView()开始。在Activity中的attach()方法中,生成了PhoneWindow实例。既然有了Window对象,那么咱们就能够**设置DecorView给Window对象了。
    • 从中获取mContentParent。得到到以后,而后经过installDecor方法,而后生成DecorView,不过这里操做很复杂,大概流程先从主题中获取样式,而后根据样式,加载对应的布局到DecorView中,为mContentParent添加View,即Activity中的布局。
    • 具体能够看这篇文章:10.DecorView介绍

10.0.1.0 DecorView如何显示出来,为何setContentView()设置的界面,为何在onResume()以后才对用户可见呢?

  • 经过setContentView()设置的界面,为何在onResume()以后才对用户可见呢?这就要从ActivityThread开始提及。

    private void handleLaunchActivity(ActivityClientRecord r, Intent customIntent) {
    
        //就是在这里调用了Activity.attach()呀,接着调用了Activity.onCreate()和Activity.onStart()生命周期,
        //可是因为只是初始化了mDecor,添加了布局文件,尚未把
        //mDecor添加到负责UI显示的PhoneWindow中,因此这时候对用户来讲,是不可见的
        Activity a = performLaunchActivity(r, customIntent);
    
        ......
    
        if (a != null) {
        //这里面执行了Activity.onResume()
        handleResumeActivity(r.token, false, r.isForward,
                            !r.activity.mFinished && !r.startsNotResumed);
    
        if (!r.activity.mFinished && r.startsNotResumed) {
            try {
                    r.activity.mCalled = false;
                    //执行Activity.onPause()
                    mInstrumentation.callActivityOnPause(r.activity);
                    }
            }
        }
    }
  • 重点看下handleResumeActivity(),在这其中,DecorView将会显示出来,同时重要的一个角色:ViewRoot也将登场。

    • 这个方法里面会调用performResumeActivity方法,这个时候,Activity.onResume()已经调用了,可是如今界面仍是不可见的
    • 接着讲decorView添加进WindowManager了,可是这个时候,仍是不可见的
    • 最后执行makeVisible,执行了重要的操做,使得DecorView可见
  • 当咱们执行了Activity.makeVisible()方法以后,界面才对咱们是可见的。博客

    void makeVisible() {
       if (!mWindowAdded) {
            ViewManager wm = getWindowManager();
            wm.addView(mDecor, getWindow().getAttributes());//将DecorView添加到WindowManager
            mWindowAdded = true;
        }
        mDecor.setVisibility(View.VISIBLE);//DecorView可见
    }
    • 到此DecorView即可见,显示在屏幕中。可是在这其中,wm.addView(mDecor, getWindow().getAttributes());起到了重要的做用,由于其内部建立了一个ViewRootImpl对象,负责绘制显示各个子View。
  • 最后经过WindowManagerImpl的addView方法将DecorView加载出来

    • 看到其中实例化了ViewRootImpl对象,而后调用其setView()方法。其中setView()方法通过一些列折腾,最终调用了performTraversals()方法,而后依照下图流程层层调用,完成绘制,最终界面才显示出来。
    • img
    • 具体更加详细的过程,能够看10.DecorView介绍

10.0.1.1 什么是ViewRoot?ViewRoot属于View树的一份子吗?ViewRoot的工做流程是怎么样的?

  • 什么是ViewRoot

    • ViewRoot可能比较陌生,可是其做用很是重大。全部View的绘制以及事件分发等交互都是经过它来执行或传递的。
    • ViewRoot对应ViewRootImpl类,它是链接WindowManagerService和DecorView的纽带,View的三大流程(测量(measure),布局(layout),绘制(draw))均经过ViewRoot来完成。
  • ViewRoot属于View树的一份子吗?

    • ViewRoot并不属于View树的一份子。
    • 从源码实现上来看,它既非View的子类,也非View的父类,可是,它实现了ViewParent接口,这让它能够做为View的名义上的父视图。RootView继承了Handler类,能够接收事件并分发,Android的全部触屏事件、按键事件、界面刷新等事件都是经过ViewRoot进行分发的。博客
  • 下面结构图能够清晰的揭示四者之间的关系:

    • img

10.0.1.2 吐司为什么会出现内存泄漏?在Toast构造方法中建立NT对象是干什么用的?Toast是怎么show出来的?

  • 吐司为什么会出现内存泄漏

    • 缘由在于:若是在 Toast 消失以前,Toast 持有了当前 Activity,而此时,用户点击了返回键,致使 Activity 没法被 GC 销毁, 这个 Activity 就引发了内存泄露。
  • 在Toast构造方法中建立NT对象是干什么用的?

    • TN是属于Toast内部一个私有静态类,它是经过aidl进行通讯,主要做用是实现吐司的show和hide功能。
    • 在构造方法中,建立了NT对象,那么有人便会问,NT是什么东西呢?看看NT的源码,能够发现NT实现了ITransientNotification.Stub,提到这个感受是否是很熟悉,没错,在aidl中就会用到这个。

      public Toast(Context context) {
          mContext = context;
          mTN = new TN();
          mTN.mY = context.getResources().getDimensionPixelSize(
                  com.android.internal.R.dimen.toast_y_offset);
          mTN.mGravity = context.getResources().getInteger(
                  com.android.internal.R.integer.config_toastDefaultGravity);
      }
      • image
    • 在TN类中,能够看到,实现了AIDL的show与hide方法

      • TN是Toast内部的一个私有静态类,继承自ITransientNotification.Stub,ITransientNotification.Stub是出如今服务端实现的Service中,就是一个Binder对象,也就是对一个aidl文件的实现而已
      @Override
      public void show(IBinder windowToken) {
          if (localLOGV) Log.v(TAG, "SHOW: " + this);
          mHandler.obtainMessage(0, windowToken).sendToTarget();
      }
      
      @Override
      public void hide() {
          if (localLOGV) Log.v(TAG, "HIDE: " + this);
          mHandler.post(mHide);
      }
    • 接着看下这个ITransientNotification.aidl文件

      /** @hide */
      oneway interface ITransientNotification {
          void show();
          void hide();
      }
  • Toast是怎么show出来的?

    • 经过AIDL(Binder)通讯拿到NotificationManagerService的服务访问接口,而后把TN对象和一些参数传递到远程NotificationManagerService中去
    • 当 Toast在show的时候,而后把这个请求放在 NotificationManager 所管理的队列中,而且为了保证 NotificationManager 能跟进程交互,会传递一个TN类型的Binder对象给NotificationManager系统服
    • 而后经过service.enqueueToast方法,record是将Toast封装成ToastRecord对象,放入mToastQueue中。经过下面代码能够得知:经过isSystemToast判断是否为系统Toast。若是当前Toast所属的进程的包名为“android”,则为系统Toast。若是是系统Toast必定能够进入到系统Toast队列中,不会被黑名单阻止。

10.0.1.3 连续吐司是如何肯定吐司的前后顺序?为何Toast执行show后过了一下子就自动销毁?

  • 连续吐司是如何肯定吐司的前后顺序?

    • 主要是说一下showNextToastLocked()方法中的源代码

      • 首先获取吐司消息队列中第一个ToastRecord对象,而后判断该对象若是不为null的话,就开始经过callback进行show,且传递了token参数,注意这个show是通知进程显示。而后再调用scheduleTimeoutLocked(record)方法执行超时后自动取消的逻辑。同时须要注意的时,若是出现了异常,则会从吐司消息队列中移除该record……
      • 那么callback是干吗的呢,通常印象中callback是处理回调的?从ITransientNotification callback得知,这个callback哥们居然是是一个 ITransientNotification 类型的对象,也就是前面说到的TN的Binder代理对象。
    • 简而言之,也就是说,TN中的消息机制也是经过handler进行实现的。在show方法中发送消息,当mHandler接受到消息以后,就调用handleShow(token)处理逻辑,经过WindowManager将view添加进来,同时在该方法中也设置了大量的布局属性。
  • 为何Toast执行show后过了一下子就自动销毁?博客

    • 回调了Toast的TN的show,当timeout可能就是hide呢。分析NotificationManagerService源码中的showNextToastLocked()的scheduleTimeoutLocked(record)源码,能够知道在NotificationManagerService经过handler延迟delay时间发送消息,而后经过callback调用hide,因为callback是TN中Binder的代理对象, 因此即可以调用到TN中的hide方法达到销毁吐司的目的。
    • handleHide()源码以下所示,可知当销毁后先将view移除,而后在置空操做。
    public void handleHide() {
        if (localLOGV) Log.v(TAG, "HANDLE HIDE: " + this + " mView=" + mView);
        if (mView != null) {
            // note: checking parent() just to make sure the view has
            // been added...  i have seen cases where we get here when
            // the view isn't yet added, so let's try not to crash.
            if (mView.getParent() != null) {
                if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);
                mWM.removeViewImmediate(mView);
            }
    
            mView = null;
        }
    }

10.0.1.4 如何理解普通应用的Toast显示数量是有限制的?为何要判断是不是系统吐司?为什么Activity销毁后Toast仍会显示?

  • 如何理解普通应用的Toast显示数量是有限制的?

    • 如何判断是不是系统吐司呢?若是当前Toast所属的进程的包名为“android”,则为系统Toast,或者调用isCallerSystem()方法
    final boolean isSystemToast = isCallerSystem() || ("android".equals(pkg));
    • 接着看看isCallerSystem()方法源码,isCallerSystem的源码也比较简单,就是判断当前Toast所属进程的uid是否为SYSTEM_UID、0、PHONE_UID中的一个,若是是,则为系统Toast;若是不是,则不为系统Toast。
    private static boolean isUidSystem(int uid) {
        final int appid = UserHandle.getAppId(uid);
        return (appid == Process.SYSTEM_UID || appid == Process.PHONE_UID || uid == 0);
    }
    
    private static boolean isCallerSystem() {
        return isUidSystem(Binder.getCallingUid());
    }
  • 为何要判断是不是系统吐司?

    • 从源码可知:首先系统Toast必定能够进入到系统Toast队列中,不会被黑名单阻止。而后系统Toast在系统Toast队列中没有数量限制,而普通pkg所发送的Toast在系统Toast队列中有数量限制。
    • 那么关于数量限制这个结果从何而来,大概是多少呢?查看将要入队的Toast是否已经在系统Toast队列中。这是经过比对pkg和callback来实现的。经过下面源码分析可知:只要Toast的pkg名称和tn对象是一致的,则系统把这些Toast认为是同一个Toast。
    • 而后再看看下面这个源码截图,可知,非系统Toast,每一个pkg在当前mToastQueue中Toast有总数限制,不能超过MAX_PACKAGE_NOTIFICATIONS,也就是50
    • image
    • image
  • 为什么Activity销毁后Toast仍会显示

    • 记得之前昊哥问我,为什么toast在activity销毁后仍然会弹出呢,我绝不思索地说,由于toast是系统级别的呀。那么是如何实现的呢,我就无言以对呢……今天终于能够回答呢!
    • 仍是回到NotificationManagerService类中的enqueueToast方法中,直接查看keepProcessAliveIfNeededLocked(callingPid)方法。这段代码的意思是将当前Toast所在进程设置为前台进程,这里的mAm = ActivityManager.getService(),调用了setProcessImportant方法将当前pid的进程置为前台进程,保证不会系统杀死。这也就解释了为何当咱们finish当前Activity时,Toast还能够显示,由于当前进程还在执行。
    • image

10.0.1.5 为何说Toast尽可能用全局上下文?说一下Toast的显示和隐藏重点逻辑,说下你的理解?

  • 为何说Toast尽可能用全局上下文?

    • 在使用Toast时context参数尽可能使用getApplicationContext(),能够有效的防止静态引用致使的内存泄漏。
    • 有时候咱们会发现Toast弹出过多就会延迟显示,由于上面源码分析能够看见Toast.makeText是一个静态工厂方法,每次调用这个方法都会产生一个新的Toast对象,当咱们在这个新new的对象上调用show方法就会使这个对象加入到NotificationManagerService管理的mToastQueue消息显示队列里排队等候显示;因此若是咱们不每次都产生一个新的Toast对象(使用单例来处理)就不须要排队,也就能及时更新呢。
  • 说一下Toast的显示和隐藏重点逻辑,说下你的理解?博客

    • Toast调用show方法 ,其实就是是将本身归入到NotificationManager的Toast管理中去,期间传递了一个本地的TN类型或者是 ITransientNotification.Stub的Binder对象
    • NotificationManager 收到 Toast 的显示请求后,将生成一个 Binder 对象,将它做为一个窗口的 token 添加到 WMS 对象,而且类型是 TOAST
    • NotificationManager 将这个窗口token经过ITransientNotification的show方法传递给远程的TN对象,而且抛出一个超时监听消息 scheduleTimeoutLocked
    • TN 对象收到消息之后将往 Handler 对象中 post 显示消息,而后调用显示处理函数将 Toast 中的 View 添加到了 WMS 管理中,Toast窗口显示
    • NotificationManager的WorkerHandler收到MESSAGE_TIMEOUT消息, NotificationManager远程调用hide方法进程隐藏Toast 窗口,而后将窗口token从WMS中删除,而且判断吐司消息队列中是否还有消息,若是有,则继续吐司!

10.0.1.6 Toast报错Unable to add window是什么意思?Toast运行在子线程会问题,在子线程或者service中能运行吗?

  • Toast偶尔报错Unable to add window

    • 报错日志,是否是有点眼熟呀?更多能够看个人开源项目:https://github.com/yangchong211

      android.view.WindowManager$BadTokenException
          Unable to add window -- token android.os.BinderProxy@7f652b2 is not valid; is your activity running?
    • 查询报错日志是从哪里来的

      • image
    • 发生该异常的缘由

      • 这个异常发生在Toast显示的时候,缘由是由于token失效。一般状况下,通常是不会出现这种异常。可是因为在某些状况下, Android进程某个UI线程的某个消息阻塞。致使 TN 的 show 方法 post 出来 0 (显示) 消息位于该消息以后,迟迟没有执行。这时候,NotificationManager 的超时检测结束,删除了 WMS 服务中的 token 记录。删除 token 发生在 Android 进程 show 方法以前。这就致使了上面的异常。
      • 测试代码。模拟一下异常的发生场景,其实很容易,只须要这样作就能够出现上面这个问题
      Toast.makeText(this,"潇湘剑雨-yc",Toast.LENGTH_SHORT).show();
          try {
              Thread.sleep(20000);
          } catch (InterruptedException e) {
              e.printStackTrace();
          }
    • 解决办法,目前见过好几种,思考一下那种比较好……

      • 第一种,既然是报is your activity running,那能够不能够在吐司以前先判断一下activity是否running呢?
      • 第二种,抛出异常增长try-catch,代码以下所示,最后仍然没法解决问题

        • 按照源码分析,异常是发生在下一个UI线程消息中,所以在上一个ui线程消息中加入try-catch是没有意义的。并且用到吐司地方这么多,这样作也不方便啦!
      • 第三种,那就是自定义相似吐司Toast的view控件。我的建议除非要求很是高,否则不要这样作。毕竟发生这种异常仍是比较少见的
    • 哪些状况会发生该问题?

      • UI 线程执行了一条很是耗时的操做,好比加载图片等等,就相似上面用 sleep 模拟状况
      • 进程退后台或者息屏了,系统为了减小电量或者某种缘由,分配给进程的cpu时间减小,致使进程内的指令并不能被及时执行,这样同样会致使进程看起来”卡顿”的现象
      • 当TN抛出消息的时候,前面有大量的 UI 线程消息等待执行,而每一个 UI 线程消息虽然并不卡顿,可是总和若是超过了 NotificationManager 的超时时间,仍是会出现问题
  • Toast运行在子线程问题

    • 先来看看问题代码,会出现什么问题呢?
    new Thread(new Runnable() {
        @Override
        public void run() {
            ToastUtils.showRoundRectToast("潇湘剑雨-杨充");
        }
    }).start();
    • 报错日志以下所示:
    • image
  • 子线程中吐司的正确作法,代码以下所示

    new Thread(new Runnable() {
        @Override
        public void run() {
            Looper.prepare();
            ToastUtils.showRoundRectToast("潇湘剑雨-杨充");
            Looper.loop();
        }
    }).start();
  • 得出的结论

    • Toast也能够在子线程执行,不过须要手动提供Looper环境的。
    • Toast在调用show方法显示的时候,内部实现是经过Handler执行的,所以天然是不阻塞Binder线程,另外,若是addView的线程不是Loop线程,执行完就结束了,固然就没机会执行后续的请求,这个是由Hanlder的构造函数保证的。能够看看handler的构造函数,若是Looper==null就会报错,而Toast对象在实例化的时候,也会为本身实例化一个Hanlder,这就是为何说“必定要在主线程”,其实准确的说应该是 “必定要在Looper非空的线程”。博客

10.0.1.7 为何建议用DialogFragment替代Dialog?如何定义DialogFragment样式?使用dialogFragment有何好处?

  • 为何建议用DialogFragment替代Dialog

    • Android比较推荐采用DialogFragment实现对话框,它彻底可以实现Dialog的全部需求,而且还能复用Fragment的生命周期管理,被后台杀死后,能够恢复重建。
    • 在手机配置变化致使 Activity 须要从新建立时,例如旋转屏幕,基于 DialogFragment 的对话框将会由 FragmentManager 自动重建,然而基于 Dialog 实现的对话框却没有这样的能力。
  • 如何定义DialogFragment样式

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        if (local == BOTTOM) {
            setStyle(DialogFragment.STYLE_NO_TITLE, R.style.BottomDialog);
        } else if (local == CENTER || local == TOP) {
            setStyle(DialogFragment.STYLE_NO_TITLE, R.style.CenterDialog);
        }
    }
  • 建立theme主题样式,而且进行设置

    • 设置样式,以DialogFragment为例,只须要在onCreate中setStyle(DialogFragment.STYLE_NO_TITLE, R.style.CenterDialog)便可。
    • 注意,CenterDialog中能够设置弹窗的动画效果。
    • 注意一下style常量,这里只是展现经常使用的。

      STYLE_NORMAL:会显示一个普通的dialog
      STYLE_NO_TITLE:不带标题的dialog
      STYLE_NO_FRAME:无框的dialog
      STYLE_NO_INPUT:没法输入内容的dialog,即不接收输入的焦点,并且触摸无效。
    • 注意动画设置以下所示

      <style name="CenterDialog" parent="@android:style/Theme.Dialog">
          <item name="android:windowTitleStyle">@null</item>
          <item name="android:windowBackground">@android:color/transparent</item>
          <item name="android:colorBackgroundCacheHint">@null</item>
          <item name="android:windowAnimationStyle">@style/CenterDialogAnimationStyle</item>
          <item name="android:windowSoftInputMode">stateUnspecified|adjustPan</item>
      </style>
  • 使用dialogFragment有何好处?

    • DialogFragment是继承Fragment,具备Fragment的生命周期,本质上说就是Fragment,只是其内部还有一个dialog而已。你既能够当它是Dialog使用,也能够把它做为Fragment使用。
    • onCreateView能够加载客户化更高的对话框,onCreateDialog加载系统AlertDialog类型对话框比较合适。
    • DialogFragmnet对话框横屏时对话框不会关闭,由于DailogFragment有Fragment属性,会在屏幕发生变化时从新建立DialogFragment。博客
    • setStyle的调用点,要放在onCreateView前,通常是放在onCreat方法中执行,不然,设置的style和theme将不起做用!setStyle中,style的参数是不能够相互一块儿使用的,只能用一个,若是还不知足你使用,能够经过设置theme来知足。

10.0.1.8 Dialog的Window建立过程是怎样的?为何Dialog不能用Application的Context,说一下缘由?

  • Dialog的Window建立过程是怎样的?

    • 建立Window——一样是经过PolicyManager的makeNewWindow方法完成,与Activity建立过程一致
    • 初始化DecorView并将Dialog的视图添加到DecorView中——和Activity一致(setContentView)
    • 将DecorView添加到Window中并显示——在Dialog的show方法中,经过WindowManager将DecorView添加到Window中(mWindowManager.addView(mDecor, 1))
    • Dialog关闭时会经过WindowManager来移除DecorView:mWindowManager.removeViewImmediate(mDecor)
    • Dialog必须采用Activity的Context,由于有应用token(Application的Context没有应用token),也能够将Dialog的Window经过type设置为系统Window就再也不须要token。
  • 为何Dialog不能用Application的Context,说一下缘由?

    • Dialog自己的Token为null,在初始化时若是是使用Application或者Service的Context,在获取到WindowManager时,获取到的token依然是null。
    • Dialog若是采用Activity的Context,获取到的WindowManager是在activity.attach()方法中建立,token指向了activity的token。
    • 由于经过Application和Service的Context将没法获取到Token从而致使失败。

10.0.1.9 Dialog和Window有什么关系?Dialog的dismiss和cancel()方法均可销毁弹窗,它们有什么区别?

  • Dialog和Window有什么关系?

    • 看源码可知在Dialog的构造方法中直接直接构造了一个PhoneWindow,并赋值给Dialog的成员变量mWindow,从这里能够看出其实Dialog和Activity的显示逻辑都是相似的,都是经过对应的Window变量来实现窗口的加载与显示的。而后咱们执行了一些Window对象的初始化操做,好比设置回调函数为自己,而后调用Window类的setWindowManager方法,并传入了WindowManager。而后建立一个对话框监听handler对象。
    Dialog(@NonNull Context context, @StyleRes int themeResId, boolean createContextThemeWrapper) {
        if (createContextThemeWrapper) {
            if (themeResId == 0) {
                final TypedValue outValue = new TypedValue();
                context.getTheme().resolveAttribute(R.attr.dialogTheme, outValue, true);
                themeResId = outValue.resourceId;
            }
            //建立一个Context
            mContext = new ContextThemeWrapper(context, themeResId);
        } else {
            mContext = context;
        }
    
        //获取一个WindowManager对象
        mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
        //建立一个Window对象
        final Window w = new PhoneWindow(mContext);
        //将Window对象w赋值给mWindow
        mWindow = w;
        //为Windowd对象设置回调,而且它自己实现了这些回调函数
        w.setCallback(this);
        w.setOnWindowDismissedCallback(this);
        //为Window对象设置WindowManager对象
        w.setWindowManager(mWindowManager, null, null);
        w.setGravity(Gravity.CENTER);
        //建立一个对话框监听Handler
        mListenersHandler = new ListenersHandler(this);
    }
  • Dialog的dismiss和cancel()方法均可销毁弹窗,它们有什么区别?

    • 调用alertDialog.cancel()或者alertDialog.dismiss()均可以达到销毁弹窗的效果。
    • 若是没有设置setOnCancelListener事件,则两个方法是等效的。为何这样说呢?
    • 首先看一下Dialog的cannel方法的具体实现:能够看到方法体中,若当前Dialog没有取消,而且设置了取消message,则调用Message.obtain(mCancelMessage).sendToTarget()。而这个mCancelMessage则是在setOnCancelListener方法中建立的。调用的是设置的OnCancelListener的onCancel方法,也就是说调用dialog.cancel方法时首先会判断dialog是否调用了setOnCancelListener若设置了,则先调用OnCancelListener的onCancel方法,而后再次执行dismiss方法,若咱们没有为Dialog.Builder设置OnCancelListener那么cancel方法和dismiss方法是等效的。博客
    public void cancel() {
        if (!mCanceled && mCancelMessage != null) {
            mCanceled = true;
            // Obtain a new message so this dialog can be re-used
            Message.obtain(mCancelMessage).sendToTarget();
        }
        dismiss();
    }
    
    public void setOnCancelListener(final OnCancelListener listener) {
        if (listener != null) {
            mCancelMessage = mListenersHandler.obtainMessage(CANCEL, listener);
        } else {
            mCancelMessage = null;
        }
    }
    
    private static final class ListenersHandler extends Handler {
        private WeakReference<DialogInterface> mDialog;
    
        public ListenersHandler(Dialog dialog) {
            mDialog = new WeakReference<DialogInterface>(dialog);
        }
    
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case DISMISS:
                    ((OnDismissListener) msg.obj).onDismiss(mDialog.get());
                    break;
                case CANCEL:
                    ((OnCancelListener) msg.obj).onCancel(mDialog.get());
                    break;
                case SHOW:
                    ((OnShowListener) msg.obj).onShow(mDialog.get());
                    break;
            }
        }
    }
  • dismiss方法主要是作了什么?

    • 能够看到,这里首先判断当前线程的Looper是不是主线程的Looper(因为mHandler是在主线程中建立的,因此mHandler.getLooper返回的是主线程中建立的Looper对象),如果的话,则直接执行dismissDialog()方法,不然的话,经过mHandler发送异步消息至主线程中,简单来讲就是判断当前线程是不是主线程,如果主线程则执行dismissDialog方法不然发送异步消息。而这里的异步消息最终也是调用的dismissDialog方法
    public void dismiss() {
        if (Looper.myLooper() == mHandler.getLooper()) {
            dismissDialog();
        } else {
            mHandler.post(mDismissAction);
        }
    }

10.0.2.0 PopupWindow中不设置为何必须设置宽高?PopupWindow和Dialog有什么区别?说下建立和销毁的大概流程?

  • PopupWindow中不设置为何必须设置宽高?

    • 先看问题代码,下面这个不会出现弹窗,思考:为何?

      PopupWindow popupWindow = new PopupWindow(this);
      View inflate = LayoutInflater.from(this).inflate(R.layout.view_pop_custom, null);
      popupWindow.setContentView(inflate);
      popupWindow.setAnimationStyle(R.style.BottomDialog);
      popupWindow.showAsDropDown(mTv1);
    • 注意:必须设置宽和高,不然不显示任何东西

      • 这里的WRAP_CONTENT能够换成fill_parent 也能够是具体的数值,它是指PopupWindow的大小,也就是contentview的大小,注意popupwindow根据这个大小显示你的View,若是你的View自己是从xml获得的,那么xml的第一层view的大小属性将被忽略。至关于popupWindow的width和height属性直接和第一层View相对应。
  • PopupWindow和Dialog有什么区别?

    • 二者最根本的区别在于有没有新建一个window,PopupWindow没有新建,而是将view加到DecorView;Dialog是新建了一个window,至关于走了一遍Activity中建立window的流程
    • 从源码中能够看出,PopupWindow最终是执行了mWindowManager.addView方法,全程没有新建window
  • 说下建立和销毁的大概流程?

    • 建立PopupWindow的时候,先建立WindowManager,由于WIndowManager拥有控制view的添加和删除、修改的能力。这一点关于任主席的艺术探索书上写的很详细……博客
    • 而后是setContentView,保存contentView,这个步骤就作了这个
    • 显示PopupWindow,这个步骤稍微复杂点,建立并初始化LayoutParams,设置相关参数,做为之后PopupWindow在应用DecorView里哪里显示的凭据。而后建立PopupView,而且将contentView插入其中。最后使用WindowManager将PopupView添加到应用DecorView里。
    • 销毁PopupView,WindowManager把PopupView移除,PopupView再把contentView移除,最后把对象置为null
  • 为什么弹窗点击一下就dismiss呢?

    • PopupWindow经过为传入的View添加一层包裹的布局,并重写该布局的点击事件,实现点击PopupWindow以外的区域PopupWindow消失的效果

10.0.2.1 Snackbar与吐司有何区别在哪里?Snackbar控件show时为什么从下往上移出来?为何显示在最下面?

  • Snackbar与吐司有何区别

    • 与Toast进行比较,SnackBar有优点:
    • 1.SnackBar能够自动消失,也能够手动取消(侧滑取消,可是须要在特殊的布局中,后面会仔细说)
    • 2.SnackBar能够经过setAction()来与用户进行交互
    • 3.经过CallBack咱们能够获取SnackBar的状态
    • image
  • Snackbar控件show时为什么从下往上移出来?

    • 至于说Snackbar控件show时为什么从下往上移出来,看下面这段代码就知道呢,以下所示
    • image
  • 为何显示在最下面?

    • 直接找到make方法中的填充布局,而后去看design_layout_snackbar_include的布局参数,结果以下:
    • image
  • Snackbar显示会致使FloatingActionButton上移?

    • 为何CoordinatorLayout + FloatingActionButton,当Snackbar显示的时候FloatingActionButton会上移呢,这个是怎么实现的?
    • 把CoordinatorLayout替换成FrameLayout确不行。这个问题咱们还没说。其实这个不是在Snackbar里面处理的,是经过CoordinatorLayout和Behavior来处理的。那具体的处理在哪里呢。FloatingActionButton类里面Behavior类。正是Behavior里面的两个函数layoutDependsOn()和onDependentViewChanged()函数做用的结果。直接进去看下FloatingActionButton内部类Behavior里面这两个函数的代码。

10.0.2.2 说一下Snackbar和SnackbarManager类的设计有哪些奥妙的地方,如何处理消息的显示顺序?

  • Snackbar和SnackbarManager,SnackbarManager内部有两个SnackbarRecord,一个mCurrentSnackbar,一个mNextSnackbar,SnackbarManager经过这两个对象实现Snackbar的顺序显示,若是在一个Snackbar显示以前有Snackbar正在显示,那么使用mNextSnackbar保存第二个Snackbar,而后让第一个Snackbar消失,而后消失以后再调用SnackbarManager显示下一个Snackbar,如此循环,实现了Snackbar的顺序显示。 博客
  • Snackbar负责显示和消失,具体来讲其实就是添加和移除View的过程。Snackbar和SnackbarManager的设计很巧妙,利用一个SnackbarRecord对象保存Snackbar的显示时间以及SnackbarManager.Callback对象,前面说到每个Snackbar都有一个叫作mManagerCallback的SnackbarManager.Callback对象,下面看一下SnackRecord类的定义:

    • image
  • Snackbar向SnackbarManager发送消息主要是调用SnackbarManager.getInstace()返回一个单例对象;而SnackManager向Snackbar发送消息就是经过show方法传入的Callback对象。SnackbarManager中的Handler只处理一个MSG_TIMEOUT事件,最后是调用Snackbar的hideView消失的;Snackbar的sHandler处理两个消息,showView和hideView,而消息的发送者是mManagerCallback,控制者是SnackbarManager。

其余介绍

01.关于博客汇总连接

02.关于个人博客

弹窗开源库地址:https://github.com/yangchong2...

笔记开源库地址:https://github.com/yangchong2...

相关文章
相关标签/搜索