子线程中的Toast 在写代码的时候发现一个现象,在子线程中使用Toast会crash,错误如下
1 java.lang.RuntimeException: Can't create handler inside thread that has not called Looper.prepare()
可以很明显的看出问题出在当前线程企图创建Handler,但是由于本线程没有Looper所以crash了,这时候我不禁对Toast的实现原理产生兴趣,接下来就一步一步的分析源码。
Toast的创建 在日常使用中都是使用Toast.makeText()来创建一个Toast,代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public static Toast makeText (Context context, CharSequence text, @Duration int duration) { Toast result = new Toast (context); LayoutInflater inflate = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 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); result.mNextView = v; result.mDuration = duration; return result; }
其实很简单,就是设置了mNextView和mDuration,创建一个TextView然后设置传入的String就完成mNextView的设置,mDuration参数被@Duration注解标记,注解如下
1 2 3 4 5 6 @IntDef({LENGTH_SHORT, LENGTH_LONG}) @Retention(RetentionPolicy.SOURCE) public @interface Duration {}public static final int LENGTH_SHORT = 0 ;public static final int LENGTH_LONG = 1 ;
@IntDef限制了只能传入两个给定的int,也就是说我们只能设置显示时间的长短,而无法设置具体的时长。
Toast.show() show()方法源码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public void show () { if (mNextView == null ) { throw new RuntimeException ("setView must have been called" ); } INotificationManager service = getService(); String pkg = mContext.getOpPackageName(); TN tn = mTN; tn.mNextView = mNextView; try { service.enqueueToast(pkg, tn, mDuration); } catch (RemoteException e) { } }
可以看出这里需要得到一个INotificationManager的服务,传入了TN的实例,首先我们先看看TN是什么东西
1 2 3 private static class TN extends ITransientNotification .Stub { ... }
查看ITransientNotification的代码
1 2 3 4 5 6 7 8 9 10 public interface ITransientNotification extends android .os.IInterface{ public static abstract class Stub extends android .os.Binder implements android .app.ITransientNotification{ ... public void show () throws android.os.RemoteException;public void hide () throws android.os.RemoteException;}
可以看出这就是一个AIDL的接口,有show()和hide()两个办法,可以猜测主要用于远程服务来控制Toast显示和隐藏的。我们就看看show和hide的具体实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 final Runnable mHide = new Runnable () { @Override public void run () { handleHide(); mNextView = null ; } }; final Handler mHandler = new Handler () { @Override public void handleMessage (Message msg) { IBinder token = (IBinder) msg.obj; handleShow(token); } }; @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); }
这里就用到了Handler,通过Handler实现show和hide,这也就解释了为什么子线程会报错。show()最终调用handlerShow()方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 public void handleShow (IBinder windowToken) { if (localLOGV) Log.v(TAG, "HANDLE SHOW: " + this + " mView=" + mView + " mNextView=" + mNextView); if (mView != mNextView) { handleHide(); mView = mNextView; Context context = mView.getContext().getApplicationContext(); String packageName = mView.getContext().getOpPackageName(); if (context == null ) { context = mView.getContext(); } mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE); final Configuration config = mView.getContext().getResources().getConfiguration(); final int gravity = Gravity.getAbsoluteGravity(mGravity, config.getLayoutDirection()); mParams.gravity = gravity; if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.FILL_HORIZONTAL) { mParams.horizontalWeight = 1.0f ; } if ((gravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.FILL_VERTICAL) { mParams.verticalWeight = 1.0f ; } mParams.x = mX; mParams.y = mY; mParams.verticalMargin = mVerticalMargin; mParams.horizontalMargin = mHorizontalMargin; mParams.packageName = packageName; mParams.hideTimeoutMilliseconds = mDuration == Toast.LENGTH_LONG ? LONG_DURATION_TIMEOUT : SHORT_DURATION_TIMEOUT; mParams.token = windowToken; if (mView.getParent() != null ) { if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this ); mWM.removeView(mView); } if (localLOGV) Log.v(TAG, "ADD! " + mView + " in " + this ); mWM.addView(mView, mParams); trySendAccessibilityEvent(); } }
可以看出主要就是通过WindowManager来addView显示Toast。hide()最终调用handleHide方法实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public void handleHide () { if (localLOGV) Log.v(TAG, "HANDLE HIDE: " + this + " mView=" + mView); if (mView != null ) { if (mView.getParent() != null ) { if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this ); mWM.removeViewImmediate(mView); } mView = null ; } }
也是通过WindowManager来removevView。
Toast的时长 我们在使用Toast的时候只能穿入LENGTH_LONG活着LENGTH_SHORT两个变量,而具体的时间在handleShow()的代码中可以发现
1 2 mParams.hideTimeoutMilliseconds = mDuration == Toast.LENGTH_LONG ? LONG_DURATION_TIMEOUT : SHORT_DURATION_TIMEOUT;
1 2 static final long SHORT_DURATION_TIMEOUT = 5000 ;static final long LONG_DURATION_TIMEOUT = 1000 ;
可以看出长短时间分别对应5秒和1秒
为何使用AIDL而不是自己控制显示和隐藏 不知道大家有没有发现,在显示两个Toast的时候,总是第一个显示完毕才会显示第二个,如果让Toast自己控制,那么是很难实现这样的效果的,它并不知道其他Toast的状态,所以所有Toast交由系统同意管理,通过队列来依次显示Toast,并会按照设置的时间来hide Toast。
如何自己控制时间 我们可以跳过AIDL,获取NT对象,直接调用show和hide方法,但是NT对象构造方法是私有的,我们可以通过反射来解决,当然也可以反射设置time,然后正常show。