最近跟朋友讨论了面试的时候碰到的一道面试题:可以在子线程里面弹Toast吗?为什么
Toast每天都在用,用的理所当然,却没有进去看一眼源码,就这个问题,我搜了网上的资料,然后自己也进去看了一下源码。给同行的朋友们分享下,同时也当做自己的笔记。感谢此篇博客博主的无私分享
https://blog.csdn.net/sinat_17314503/article/details/53015163
首先看这个
public void click(View view) {
new Thread(new Runnable() {
@Override
public void run() {
Toast.makeText(MainActivity.this,"helloworld",Toast.LENGTH_SHORT).show();
}
}).start();
}
运行报错
java.lang.RuntimeException: Can't create handler inside thread that has not called Looper.prepare()
平时我们用Toast也是直接调用这行代码,那么?直接调用和在子线程里面区别在哪里?
我们看Toast的show方法
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
}
}
static private INotificationManager getService() {
if (sService != null) {
return sService;
}
sService = INotificationManager.Stub.asInterface(ServiceManager.getService("notification"));
return sService;
}
这里涉及到AIDL进程间通讯,这里有个TN类,TN继承自远程NotificationService的代理类,实际弹Toast的工作是在TN这个类里面完成的。
再看TN代理类:
private static class TN extends ITransientNotification.Stub {
final Runnable mHide = new Runnable() {
@Override
public void run() {
handleHide();
mNextView = null;
}
};
final Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
IBinder token = (IBinder) msg.obj;
handleShow(token);
}
};
@Override
public void show(IBinder windowToken) {
if (localLOGV) Log.v(TAG, "SHOW: " + this);
mHandler.obtainMessage(0, windowToken).sendToTarget();
}
/**
* schedule handleHide into the right thread
*/
@Override
public void hide() {
if (localLOGV) Log.v(TAG, "HIDE: " + this);
mHandler.post(mHide);
}
public void handleShow(IBinder windowToken) {
//省略
}
public void handleHide() {
//省略
}
}
}
这里TN里的show 和hide方法实际上都是通过handler发送消息,执行相应的show 和hide任务。
重点来了!!!:
弹Toast过程实际上是TN类向当前线程发送消息,执行相应show 和hide任务过程,这里又涉及到Handler的消息机制:
Handler的消息机制涉及到以下三个重要的类Handler、消息队列MessageQueue、轮询器Looper,该消息机制要完整运行这三个必不可少,而Handler的创建过程中:
public Handler(Callback callback, boolean async) {
if (FIND_POTENTIAL_LEAKS) {
final Class<? extends Handler> klass = getClass();
if ((klass.isAnonymousClass() || klass.isMemberClass() || klass.isLocalClass()) &&
(klass.getModifiers() & Modifier.STATIC) == 0) {
Log.w(TAG, "The following Handler class should be static or leaks might occur: " +
klass.getCanonicalName());
}
}
mLooper = Looper.myLooper();
if (mLooper == null) {
throw new RuntimeException(
"Can't create handler inside thread that has not called Looper.prepare()");
}
mQueue = mLooper.mQueue;
mCallback = callback;
mAsynchronous = async;
}
必要条件之一是需要一个轮询器Looper, 这里大家已经看到开头的那个报错的异常了--没错,是因为缺少looper轮询器。那为什么主线程不会报错呢?因为主线程在创建的时候已经默认执行了Looper.prepare()这个方法并且调用Looper.loop()使其开始轮询工作。而子线程是没有默认给创建这个Looper的 ,这也解释了为什么 mLooper = Looper.myLooper();返回的是一个空的对象
好的,那么我是不是也可以模仿主线程,在子线程中创建一个Looper对象并使之开始工作呢,TN类创建Handler的条件不就有了吗?尝试一下:
public void click(View view) {
new Thread(new Runnable() {
@Override
public void run() {
Looper.prepare();
Toast.makeText(MainActivity.this,"helloworld",Toast.LENGTH_SHORT).show();
Looper.loop();
}
}).start();
}
加上Looper的创建和开始轮询,在运行 ,现在你的子线程同样也可以弹Toast消息了。
感谢博主分享的博客https://blog.csdn.net/sinat_17314503/article/details/53015163。以前只做伸(tai)手(lan)党(l),其中有何不对之处,忘朋友们多多指正!