前言
DevTips是一个新系列,旨在记录一些开发过程中的小技巧,没有太大的技术含量,更多的作用是备忘;
Tip
因此先上结论:
- 在
Toast
显示过程中,可通过adb shell dumpsys notification
查看:Current Notification Manager state: Toast Queue: ToastRecord{ea7f229 pkg=com.xxx callback=android.app.ITransientNotification$Stub$Proxy@a550c4e duration=0
- 如果没有办法抓到现场,从日志的
NotificationService
也会有相应记录:06-10 10:50:06.551 1364 2522 I NotificationService: enqueueToast pkg=com.xxx callback=android.app.ITransientNotification$Stub$Proxy@a550c4e duration=0 06-10 10:50:06.575 1364 2522 I NotificationService: cancelToast pkg=com.xxx callback=android.app.ITransientNotification$Stub$Proxy@790df67 06-10 10:50:06.575 1364 2522 W NotificationService: Toast already cancelled. pkg=com.xxx callback=android.app.ITransientNotification$Stub$Proxy@790df67 06-10 10:50:06.575 1364 2522 I NotificationService: cancelToast pkg=com.xxx callback=android.app.ITransientNotification$Stub$Proxy@d6ab114 06-10 10:50:06.575 1364 2522 W NotificationService: Toast already cancelled. pkg=com.xxx callback=android.app.ITransientNotification$Stub$Proxy@d6ab114 06-10 10:50:08.581 1364 7015 W NotificationService: Toast already killed. pkg=com.xxx callback=android.app.ITransientNotification$Stub$Proxy@cbb990d
溯源
为什么NotificationService(NotificationManagerService)
记录了Toast
的记录?
- 首先查看
Toast.java
的show()
方法实现:/** * Show the view for the specified duration. */ public void show() { if (Compatibility.isChangeEnabled(CHANGE_TEXT_TOASTS_IN_THE_SYSTEM)) { checkState(mNextView != null || mText != null, "You must either set a text or a view"); } else { 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 { if (Compatibility.isChangeEnabled(CHANGE_TEXT_TOASTS_IN_THE_SYSTEM)) { if (mNextView != null) { // It's a custom toast service.enqueueToast(pkg, mToken, tn, mDuration, displayId); } else { // It's a text toast ITransientNotificationCallback callback = new CallbackBinder(mCallbacks, mHandler); service.enqueueTextToast(pkg, mToken, mText, mDuration, displayId, callback); } } else { service.enqueueToast(pkg, mToken, tn, mDuration, displayId); } } catch (RemoteException e) { // Empty } }
- 可见
Toast.show()
的实现,实际上会调用到INotificationManager.enqueueTextToast()/enqueueToast()
中; NotificationManagerService.mService
作为INotificationManager
的实现,其enqueueTextToast()/enqueueToast()
实现如下:@Override public void enqueueTextToast(String pkg, IBinder token, CharSequence text, int duration, int displayId, @Nullable ITransientNotificationCallback callback) { enqueueToast(pkg, token, text, null, duration, displayId, callback); } @Override public void enqueueToast(String pkg, IBinder token, ITransientNotification callback, int duration, int displayId) { enqueueToast(pkg, token, null, callback, duration, displayId, null); } private void enqueueToast(String pkg, IBinder token, @Nullable CharSequence text, @Nullable ITransientNotification callback, int duration, int displayId, @Nullable ITransientNotificationCallback textCallback) { if (DBG) { Slog.i(TAG, "enqueueToast pkg=" + pkg + " token=" + token + " duration=" + duration + " displayId=" + displayId); } if (pkg == null || (text == null && callback == null) || (text != null && callback != null) || token == null) { Slog.e(TAG, "Not enqueuing toast. pkg=" + pkg + " text=" + text + " callback=" + " token=" + token); return; } final int callingUid = Binder.getCallingUid(); checkCallerIsSameApp(pkg); final boolean isSystemToast = isCallerSystemOrPhone() || PackageManagerService.PLATFORM_PACKAGE_NAME.equals(pkg); boolean isAppRenderedToast = (callback != null); if (!checkCanEnqueueToast(pkg, callingUid, isAppRenderedToast, isSystemToast)) { return; } synchronized (mToastQueue) { int callingPid = Binder.getCallingPid(); final long callingId = Binder.clearCallingIdentity(); try { ToastRecord record; int index = indexOfToastLocked(pkg, token); // If it's already in the queue, we update it in place, we don't // move it to the end of the queue. if (index >= 0) { record = mToastQueue.get(index); record.update(duration); } else { // Limit the number of toasts that any given package can enqueue. // Prevents DOS attacks and deals with leaks. 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_TOASTS) { Slog.e(TAG, "Package has already queued " + count + " toasts. Not showing more. Package=" + pkg); return; } } } Binder windowToken = new Binder(); mWindowManagerInternal.addWindowToken(windowToken, TYPE_TOAST, displayId, null /* options */); record = getToastRecord(callingUid, callingPid, pkg, isSystemToast, token, text, callback, duration, windowToken, displayId, textCallback); mToastQueue.add(record); index = mToastQueue.size() - 1; keepProcessAliveForToastIfNeededLocked(callingPid); } // If it's at index 0, it's the current toast. It doesn't matter if it's // new or just been updated, show it. // If the callback fails, this will remove it from the list, so don't // assume that it's valid after this. if (index == 0) { showNextToastLocked(false); } } finally { Binder.restoreCallingIdentity(callingId); } } }
- 无论是
enqueueTextToast()
还是enqueueToast()
其最终都会有enqueueToast
的日志输出,且会调用到mToastQueue.add()
,而后者则会在dump()
时输出;
@Override
protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
if (!DumpUtils.checkDumpAndUsageStatsPermission(getContext(), TAG, pw)) return;
final DumpFilter filter = DumpFilter.parseFromArguments(args);
final long token = Binder.clearCallingIdentity();
try {
if (filter.stats) {
dumpJson(pw, filter);
} else if (filter.rvStats) {
dumpRemoteViewStats(pw, filter);
} else if (filter.proto) {
dumpProto(fd, filter);
} else if (filter.criticalPriority) {
dumpNotificationRecords(pw, filter);
} else {
dumpImpl(pw, filter);
}
} finally {
Binder.restoreCallingIdentity(token);
}
}
void dumpImpl(PrintWriter pw, @NonNull DumpFilter filter) {
pw.print("Current Notification Manager state");
if (filter.filtered) {
pw.print(" (filtered to "); pw.print(filter); pw.print(")");
}
pw.println(':');
int N;
final boolean zenOnly = filter.filtered && filter.zen;
if (!zenOnly) {
synchronized (mToastQueue) {
N = mToastQueue.size();
if (N > 0) {
pw.println(" Toast Queue:");
for (int i=0; i<N; i++) {
mToastQueue.get(i).dump(pw, " ", filter);
}
pw.println(" ");
}
}
}
...
}