目录
(1)public void enqueueToast(String pkg, ITransientNotification callback, int duration,int displayId)
(3)void scheduleDurationReachedLocked(ToastRecord r)
(4)handleDurationReached((ToastRecord) msg.obj)
(5 )void cancelToastLocked(int index)
最近项目中出现一个问题,就是有的手机在关闭系统通知,结果项目中使用的原生Toast在有的手机上竟然不显示了,然后就去查系统源码,发现原来原生的Toast是基于NotificaitionManagerService实现的,难怪有些手机不显示。那些显示的手机厂商应该发现了这个问题,在系统修改了源码。特别记录下这个过程,并且附上可以解决这个问题的源码,供大家参考。
通常我们在使用Toast的时候,都是下面的简单的一行代码就可以解决问题,就可以一个Toast显示。从源码的角度来看下这个Toast是怎么一步一步显示出来的。
Toast.makeText(mContext, "原生Toast",Toast.LENGTH_SHORT).show();
Toast的类本身就仅仅是一个没有任何继承的工具类。通过一个内部类TN管理Toast的添加/移除,主要就是对WindowManager上进行添加/移除Toast的View,NotificationManagerService来控制着在什么时候将View添加到WindowManager中,什么时候将View从WindowManager移除。
public class Toast {
private static class TN extends ITransientNotification.Stub {
}
}
一.Toast成员变量
TN内部类用来维护一个WindowManager来对Toast上面的View进行添加和移除;
mNextView也就是Toast即将要显示的Toast的View,如果调用setView(),就会将该View赋值到mNextView中。
//维护着一个WindowManager来添加/移除Toast的View
final TN mTN;
@UnsupportedAppUsage
//延时时间
int mDuration;
//调用Toast的时候需要显示的View,初始化的时候就是默认的UI,用户也可以调用setView来重新设置这个View
View mNextView;
二. Toast显示流程
1. Toast makeText(@NonNull Context context, @Nullable Looper looper,@NonNull CharSequence text, @Duration int duration)
主要是初始化Toast的View,默认的就是一个TextView,然后将mDuration、mNextView赋值到内部管理类TN。
/**
* Make a standard toast to display using the specified looper.
* If looper is null, Looper.myLooper() is used.
* @hide
*/
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;
}
内部类TN本身继承于ITransientNotification.Stub,对Toast的View的进行显示/隐藏的管理,同时NotificationServiceManager可以跨进程访问Toast所在的进程。
private static class TN extends ITransientNotification.Stub {
}
ITransientNotification.aidl文件里面其实就是show()/hide()两个方法,而内部类TN实现了Toast的show()/hide()。
/** @hide */
oneway interface ITransientNotification {
void show();
void hide();
}
而这两个方法中就是就是发送SHOW和HIDE消息给到Handler。
/**
* schedule handleShow into the right thread
*/
@Override
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
public void show(IBinder windowToken) {
if (localLOGV) Log.v(TAG, "SHOW: " + this);
mHandler.obtainMessage(SHOW, windowToken).sendToTarget();
}
/**
* schedule handleHide into the right thread
*/
@Override
public void hide() {
if (localLOGV) Log.v(TAG, "HIDE: " + this);
mHandler.obtainMessage(HIDE).sendToTarget();
}
从TN的构造方法中可以看到,我们可以传进来一个线程的Looper对象,我们就可以在当前线程中 显示Toast。也就是说可以在线程显示Toast,但是必须传入一个当前子线程的Looper对象。
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()");
}
}
另外注意的是,在子线程中显示Toast的时候,要注意要将Toast的show()方法要在 Looper.loop()之前,否则在Toast中的Handler无法循环取队列中的消息,该Toast就无法显示。
new Thread() {
@Override
public void run() {
super.run();
Looper.prepare();
Toast.makeText(ToastActivity.this,"22",Toast.LENGTH_SHORT).show();
Looper.loop();
}
}.start();
在内部类TN中的Handler里面就是去处理不同的消息。最终这些消息的发送是在NotificationManagerService中进行维护的。
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;
}
}
}
};
}
从源码中也可以看到handleShow()/handleHide()就是将Toast中的View从WindowManager中添加/移除。在 handleShow()方法中主要提一个 if (mView != mNextView) 这个逻辑判断,这个mNextView是开发者在调用setView()之后设置的,如果开发者没有主动调用该方法,那么就直接就是在调用makeText()实例化Toast的布局View。
public void handleShow(IBinder windowToken) {
if (localLOGV) Log.v(TAG, "HANDLE SHOW: " + this + " mView=" + mView
+ " mNextView=" + mNextView);
// If a cancel/hide is pending - no need to show - at this point
// the window token is already invalid and no need to do any work.
if (mHandler.hasMessages(CANCEL) || mHandler.hasMessages(HIDE)) {
return;
}
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);
// 省略一些代码。。。。主要就是设置mWM中的一些参数
//。。。。。。。。。
if (mView.getParent() != null) {
if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);
mWM.removeView(mView);
}
//将mView添加到WindowManager上面
try {
mWM.addView(mView, mParams);
trySendAccessibilityEvent();
} catch (WindowManager.BadTokenException e) {
/* ignore */
}
}
}
在 handleHide()中也就是将mView移除,并置空。所以mNextView就是即将显示的View,然后用mView来保存这个View,显示出来。
@UnsupportedAppUsage
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.
//将mView从WindowManager移除
if (mView.getParent() != null) {
if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);
mWM.removeViewImmediate(mView);
}
// Now that we've removed the view it's safe for the server to release
// the resources.
try {
getService().finishToken(mPackageName, this);
} catch (RemoteException e) {
}
mView = null;
}
}
2.show()
第一个makeText()方法是实例化Toast对象,那么show()就是将Toast显示出来,我们从源码中看到该主要是通过NotificationManagerService来进行管理所加入的Toast队列
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;
final int displayId = mContext.getDisplayId();
try {
service.enqueueToast(pkg, tn, mDuration, displayId);
} catch (RemoteException e) {
// Empty
}
}
我们从源码中可以看到此时已经通过NotificationManagerService来进行显示Toast。
3.NotificationManagerService
(1)public void enqueueToast(String pkg, ITransientNotification callback, int duration,int displayId)
该方法主要的逻辑就是:首先会判断加入的这个Toast,如果之前已经加入过mToastQueue中,则直接更新该Toast的duration(这种情况出现在如果在使用的时候,先把Toast实例化,然后通过该实例在不同的地方分别调用show()的时候,那么此时传入的Toast就会走该逻辑),否则将该Toast加入到mToastQueue中。
当将Toast加入到mToastQueue的时候,也会去判断是否为系统Toast,如果不是系统Toast,那么对应的该应用下,最多同时可加入25个Toast。
最后判断mToastQueue集合中的Toast是否为第一个,只有是集合中的第一个元素的时候,才会显示该Toast。
@Override
public void enqueueToast(String pkg, ITransientNotification callback, int duration,
int displayId)
{
if (pkg == null || callback == null) {
Slog.e(TAG, "Not enqueuing toast. pkg=" + pkg + " callback=" + callback);
return ;
}
//。。。。省略部分非重要逻辑。。。。
//mToastQueue维护着加入到队列中的所有Toast的集合
synchronized (mToastQueue) {
int callingPid = Binder.getCallingPid();
long callingId = Binder.clearCallingIdentity();
try {
// ToastRecord 是对Toast的封装、含有Toast的所在的包名、延时时间等
ToastRecord record;
//找到该Toast对应的索引值
int index = indexOfToastLocked(pkg, callback);
// If it's already in the queue, we update it in place, we don't
// move it to the end of the queue.
//(1)如果该Toast已经存在在队列中,则只更新Toast显示的时间
if (index >= 0) {
record = mToastQueue.get(index);
record.update(duration);
} else {
//(2)说明该Toast没有被加入到队列中,后面的逻辑就是将Toast加入到Toast队列
中,并显示第一个Toast
//(2.1)如果不是系统的Toast,那么每个应用下只能加入MAX_PACKAGE_NOTIFICATIONS个Toast,超过这个数量之后则不在显示
if (!isSystemToast) {
int count = 0;
final int N = mToastQueue.size();
for (int i=0; i<N; i++) {
final ToastRecord r = mToastQueue.get(i);
if (r.pkg.equals(pkg)) {
count++;
if (count >= MAX_PACKAGE_NOTIFICATIONS) {
Slog.e(TAG, "Package has already posted " + count
+ " toasts. Not showing more. Package=" + pkg);
return;
}
}
}
}
(2.2)把符合条件的将Toast封装成ToastRecord,并且加入到mToastQueue中
Binder token = new Binder();
mWindowManagerInternal.addWindowToken(token, TYPE_TOAST, displayId);
record = new ToastRecord(callingPid, pkg, callback, duration, token,
displayId);
mToastQueue.add(record);
//(2.3)把当前的索引值index指向刚加入的这个Toast的位置
index = mToastQueue.size() - 1;
keepProcessAliveIfNeededLocked(callingPid);
}
// If it's at index 0, it's the current toast. It doesn't matter if it's
// new or just been updated. Call back and tell it to show itself.
// If the callback fails, this will remove it from the list, so don't
// assume that it's valid after this.
//(3)如果刚加入的这个Toast恰好是该队列中的第一个,则将该Toast显示出来
if (index == 0) {
showNextToastLocked();
}
} finally {
Binder.restoreCallingIdentity(callingId);
}
}
}
那么现在就有一个问题了,那如果我当前Toast没有显示完的时候,又多次调用
Toast.makeText(mContext, "原生Toast",Toast.LENGTH_SHORT).show();
那么此时该mToastQueue集合中 已经有多个Toast,那么其他的Toast怎么显示呢?带着这个疑问,去看下面的代码。
(2)void showNextToastLocked()
从上面的方法中可以看到此时如果此时mToastQueue有一个Toast的话,就会调用该方法来显示Toast。
void showNextToastLocked() {
//(1)取出该Toast封装成的对象ToastRecord
ToastRecord record = mToastQueue.get(0);
//这里使用的是一个无限循环,我觉得是有点浪费资源的,不知道源码在写的时候采用这种方式有什么好处,所以在自定义Toast的时候,已经将该逻辑改掉了。
while (record != null) {
if (DBG) Slog.d(TAG, "Show pkg=" + record.pkg + " callback=" + record.callback);
try {
//(2)调用该Toast的内部类TN中的show()显示该Toast
record.callback.show(record.token);
//(3)这个就是向NotificationManagerService中维护的Handler中发送duration消息来隐藏
该Toast
scheduleDurationReachedLocked(record);
return;
} catch (RemoteException e) {
Slog.w(TAG, "Object died trying to show notification " + record.callback
+ " in package " + record.pkg);
// remove it from the list and let the process die
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;
}
}
}
}
(3)void scheduleDurationReachedLocked(ToastRecord r)
该方法的代码逻辑很简单,就是根据Toast.LENGTH_LONG还是Toast.LENGTH_SHORT来得到对应的延时时间发送到Handler对象中来隐藏Toast
private void scheduleDurationReachedLocked(ToastRecord r)
{
mHandler.removeCallbacksAndMessages(r);
Message m = Message.obtain(mHandler, MESSAGE_DURATION_REACHED, r);
//(1)根据Toast.LENGTH_LONG还是Toast.LENGTH_SHORT来获得对应的延时时间
int delay = r.duration == Toast.LENGTH_LONG ? LONG_DELAY : SHORT_DELAY;
// Accessibility users may need longer timeout duration. This api compares original delay
// with user's preference and return longer one. It returns original delay if there's no
// preference.
delay = mAccessibilityManager.getRecommendedTimeoutMillis(delay,
AccessibilityManager.FLAG_CONTENT_TEXT);
//(2)发送延时消息来来隐藏Toast
mHandler.sendMessageDelayed(m, delay);
}
那么就看下Handler中对应的内容
(4)handleDurationReached((ToastRecord) msg.obj)
这里的逻辑就是找到对应的Toast的索引值,然后调用 cancelToastLocked(index)方法将该Toast隐藏。
private void handleDurationReached(ToastRecord record)
{
if (DBG) Slog.d(TAG, "Timeout pkg=" + record.pkg + " callback=" + record.callback);
synchronized (mToastQueue) {
int index = indexOfToastLocked(record.pkg, record.callback);
if (index >= 0) {
cancelToastLocked(index);
}
}
}
(5 )void cancelToastLocked(int index)
主要就是找到对应的ToastRecord,回调到Toast内部类中的hide()方法来隐藏Toast。然后从mToastQueue队列中将Toast移除,将该Toast对应的消息从Handler中移除,最后判断下mToastQueue集合中是否有未显示的Toast,如果还有,则重复第2个方法,依次将集合中的第0个Toast显示完。这里也就回答了在第一个方法enqueueToast()中的疑问,完成mToastQueue集合中的所有的Toast依次显示完的逻辑。
void cancelToastLocked(int index) {
ToastRecord record = mToastQueue.get(index);
//(1)找到对应的ToastRecord,然后调用Toast中的hide()
try {
record.callback.hide();
} catch (RemoteException e) {
Slog.w(TAG, "Object died trying to hide notification " + record.callback
+ " in package " + record.pkg);
// don't worry about this, we're about to remove it from
// the list anyway
}
//(2)将该Toast从队列中移除
ToastRecord lastToast = mToastQueue.remove(index);
mWindowManagerInternal.removeWindowToken(lastToast.token, false /* removeWindows */,
lastToast.displayId);
// We passed 'false' for 'removeWindows' so that the client has time to stop
// rendering (as hide above is a one-way message), otherwise we could crash
// a client which was actively using a surface made from the token. However
// we need to schedule a timeout to make sure the token is eventually killed
// one way or another.
//(3)将该Toast对应的消息从Handler中移除
scheduleKillTokenTimeout(lastToast);
keepProcessAliveIfNeededLocked(record.pid);
//(4)如果集合中仍有Toast还没有显示完,那么就在重复第2个方法进行依次显示集合中的第0个元素
if (mToastQueue.size() > 0) {
// Show the next one. If the callback fails, this will remove
// it from the list, so don't assume that the list hasn't changed
// after this point.
showNextToastLocked();
}
}
四、Toast取消流程
如果我们在使用Toast的时候,需要在未显示完的时候就取消该Toast,这个时候就需要调用到cancel(),其实最终还是调用到内部管理类TN的cancel()方法
/**
* Close the view if it's showing, or don't show it if it isn't showing yet.
* You do not normally have to call this. Normally view will disappear on its own
* after the appropriate duration.
*/
public void cancel() {
mTN.cancel();
}
TN中的cancel()方法最终就是发送一个CANCEL消息到Handler,具体还是到NotificationManagerService中的canelToast()。
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) {
}
从NotificationManagerService中可以看到和前面的Toast在延时之后隐藏Toast的逻辑是一致的,只不过前面的是在延时时间到了之后,去调用 cancelToastLocked(index)方法,而这里是调用cancel()方法的时候,即时就调用cancelToastLocked(index)方法。
@Override
public void cancelToast(String pkg, ITransientNotification callback) {
Slog.i(TAG, "cancelToast pkg=" + pkg + " callback=" + callback);
if (pkg == null || callback == null) {
Slog.e(TAG, "Not cancelling notification. pkg=" + pkg + " callback=" + callback);
return ;
}
synchronized (mToastQueue) {
long callingId = Binder.clearCallingIdentity();
try {
int index = indexOfToastLocked(pkg, callback);
if (index >= 0) {
cancelToastLocked(index);
} else {
Slog.w(TAG, "Toast already cancelled. pkg=" + pkg
+ " callback=" + callback);
}
} finally {
Binder.restoreCallingIdentity(callingId);
}
}
}
五、原生Toast存在的问题
1.重复创建Toast
通常我们在用Toast的时候,都会直接调用下面一行代码来显示Toast,从我们在第二部分分析的显示流程中我们可以看到:
Toast.makeText(mContext, "原生Toast",Toast.LENGTH_SHORT).show();
每调用一次这行代码,都会实例化一个Toast,然后加入到NotificationServiceManager的mToastQueue队列中。如果恰好在点击按钮时调用这行代码,很容易会多次调用这行代码,引起重复创建Toast。有时候项目中为了避免重复创建Toast,所以通常会创建一个Toast实例,全局调用这一个Toast实例,例如:
private static Toast toast;
public static void showToast(Context context, String content) {
if (toast == null) {
toast = Toast.makeText(context, content, Toast.LENGTH_SHORT);
} else {
toast.setText(content);
}
toast.show();
}
上面的这行代码其实是会有内存泄漏的问题。例如当这个Toast在显示的时候,会持有Activity对象,当还未消失的时候,关闭了该Activity,就会导致Activity对象无法回收,引起Activity的内存泄漏。所以针对这个问题,已经在自定义的Toast中进行了改进。从源码中可以看到,每次显示的时候,其实都是取了mToastQueue中的第0个元素来显示,直到显示完才将该元素从集合中删除,那么我们完全可以在加入Toast之前,先去判断下该Toast的显示的文字内容与当前的Toast的文字内容是否一致,如果一致的话,可以先不加入到mToastQueue队列中。在自定义的代码(代码路径链接 GitHub 地址为https://github.com/wenjing-bonnie/toast.git)中已针对这点做出了优化。具体在PowerfulToastManagerService中:
protected void enqueueToast(PowerfulToast toast, String content, ITransientPowerfulToast callBack, int duration) {
//。。。。。。省略其他代码
//如果与正在显示的Toast的内容一致,则不将该Toast加入到Toast队列中;
//(1)恰好该workHandler的延时MESSAGE_DURATION_REACHED到了在执行remove操作的时候,此时为null,会向下加入这个Toast
//(2)只要这个Toast没有显示完,则取出来的值不为空,则不会加入到显示mToastQueue队列中
if (mToastQueue != null && !mToastQueue.isEmpty()) {
PowerfulToastRecord curRecord = mToastQueue.get(0);
if (curRecord != null && content.equals(curRecord.content)) {
return;
}
}
//将新增的toast加入到队列中
record = new PowerfulToastRecord(toast, content, callBack, duration);
mToastQueue.add(record);
//。。。。。。省略其他代码
}
2.原生Toast的显示的死循环
在原生的Toast显示的时候,这里取出mToastQueue的第0个元素,然后显示出来到最后消失的时候,这个循环一直在执行,一直到Toast显示完的时候,这个循环一直存在,其实为什么这里不直接使用一个if(record!=null)来进行判断就可以了呢?这个源码之所以采用这种方式有什么好处,暂时没有想到原因。所以在自定义Toast的时候,已经将该逻辑改了,直接使用的就是if(record!=null)来判断。
void showNextToastLocked() {
ToastRecord record = mToastQueue.get(0);
//这块为什么会要用一个死循环的方式呢?
while (record != null) {
}
}
3.原生Toast为系统级别的Toast
原生Toast在显示的时候,设置的WindowManager.LayoutParams的时候,采用的是下面的这种类型,但是对于自定义的Toast的时候,
params.type = WindowManager.LayoutParams.TYPE_TOAST;
WindowManager的类型分为应用Window、子Window、系统Window。应用Window对应的一个Activity,子Window不能单独存在,必须附属到父Window中,而系统Window在使用的时候,必须声明权限。
所以我们在自定义的Toast的不能采用这种类型,因为通知权限在关闭后设置显示的类型为TYPE_TOAST会抛android.view.WindowManager$BadTokenException这个异常。而系统Window的类型,在使用的时候,会提示用户给到相应的权限,这样在用户体验很差,所以只能采用应用Window,那么使用应用Window类型的时候,就会有另外一个问题,如果在Toast没有消失的时候,关闭Activity的时候,会抛出 android.view.WindowLeaked: Activity。所以为了避免这种情况,所以监听Activity的生命周期,在Activity关闭的时候,取消所有mToastQueue中的Toast。所以需要在使用自定义Toast的时候,需要先注册该Toast。
public class PowerfulToastManagerService implements Application.ActivityLifecycleCallbacks {
/**
* 将application传入用来管理Activity的生命周期
*
* @param application
*/
protected void registerToast(Application application) {
application.registerActivityLifecycleCallbacks(this);
}
/**
* {@link android.app.Application.ActivityLifecycleCallbacks}
*/
@Override
public void onActivityCreated(@NonNull Activity activity, @Nullable Bundle savedInstanceState) {
}
@Override
public void onActivityStarted(@NonNull Activity activity) {
}
@Override
public void onActivityResumed(@NonNull Activity activity) {
}
@Override
public void onActivityPaused(@NonNull Activity activity) {
Log.logV(TAG, activity.getClass().getSimpleName() + " , is paused ! " + " , size is " + mToastQueue.size());
cancelAllPowerfulToast();
}
@Override
public void onActivityStopped(@NonNull Activity activity) {
}
@Override
public void onActivitySaveInstanceState(@NonNull Activity activity, @NonNull Bundle outState) {
}
@Override
public void onActivityDestroyed(@NonNull Activity activity) {
}
}