在service子线程中使用Toast会抛出"Can't create handler inside thread that has not called Looper.prepare()"异常。百思不得其解,后来终于想通了,分享下我的心得。
Toast可以看出是个特殊的(不可操作,不可获焦,不可触摸)UI组件,因此,他的操作必须放在主线程(UI线程)中。网上看到很多种解决方法,其中一种是在子线程中执行Loop.prepare();方法,虽然能解决问题,但是觉得有违操作UI组件的原则。下面通过源码解释一下原因。先从Toast的创建说起。
当我们想要一个Toast的时候,一般使用一下方法创建:
Toast.makeText(getApplicationContext(), msg, Toast.LENGTH_LONG).show();
F3进去看看makeText()方法,源码如下:
public static Toast makeText(Context context, CharSequence text, int duration) {
Toast result = new Toast(context);
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;
}
发现在此方法中创建了一个Toast对象,继续进去,看看构造方法做了些什么工作。
public Toast(Context context) {
mContext = context;
mTN = new TN();
mTN.mY = context.getResources().getDimensionPixelSize(
com.android.internal.R.dimen.toast_y_offset);
}
发现在构造方法中又创建了TN,TN是干嘛的呢?继续。
private static class TN extends ITransientNotification.Stub {
//...此处省略其他代码
TN() {
// XXX This should be changed to use a Dialog, with a Theme.Toast
// defined that sets up the layout params appropriately.
final WindowManager.LayoutParams params = mParams;
params.height = WindowManager.LayoutParams.WRAP_CONTENT;
params.width = WindowManager.LayoutParams.WRAP_CONTENT;
params.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
| WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
| WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON;
params.format = PixelFormat.TRANSLUCENT;
params.windowAnimations = com.android.internal.R.style.Animation_Toast;
params.type = WindowManager.LayoutParams.TYPE_TOAST;
params.setTitle("Toast");
}
}
请注意TN的类型,后面接着解释。继续看Toast.show();
public void show() {
if (mNextView == null) {
throw new RuntimeException("setView must have been called");
}
INotificationManager service = getService();
String pkg = mContext.getPackageName();
TN tn = mTN;
tn.mNextView = mNextView;
try {
service.enqueueToast(pkg, tn, mDuration);
} catch (RemoteException e) {
// Empty
}
}
发现toast发给NotificationManager了,继续进去NotificationManagerService中看看调用了showNextToastLocked()方法。
private void showNextToastLocked() {
ToastRecord record = mToastQueue.get(0);
while (record != null) {
if (DBG) Slog.d(TAG, "Show pkg=" + record.pkg + " callback=" + record.callback);
try {
record.callback.show();
scheduleTimeoutLocked(record, false);
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);
}
keepProcessAliveLocked(record.pid);
if (mToastQueue.size() > 0) {
record = mToastQueue.get(0);
} else {
record = null;
}
}
}
}
发现最终显示是在这条语句中:record.callback.show();这个callback是什么?继续
在NotificationManagerService的enqueueToast()
public void enqueueToast(String pkg, ITransientNotification callback, int duration)
{
if (DBG) Slog.i(TAG, "enqueueToast pkg=" + pkg + " callback=" + callback + " duration=" + duration);
if (pkg == null || callback == null) {
Slog.e(TAG, "Not doing toast. pkg=" + pkg + " callback=" + callback);
return ;
}
synchronized (mToastQueue) {
int callingPid = Binder.getCallingPid();
long callingId = Binder.clearCallingIdentity();
try {
ToastRecord record;
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.
if (index >= 0) {
record = mToastQueue.get(index);
record.update(duration);
} else {
// Limit the number of toasts that any given package except the android
// package can enqueue. Prevents DOS attacks and deals with leaks.
if (!"android".equals(pkg)) {
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;
}
}
}
}
record = new ToastRecord(callingPid, pkg, callback, duration);
mToastQueue.add(record);
index = mToastQueue.size() - 1;
keepProcessAliveLocked(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.
if (index == 0) {
showNextToastLocked();
}
} finally {
Binder.restoreCallingIdentity(callingId);
}
}
}
原来这个callback就是前面提到的TN,继续进去看看TN.show()
public void show() {
if (localLOGV) Log.v(TAG, "SHOW: " + this);
mHandler.post(mShow);
}
现在你一下思路,看看Toast从发送到在屏幕上显示的过程,Toast先通过静态方法,创建一个View和回调,然后把这个record发给NotificationManagerService,service仅仅是管理Toast的显示顺序,因为同一时间只能有一个Toast显示在屏幕,最终的显示是通过回调显示的,也就是说显示的操作最会还是在创建Toast的线程中执行的。
看看TN.show(),现在问题来了,TN中的那个Handler是在哪里初始化的呢?继续代码;
final Handler mHandler = new Handler();
发现就是一个空构造方法。回忆一下Handler的使用,这个构造方法说明handler.post的runnable是在当前的线程中运行,如果我们在子线程中弹一个Toast,那就是说在这个子线程中显示一个View,显然,子线程不能直接操作UI组件。所以问题就出现了。
解决方法:
将Toast的显示放在在主线程中创建的Handler中。
代码如下:
public class MyService extends Service {
Handler mHandler = new Handler();
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
// TODO Auto-generated method stub
new Thread(){
public void run() {
mHandler.post(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
Toast.makeText(getApplicationContext(), "service start", Toast.LENGTH_LONG).show();
}
});
}
}.start();
return super.onStartCommand(intent, flags, startId);
}
@Override
public IBinder onBind(Intent intent) {
// TODO Auto-generated method stub
return null;
}
}