Toast源码解析

子线程中的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) {
// Empty
}
}

可以看出这里需要得到一个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
{
/** Local-side IPC implementation stub class. */
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();
// Don't do this in handleHide() because it is also invoked by handleShow()
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) {
// remove the old view if necessary
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);
// We can resolve the Gravity here by using the Locale for getting
// the layout direction
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) {
// 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;
}
}

也是通过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。