Toast源码学习
Toast的作用主要是快速的展示相关信息给用户,同时又不占据太多屏幕位置,也不夺取焦点,更重要的时其调用非常简单,一行代码就可以实现。所以学习一下Toast的源码还是很有必要的。
Toast的源码在这个位置:
frameworks\base\core\java\android\widget\Toast.java
先看我们常用的调用方式
Toast.makeText(context,"Hello ",Toast.LENGTH_SHORT).show();
一段非常简洁的链式调用代码,一个方法就是设置内容和时长,第二个方法就是显示。
我们先从第一个方法入手。用过的都知道makeText有一个重载方法,主要就是所传内容的参数类型不同,一个可以直接传入字符串,一个则可以传入资源ID,如下:
public static Toast makeText(Context context, CharSequence text, @Duration int duration) {
...
}
public static Toast makeText(Context context, @StringRes int resId, @Duration int duration)
throws Resources.NotFoundException {
...
}
但是从8.0开始,又多了一个重载方法,如下:
public static Toast makeText(@NonNull Context context, @Nullable Looper looper,
@NonNull CharSequence text, @Duration int duration) {
}
可以看到多了一个Looper参数,用过的朋友都知道Toast是不可以直接在子线程调用的,否则会有如下错误:
Can't toast on a thread that has not called Looper.prepare()
具体原因后面在分析,根据其提示信息,我们可以分别调用 Looper.prepare()和 Looper.loop()来实现在子线程使用Toast。但这样比较麻烦,所以8.0中又多了一个这样的方法,可以直接传入一个主线程的looper,然后在子线程调用Toast。但是这个方法目前是hide状态的,我们无法直接调用,为了试试这个方法,我们可以通过反射调用一下,看看效果。
final Class clazz = Class.forName("android.widget.Toast");
final Method method = clazz.getMethod("makeText", Context.class, Looper.class,CharSequence.class,int.class);
new Thread(){
@Override
public void run() {
super.run();
try {
Toast toast = (Toast) method.invoke(null,context,Looper.getMainLooper(),"Hello",0);
toast.show();
} catch (Exception e) {
Log.d("MTC",e.getMessage());
}
}
}.start();
结果证明是可以正常调用的,至于为什么要隐藏这个方法,目前还不清楚。
现在我们开始看这个方法的具体实现:
public static Toast makeText(Context context, CharSequence text, @Duration int duration) {
return makeText(context, null, text, duration);
}
public static Toast makeText(Context context, @StringRes int resId, @Duration int duration)
throws Resources.NotFoundException {
return makeText(context, context.getResources().getText(resId), duration);
}
public static Toast makeText(@NonNull Context context, @Nullable Looper looper,
@NonNull CharSequence text, @Duration int duration) {
Toast result = new Toast(context, looper);
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;
}
可以看到前两个makeText方法都在间接调用第三个重载,只是默认将looper方法设为null而已。在第三个重载中,主要做了这几件事,1.构造一个Toast对象。2.引入一个布局并且给Textview设置内容。3.设置显示时间。
相应的在Toast中也提供了一些get,set方法来获取和设置布局和文字。如:setView,getView,setText。注意没有getText方法。其中有setDuration方法,但是并不是想象中那样自定义事件,而是只能设置为LENGTH_SHORT和LENGTH_LONG两种。对应的也有getDuration。
我们可以简单看一下这个布局文件,布局很简单,就是一个TextView
frameworks\base\core\res\res\layout\transient_notification.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="?android:attr/toastFrameBackground">
<TextView
android:id="@android:id/message"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginHorizontal="24dp"
android:layout_marginVertical="15dp"
android:layout_gravity="center_horizontal"
android:textAppearance="@style/TextAppearance.Toast"
android:textColor="@color/primary_text_default_material_light"
/>
</LinearLayout>
接下来看一下Toast的构造方法:
public Toast(Context context) {
this(context, null);
}
public Toast(@NonNull Context context, @Nullable Looper looper) {
mContext = context;
mTN = new TN(context.getPackageName(), looper);
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);
}
同样,相对于之前版本的代码,8.0中也多出了一个带Looper参数的构造函数,同样这个构造函数也是隐藏的。第一个构造也是调用第二个构造。在构造函数中,初始化了Context变量,构造了一个TN对象,并设置TN对象中的一些参数。我们浏览Toast一些布局设置的方法时发现,比如setGravity,setMargin等,都是间接的设置给了TN,说明TN是用来控制Toast的。我们就来看一下这个类。
它是Toast里的一个静态内部类,父类是ITransientNotification,构造函数如下:
TN(String packageName, @Nullable Looper looper) {
final WindowManager.LayoutParams params = mParams;
params.height = WindowManager.LayoutParams.WRAP_CONTENT;
params.width = WindowManager.LayoutParams.WRAP_CONTENT;
params.format = PixelFormat.TRANSLUCENT;
params.windowAnimations = com.android.internal.R.style.Animation_Toast;
params.type = WindowManager.LayoutParams.TYPE_TOAST;
params.setTitle("Toast");
params.flags = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
| WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
| WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
mPackageName = packageName;
if (looper == null) {
// Use Looper.myLooper() if looper is not specified.
looper = Looper.myLooper();
if (looper == null) {
throw new RuntimeException(
"Can't toast on a thread that has not called Looper.prepare()");
}
}
mHandler = new Handler(looper, null) {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case SHOW: {
IBinder token = (IBinder) msg.obj;
handleShow(token);
break;
}
case HIDE: {
handleHide();
// Don't do this in handleHide() because it is also invoked by
// handleShow()
mNextView = null;
break;
}
case CANCEL: {
handleHide();
// Don't do this in handleHide() because it is also invoked by
// handleShow()
mNextView = null;
try {
getService().cancelToast(mPackageName, TN.this);
} catch (RemoteException e) {
}
break;
}
}
}
};
}
在构造中,先是设置许多布局属性,在这里我们看到在设置flag时设置为不可触摸和不可获得焦点。然后设置looper变量,如果使用不带Looper参数的makeText方法,这里的looper会用Looper.myLooper()方法初始化,这也就是在子线程中,myLooper()返回为空,导致报错的原因。这个looper使用在Handler中的,所以不能为空。之后实例化了一个Handler对象,用于处理show,hide和cancel动作。
在TN里面也定义了显示的时长:
static final long SHORT_DURATION_TIMEOUT = 4000;
static final long LONG_DURATION_TIMEOUT = 7000;
我们也看到TN里的show,hide,cancel方法也都是通过mHandler传递消息,在Handler对象调用对应方法实现的。
@Override
public void show(IBinder windowToken) {
if (localLOGV) Log.v(TAG, "SHOW: " + this);
mHandler.obtainMessage(SHOW, windowToken).sendToTarget();
}
@Override
public void hide() {
if (localLOGV) Log.v(TAG, "HIDE: " + this);
mHandler.obtainMessage(HIDE).sendToTarget();
}
public void cancel() {
if (localLOGV) Log.v(TAG, "CANCEL: " + this);
mHandler.obtainMessage(CANCEL).sendToTarget();
}
我们首先看一下handleShow方法:
public void handleShow(IBinder windowToken) {
if (localLOGV) Log.v(TAG, "HANDLE SHOW: " + this + " mView=" + mView
+ " mNextView=" + mNextView);
if (mHandler.hasMessages(CANCEL) || mHandler.hasMessages(HIDE)) {
return;
}
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);
try {
mWM.addView(mView, mParams);
trySendAccessibilityEvent();
} catch (WindowManager.BadTokenException e) {
/* ignore */
}
}
}
可以看出还是很简单的,主要是设置窗口参数,然后通过WindowManager添加视图即可。由此可以想到hide就是通过WindowManager移除视图,具体看代码:
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;
}
}
这里总结一下,Toast的makeText方法只是实例化出来一个Toast对象和TN对象,其中Toast只是用来提供接口让我们设置各种参数,TN则是实际上用来控制Toast的显示隐藏及布局等操作。
最后看一下Toast的show方法:
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对象,然后将显示toast的请求加入队列,等待显示。
INotificationManager的源码在以下位置:
frameworks\base\services\core\java\com\android\server\notification\NotificationManagerService.java
由于Toast的显示和隐藏是由INotificationManager管理的,所以我们具体看一下相关的几个方法。
@Override
public void enqueueToast(String pkg, ITransientNotification callback, int duration)
{
if (pkg == null || callback == null) {
Slog.e(TAG, "Not doing toast. pkg=" + pkg + " callback=" + callback);
return ;
}
final boolean isSystemToast = isCallerSystemOrPhone() || ("android".equals(pkg));
final boolean isPackageSuspended =
isPackageSuspendedForUser(pkg, Binder.getCallingUid());
if (ENABLE_BLOCKED_TOASTS && !isSystemToast &&
(!areNotificationsEnabledForPackage(pkg, Binder.getCallingUid())
|| isPackageSuspended)) {
return;
}
synchronized (mToastQueue) {
int callingPid = Binder.getCallingPid();
long callingId = Binder.clearCallingIdentity();
try {
ToastRecord record;
int index;
if (!isSystemToast) {
index = indexOfToastPackageLocked(pkg);
} else {
index = indexOfToastLocked(pkg, callback);
}
if (index >= 0) {
record = mToastQueue.get(index);
record.update(duration);
record.update(callback);
} else {
Binder token = new Binder();
mWindowManagerInternal.addWindowToken(token, TYPE_TOAST, DEFAULT_DISPLAY);
record = new ToastRecord(callingPid, pkg, callback, duration, token);
mToastQueue.add(record);
index = mToastQueue.size() - 1;
}
keepProcessAliveIfNeededLocked(callingPid);
if (index == 0) {
showNextToastLocked();
}
} finally {
Binder.restoreCallingIdentity(callingId);
}
}
}
这就是进队列的方法,其中mToastQueue就是一个ArrayList,里面保存着一个个ToastRecord,ToastRecord也是一个静态内部类,负责保存进程ID,ITransientNotification实例,时长等信息。基本流程就是先判断是否存在队列中(这个判断主要是基于包名的,详见indexOfToastPackageLocked方法),若存在则更新时长和TN信息(用于更新内容等),否则加入队列末尾。若当前是队列第一个,则调用showNextToastLocked()来显示,方法如下:
void showNextToastLocked() {
ToastRecord record = mToastQueue.get(0);
while (record != null) {
try {
record.callback.show(record.token);
scheduleTimeoutLocked(record);
return;
} catch (RemoteException e) {
int index = mToastQueue.indexOf(record);
if (index >= 0) {
mToastQueue.remove(index);
}
keepProcessAliveIfNeededLocked(record.pid);
if (mToastQueue.size() > 0) {
record = mToastQueue.get(0);
} else {
record = null;
}
}
}
}
基本流程就是不断从队列中取第一个ToastRecord,然后调用TN实例中的show方法显示。也就回到之前我们看的handleShow方法中,利用WindowManager添加视图进行显示。之后调用了scheduleTimeoutLocked,主要用于移除Toast
private void scheduleTimeoutLocked(ToastRecord r)
{
mHandler.removeCallbacksAndMessages(r);
Message m = Message.obtain(mHandler, MESSAGE_TIMEOUT, r);
long delay = r.duration == Toast.LENGTH_LONG ? LONG_DELAY : SHORT_DELAY;
mHandler.sendMessageDelayed(m, delay);
}
在这里我们看到了Toast显示时长的实现,就是发送一个延迟消息,延迟期间就是显示的时机。当Handler收到MESSAGE_TIMEOUT消息时,执行下面方法:
private void handleTimeout(ToastRecord record)
{
synchronized (mToastQueue) {
int index = indexOfToastLocked(record.pkg, record.callback);
if (index >= 0) {
cancelToastLocked(index);
}
}
}
又调用了cancelToastLocked方法,附带该Toast在队列的位置:
void cancelToastLocked(int index) {
ToastRecord record = mToastQueue.get(index);
try {
record.callback.hide();
} catch (RemoteException e) {
}
ToastRecord lastToast = mToastQueue.remove(index);
mWindowManagerInternal.removeWindowToken(lastToast.token, true, DEFAULT_DISPLAY);
keepProcessAliveIfNeededLocked(record.pid);
if (mToastQueue.size() > 0) {
showNextToastLocked();
}
}
可见显示调用了TN中的hide方法,然后将ToastRecord移出队列,然后循环去显示下一个Toast。