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

来源:互联网 发布:ios10屏幕录制软件 编辑:程序博客网 时间:2024/04/29 15:01

问题引入:当我们弹出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()");    }    // 把文本直接赋值到 <span style="font-family: Arial, Helvetica, sans-serif;">R.id.</span><span style="font-family: Arial, Helvetica, sans-serif;">message 对应的TextView上面。</span>    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();// 把 该对象 赋值为 nullmNextView = 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没有显示的根本原因。

0 0
原创粉丝点击