技术记录---Toast频繁弹出问题及其流程分析

问题引入:当我们弹出toast的时候,一般会

Toast.makeText(context, message, Toast.LENGTH_SHORT).show();

可是当我们频繁点击按钮的时候,就会频繁触发上面代码。这样 toast 就会在很长的一段时间才能消亡。


解决方案:只要避免 toast 频繁创建即可。
1、我们可以加一个时间限制,比如在2s(LENGTH_SHORT)时间内,不响应事件,不重复执行代码。
2、定义一个全局 Toast的对象mToast,这样频繁点击时候,直接用同一个对象。
方法1,不要解释,不是本文重点。方法2,当你执行如下代码的时候,就会发现“没有任何现象”,toast 完全不弹出。

mToast.cancel();//取消上次
mToast.setText("new message");//设置新内容
mToast.show();// 再次呈现

但是当代码改成如下时,就可以正常弹出,而且频繁点击时只显示一个:

//mToast.cancel();
mToast.setText("new message");
mToast.show();
不管,你怎么疯狂点击,当停止以后,2s内就会消失。那么肯定心中有疑问:
1、为什么全局的 mToast 可以重复使用(当其自然消失后,还可以show)?
2、为什么在show之前加上“cancel”,就不显示了呢?


深入研究:从源码层次下,逐步分析。

1、创建 toast的时候

  public static Toast makeText(Context context, CharSequence text, int duration) {
        Toast result = new Toast(context);// 创建一个对象,并在构造函数里面实例化TN( 访问远程服务时候用到的callback函数)
       //加载view
       View v = inflate.inflate(com.android.internal.R.layout.transient_notification, null);
        TextView tv = (TextView)v.findViewById(com.android.internal.R.id.message);
        tv.setText(text);
        //保存view
        result.mNextView = v;
        result.mDuration = duration;

        return result;
    }
2、在setText方法里面进行赋值
public void setText(CharSequence s) {
    if (mNextView == null) {
        throw new RuntimeException("This Toast was not created with Toast.makeText()");
    }
    TextView tv = (TextView) mNextView.findViewById(com.android.internal.R.id.message);
    if (tv == null) {
        throw new RuntimeException("This Toast was not created with Toast.makeText()");
    }
    // 把文本直接赋值到 R.id.message 对应的TextView上面。
    tv.setText(s);
}
3、然后show方法源码
public void show() {
    if (mNextView == null) {
        throw new RuntimeException("setView must have been called");
    }

    //NotificationManagerService 服务
    INotificationManager service = getService();
    //和应用相关联,这样消息队列方便管理,每个应用最多显示50个 toast
    String pkg = mContext.getPackageName();
    //callback 用于处理服务器调度,比如 开始显示,隐藏等操作。
    TN tn = mTN;
    tn.mNextView = mNextView;
    // 加入 toast队列,等待被调度
    try {
        service.enqueueToast(pkg, tn, mDuration);
    } catch (RemoteException e) {
        // Empty
    }
}
4、下面跟踪到 NotificationManagerService 源码的 enqueueToast方法
public void enqueueToast(String pkg, ITransientNotification callback, int duration)
    {
        .....
        synchronized (mToastQueue) {
            int callingPid = Binder.getCallingPid();
            long callingId = Binder.clearCallingIdentity();
            try {
                ToastRecord record;
                int index = indexOfToastLocked(pkg, callback);
				//如果队列中已经存在,那么就直接取出来,并更新数值。
                if (index >= 0) {
                    record = mToastQueue.get(index);
                    record.update(duration);
                } else {
                      //不存在,走创建流程,保存到队列中去。
                           .........
                      record = new ToastRecord(callingPid, pkg, callback, duration);
                      mToastQueue.add(record);
                }
                if (index == 0) {
		    //调度下一个 toast
                    showNextToastLocked();
                }
            } finally {
                Binder.restoreCallingIdentity(callingId);
            }
        }
    }
这也就解释了,为什么一个全局的对象toast可以频繁使用,因为当队列中存在该 toast的时候,直接进行了 更新操作,从头开始计时;当不存在的时候,就会走创建流程,然后 通过方法 showNextToastLocked 进行调度,进行显示。
5、showNextToastLocked 方法
  private void showNextToastLocked() {
     //从队列中取出第一个toast
      ToastRecord record = mToastQueue.get(0);
      while (record != null) {
          try {
              //调用toast的 TN对象
              record.callback.show();
              scheduleTimeoutLocked(record);
              return;
          } catch (RemoteException e) {
             ......
          }
      }
  }
6、然后我们回到 Toast 对象的TN内部类中的show方法,改方法最终会调用handleShow方法。
public void handleShow() {
    //mView 是TN类的内部对象,保留是最近一次引用的view,初始为null
    //mNextView是TN类的内部对象,引用的是Toast中的mNextView,在 toast的show方法中赋值。
    if (mView != mNextView) {
        // remove the old view if necessary
        handleHide();
        // 此处对mView进行赋值,表示该 toast 的view已经显示。
        mView = mNextView;
        mWM = (WindowManager)mView.getContext().getApplicationContext()
                .getSystemService(Context.WINDOW_SERVICE);
        ..............
        // 先进行remove操作
        if (mView.getParent() != null) {
            if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);
            mWM.removeView(mView);
        }
        //然后通过 WindowManager 添加到 系统中,进行toast的显示。
        mWM.addView(mView, mParams);
        trySendAccessibilityEvent();
    }
}
7、当时间到达,或者 调用 cancel方法的时候,会调用 TN的hide方法

handleHide();
// 把 该对象 赋值为 null
mNextView = null;
handleHide方法里面的源码如下:

 public void handleHide() {
    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.removeView(mView);
        }
        //重置 mView。
        mView = null;
    }
}
8、下面当用户调用Toast的cancel方法时候
public void cancel() {
    //调用TN的hide方法,如第7步中代码所示。
    mTN.hide();
    //调用 NotificationManagerService 服务移除toast队列
    try {
        getService().cancelToast(mContext.getPackageName(), mTN);
    } catch (RemoteException e) {
        // Empty
    }
}
总结:
1、问题1的答案在于,当我们频繁对一个对象进行 show的时候,如果第一次创建就走add操作,之后的就走 udpate操作,这样就解释了,我们频繁点击的时候全局对象mToast只显示一次的原因,而且当 toast消失后,我们还可以通过 show方法再次重复调用起来。
2、问题2的解释为:当我们调用 cancel的时候,会把 mView 和 mNextView赋值为null,这样,当我们再次调用 Toast的show方法时候,由于不满足if (mView != mNextView) 条件,而不被执行,也就解释了先cancel后show, toast没有显示的根本原因。

你可能感兴趣的:(技术记录---Toast频繁弹出问题及其流程分析)