能否在子线程Toast
第一种情况,直接new一个线程进行Toast
new Thread(new Runnable() {
@Override
public void run() {
Toast.makeText(ToastActivity.this, "在子线程Toast", Toast.LENGTH_LONG).show();
}
}).start();
结果报错崩溃
java.lang.RuntimeException: Can't toast on a thread that has not called Looper.prepare()
原因为Toast需要使用Handler事件分发,必须保证运行在looper中。
然后在子线程创建了Handler,确实可以弹出┓( ´∀` )┏。
new Thread(new Runnable() {
@Override
public void run() {
Looper.prepare();
handler = new Handler() {
@Override
public void handleMessage(Message msg) {
Toast.makeText(ToastActivity.this, "在子线程Toast", Toast.LENGTH_LONG).show();
}
};
Looper.loop();
}
}).start();
findViewById(R.id.btn_toast_sub).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
handler.sendEmptyMessage(0);
}
});
也可以使用HandlerThread来初始化Looper,HandlerThread的使用参考IntentService。
new Thread(new Runnable() {
@Override
public void run() {
handlerThread = new HandlerThread("子线程Handler");
handlerThread.start();
handler = new Handler(handlerThread.getLooper()) {
@Override
public void handleMessage(Message msg) {
Toast.makeText(WindowActivity.this, "在子线程Toast", Toast.LENGTH_LONG).show();
}
};
}
}).start();
子线程更新UI报异常的原因在android/view/ViewRootImpl.java 中的 checkThread方法,Toast并没有继承View。
能任意设置Toast时长吗
我们在外部调用Toast.makeText进行Duration设置后,最终执行定位到TN(Toast的实际功能实现类)的handleShow方法中。
static final long SHORT_DURATION_TIMEOUT = 4000;
static final long LONG_DURATION_TIMEOUT = 7000;
public void handleShow(IBinder windowToken) {
if (mView != mNextView) {
//删除一堆WindowManager.LayoutParams的设置
mParams.hideTimeoutMilliseconds = mDuration ==
Toast.LENGTH_LONG ? LONG_DURATION_TIMEOUT : SHORT_DURATION_TIMEOUT;
//删除另一堆WindowManager.LayoutParams的设置
try {
//最终将View显示在Window窗口中,这块涉及WindowManager之类的知识,就是设置View在屏幕中的x,y和层级
mWM.addView(mView, mParams);
trySendAccessibilityEvent();
} catch (WindowManager.BadTokenException e) {
/* ignore */
}
}
}
通过关键代码看到无论设置什么值,最终都是显示4s或者7s(ps:印象中是2s和5s???)
上面搞错了,hideTimeoutMilliseconds是为了防止View长时间存在造成遮挡的措施。
通过调用show方法将TN放入队列中
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的实现类\android\server\notification\NotificationManagerService.java
public void enqueueToast(String pkg, ITransientNotification callback, int duration)
{
......
synchronized (mToastQueue) {
int callingPid = Binder.getCallingPid();
long callingId = Binder.clearCallingIdentity();
try {
ToastRecord record;
int index;
//若队列为空,封装为ToastRecord入队。队列中有就取出复用
if (index >= 0) {
record = mToastQueue.get(index);
record.update(duration);
try {
record.callback.hide();
} catch (RemoteException e) {
}
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);
//显示Toast的方法在这
if (index == 0) {
showNextToastLocked();
}
} finally {
Binder.restoreCallingIdentity(callingId);
}
}
}
又将Toast封装成ToastRecord。在showNextToastLock方法中取出执行scheduleDurationReachedLocked(record),最终找到设置时长的地方。
//com/android/server/policy/PhoneWindowManager.java
public static final int TOAST_WINDOW_TIMEOUT = 3500; // 3.5 seconds
static final int LONG_DELAY = PhoneWindowManager.TOAST_WINDOW_TIMEOUT;
static final int SHORT_DELAY = 2000; // 2 second
private void scheduleDurationReachedLocked(ToastRecord r)
{
mHandler.removeCallbacksAndMessages(r);
Message m = Message.obtain(mHandler, MESSAGE_DURATION_REACHED, r);
long delay = r.duration == Toast.LENGTH_LONG ? LONG_DELAY : SHORT_DELAY;
mHandler.sendMessageDelayed(m, delay);
}
得Toast.LENGTH_LONG为3.5s,Toast.LENGTH_SHORT为2s,仅支持这两种时长。
是否能够无限Toast
在调用show方法后会将TN存入INotificationManager的队列中,找到INotificationManager的实现类\android\server\notification\NotificationManagerService.java
static final int MAX_PACKAGE_NOTIFICATIONS = 50;
private boolean checkDisqualifyingFeatures(int userId, int callingUid, int id, String tag,
NotificationRecord r, boolean isAutogroup) {
//判断是否系统应用
final String pkg = r.sbn.getPackageName();
final boolean isSystemNotification =
isUidSystemOrPhone(callingUid) || ("android".equals(pkg));
final boolean isNotificationFromListener = mListeners.isListenerPackage(pkg);
if (!isSystemNotification && !isNotificationFromListener) {
synchronized (mNotificationLock) {
......
// 限制入队的速率
if (mNotificationsByKey.get(r.sbn.getKey()) != null
&& !r.getNotification().hasCompletedProgress()
&& !isAutogroup) {
final float appEnqueueRate = mUsageStats.getAppEnqueueRate(pkg);
if (appEnqueueRate > mMaxPackageEnqueueRate) {
mUsageStats.registerOverRateQuota(pkg);
final long now = SystemClock.elapsedRealtime();
if ((now - mLastOverRateLogTime) > MIN_PACKAGE_OVERRATE_LOG_INTERVAL) {
Slog.e(TAG, "Package enqueue rate is " + appEnqueueRate
+ ". Shedding " + r.sbn.getKey() + ". package=" + pkg);
mLastOverRateLogTime = now;
}
return false;
}
}
// 限制队列中的最大数量为50
int count = getNotificationCountLocked(pkg, userId, id, tag);
if (count >= MAX_PACKAGE_NOTIFICATIONS) {
mUsageStats.registerOverCountQuota(pkg);
Slog.e(TAG, "Package has already posted or enqueued " + count
+ " notifications. Not showing more. package=" + pkg);
return false;
}
}
}
......
return true;
}
其他
文中提到的HandlerThread、WindowManger还有Ibinder,AIDL实现IPC(跨进程通信)一堆的东西要学才能彻底搞懂Toast实现啊!!!
网友评论