Toast 源码详解:从创建到显示的完整流程
1、Toast 的创建与初始化
1.1、makeText 方法
Toast 的创建从 makeText() 方法开始:
public static Toast makeText(Context context, CharSequence text, @Duration int duration) {
return makeText(context, null, text, duration);
} 这个方法会调用内部的 makeText 方法,该方法会根据兼容性设置创建 Toast 实例:
public static Toast makeText(@NonNull Context context, @Nullable Looper looper,
@NonNull CharSequence text, @Duration int duration) {
if (Compatibility.isChangeEnabled(CHANGE_TEXT_TOASTS_IN_THE_SYSTEM)) {
Toast result = new Toast(context, looper);
result.mText = text;
result.mDuration = duration;
return result;
} else {
Toast result = new Toast(context, looper);
View v = ToastPresenter.getTextToastView(context, text);
result.mNextView = v;
result.mDuration = duration;
return result;
}
}
1.2、Toast 构造方法
Toast 的构造方法会初始化关键成员变量:
public Toast(@NonNull Context context, @Nullable Looper looper) {
mContext = context;
mToken = new Binder();
looper = getLooper(looper);
mHandler = new Handler(looper);
mCallbacks = new ArrayList<>();
mTN = new TN(context, context.getPackageName(), mToken,
mCallbacks, 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);
}
1.3、Looper 获取
Toast 必须在有 Looper 的线程中创建:
private Looper getLooper(@Nullable Looper looper) {
if (looper != null) {
return looper;
}
return checkNotNull(Looper.myLooper(),
"Can't toast on a thread that has not called Looper.prepare()");
}
1.4、TN 内部类
TN 是 Toast 的内部类,继承自 ITransientNotification.Stub,负责与 NotificationManagerService 进行 IPC 通信:
private static class TN extends ITransientNotification.Stub {
private final WindowManager.LayoutParams mParams;
private static final int SHOW = 0;
private static final int HIDE = 1;
private static final int CANCEL = 2;
final Handler mHandler;
int mGravity;
int mX;
int mY;
float mHorizontalMargin;
float mVerticalMargin;
View mView;
View mNextView;
int mDuration;
WindowManager mWM;
final String mPackageName;
final Binder mToken;
private final ToastPresenter mPresenter;
@GuardedBy("mCallbacks")
private final List<Callback> mCallbacks;
// 构造方法和其他方法...
}
2、Toast 的展示流程
2.1、show 方法
调用 show() 方法才会展示 Toast:
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
}
}
2.2、获取 NotificationManagerService
通过 getService() 方法获取 NotificationManagerService 的 IBinder 接口:
static private INotificationManager getService() {
if (sService != null) {
return sService;
}
sService = INotificationManager.Stub.asInterface(
ServiceManager.getService(Context.NOTIFICATION_SERVICE));
return sService;
}
2.3、NotificationManagerService 处理
NotificationManagerService 中的 enqueueToast 方法会处理 Toast 请求:
private void enqueueToast(String pkg, IBinder token, @Nullable CharSequence text,
@Nullable ITransientNotification callback, int duration, int displayId,
@Nullable ITransientNotificationCallback textCallback) {
// 检查参数
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 index = indexOfToastLocked(pkg, token);
if (index >= 0) {
// 更新已存在的 Toast
record = mToastQueue.get(index);
record.update(duration);
} else {
// 限制每个包的 Toast 数量
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;
}
}
}
// 创建新的 Toast 记录
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);
}
// 显示当前 Toast
if (index == 0) {
showNextToastLocked(false);
}
}
}
2.4、显示 Toast
showNextToastLocked 方法会依次处理队列中的 Toast:
void showNextToastLocked(boolean lastToastWasTextRecord) {
if (mIsCurrentToastShown) {
return; // 不要重复显示同一个 Toast
}
ToastRecord record = mToastQueue.get(0);
while (record != null) {
// 检查是否可以显示
int userId = UserHandle.getUserId(record.uid);
boolean rateLimitingEnabled = !mToastRateLimitingDisabledUids.contains(record.uid);
boolean isWithinQuota = mToastRateLimiter.isWithinQuota(userId, record.pkg, TOAST_QUOTA_TAG)
|| isExemptFromRateLimiting(record.pkg, userId);
boolean isPackageInForeground = isPackageInForegroundForToast(record.uid);
if (tryShowToast(record, rateLimitingEnabled, isWithinQuota, isPackageInForeground)) {
scheduleDurationReachedLocked(record, lastToastWasTextRecord);
mIsCurrentToastShown = true;
if (rateLimitingEnabled && !isPackageInForeground) {
mToastRateLimiter.noteEvent(userId, record.pkg, TOAST_QUOTA_TAG);
}
return;
}
// 移除无法显示的 Toast
int index = mToastQueue.indexOf(record);
if (index >= 0) {
mToastQueue.remove(index);
}
record = (mToastQueue.size() > 0) ? mToastQueue.get(0) : null;
}
}
2.5、SystemUI 展示
Toast 最终由 SystemUI 模块展示,通过 StatusBarManagerService 调用:
public void showToast(int uid, String packageName, IBinder token, CharSequence text,
IBinder windowToken, int duration,
@Nullable ITransientNotificationCallback callback) {
if (mBar != null) {
try {
mBar.showToast(uid, packageName, token, text, windowToken, duration, callback);
} catch (RemoteException ex) { }
}
}
3、Toast 的内部机制与优化
3.1、Toast 类型
Toast 分为两种类型:
- 文本 Toast:只显示文本内容
- 自定义 Toast:显示自定义 View
3.2、TN 类的消息处理
TN 类中的 Handler 负责处理显示和隐藏消息:
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();
mNextView = null;
break;
}
case CANCEL: {
handleHide();
mNextView = null;
try {
getService().cancelToast(mPackageName, mToken);
} catch (RemoteException e) {
}
break;
}
}
}
};
3.3、性能优化
- Toast 队列限制:每个包最多只能有有限数量的 Toast 在队列中
- 速率限制:对后台应用的 Toast 显示频率进行限制
- 进程保活:为 Toast 显示保持进程活跃
3.4、常见问题与解决方案
3.4.1、Toast 不显示
- 未调用 show() 方法
- 后台应用的自定义 Toast 被系统阻止
- Toast 队列已满
- 速率限制导致被拦截
3.4.2、内存泄漏
- 使用 Activity 作为 Context 可能导致内存泄漏
- 解决方案:使用 Application 作为 Context
3.4.3、线程问题
- Toast 必须在有 Looper 的线程中创建
- 解决方案:在子线程中创建 Looper 或使用主线程
3.5、最佳实践
- 使用 Application 作为 Context
- 避免在后台频繁显示 Toast
- 合理设置 Toast 显示时长
- 对于重要信息,考虑使用 Notification
Toast 是 Android 中常用的轻量级提示机制,其工作流程涉及应用层、系统服务层和 SystemUI 层。通过了解 Toast 的源码实现,我们可以更好地理解其工作原理,避免常见问题,并在开发中正确使用 Toast 功能。
Toast 的核心流程包括:
1. 通过 makeText() 创建 Toast 实例
2. 调用 show() 方法将 Toast 加入队列
3. NotificationManagerService 处理 Toast 请求
4. SystemUI 最终展示 Toast
掌握这些知识,将有助于我们在开发中更加灵活地使用 Toast,提升应用的用户体验。