本来想在一个文章里面记录一下在 Service 中弹出 Toast、Dialog、Activity (基于Android 8.1.0),最后还是决定分开单独写一下。因为看起来觉得这只是一点点的知识点,现在你如果在网络上搜索怎么在 Service 中弹出 Toast、Dialog、Activity ,能找到的博客一般都是有问题的。
首先这篇文章是根据我的理解,记录一下怎么在 Service 中弹出 Toast(基于Android 8.1.0 )。
开门见山,在 Android 8.1.0 Service 中弹出Toast 大概分为两种情况:
1.在没有创建子线程的前提条件下,你在 Service 的生命周期中想怎么弹出 Toast 就怎么弹。一句话:
Toast.makeText(this, "TaskService -- onStartCommand().", Toast.LENGTH_SHORT).show();
2.在 Service 中创建了工作线程, 当在工作线程做完了事情之后弹一个Toast 告诉用户当前操作做完了(大部分属于此情况)。
//在 Service 中创建全局变量 mHandler
private Handler mHandler;
//在 Service 生命周期方法 onCreate() 中初始化 mHandler
mHandler = new Handler(Looper.getMainLooper());
//在子线程中想要 Toast 的地方添加如下
mHandler.post(new Runnable() {
@Override
public void run() {
Toast.makeText(TaskService.this, "TaskService -- onStartCommand().", Toast.LENGTH_SHORT).show();
}
});
以上就是怎么在 Service 中弹Toast 的具体操作步骤了。
但是,知其然,我们也得知其所以然。本着程序员的求根问底的精神,脑海中冒出几个问题,急需解答:
(1)情况二中为什么我们必须按照上面的操作来弹 Toast ?
(2)情况二中不按照上面的方式来,直接像在 Activity 中弹吐司为什么会报错?
(3)针对情况二,有没有其他方式弹 Toast 还不会报错?
(4)从源码分析一下。
首先我们看,当我们调用 Toast.makeText() 的时候,会调用到 Toast.java 中的同名方法
/**
* Make a standard toast to display using the specified looper.
* If looper is null, Looper.myLooper() is used.//关键注释
* @hide
*/
public static Toast makeText(@NonNull Context context, @Nullable Looper looper,
@NonNull CharSequence text, @Duration int duration) {
//关键代码
Toast result = new Toast(context, looper);
//code...
return result;
}
会创建一个 Toast 对象。再看 Toast.java 的构造函数:
/**
* Constructs an empty Toast object.
* If looper is null, Looper.myLooper() is used.//关键注释
* @hide
*/
public Toast(@NonNull Context context, @Nullable Looper looper) {//heguodong
mContext = context;
//关键代码
mTN = new TN(context.getPackageName(), looper);
//code
}
在 Toast.java 的构造函数中,会创建一个内部类 TN 实例,这个 TN 是个什么鬼?
private static class TN extends ITransientNotification.Stub {//heguodong
//field.
TN(String packageName, @Nullable Looper looper) {
// code
if (looper == null) {
// Use Looper.myLooper() if looper is not specified.
looper = Looper.myLooper();
//关键代码
if (looper == null) {
throw new RuntimeException(
"Can't toast on a thread that has not called Looper.prepare()");
}
}
mHandler = new Handler(looper, null) {
@Override
public void handleMessage(Message msg) {
//code
}
};
}
//code and method.
}
看到这里,知道了吧,当调用 Toast.makeText() 的时候,首先会创建一个 Toast 对象,在创建 Toast 对象的时候,会创建一个 TN 对象,而在创建这个 TN 对象的时候,会检查当前传入的 looper 是否非空, 如果 looper 是null ,则调用 Looper.myLooper();拿到 looper 实例,使用looper 初始化 handler,然后使用 Handler 在各个回调方法如 show() 或者 hide() 中进行消息传递,控制着 Toast 的显示和隐藏。
因此上面的四个问题都得到解答了:
1.我们之所以在情况二中必须按照上面的操作方式来弹出Toast,因为最终TN 对象中需要使用这个 looper 对象初始化 Handler。
2.如果我们不按照情况二中的方式来弹 Toast,直接像Activity 中弹Toast 的话,会报错,报错信息:
java.lang.RuntimeException: Can't toast on a thread that has not called Looper.prepare()
之所以报错是因为初始化 TN 实例的时候调用 Looper.myLooper(); 方法返回了一个 null。
3.针对情况二,我们不让它报错了就是,我们在吐司之前,调用一下 Looper.prepare() 不就可以了!
可以像下面这样调用:
Looper.prepare();
Toast.makeText(TaskService.this, "TaskService -- onStartCommand().", Toast.LENGTH_SHORT).show();
Looper.loop();
但是需要注意的是,这样吐司,第一,代码不美观,每次吐司之前都要做相应的判断并调用 Looper.prepare()。第二,这样写可能会有风险。看 Looper.java 中的 prepare方法:
/** Initialize the current thread as a looper.
* This gives you a chance to create handlers that then reference
* this looper, before actually starting the loop. Be sure to call
* {@link #loop()} after calling this method, and end it by calling
* {@link #quit()}.
*/
public static void prepare() {
prepare(true);
}
private static void prepare(boolean quitAllowed) {
if (sThreadLocal.get() != null) {
throw new RuntimeException("Only one Looper may be created per thread");
}
sThreadLocal.set(new Looper(quitAllowed));
}
可以看到,这个 Looper.prepare() 能且只能调用一次,如果重复调用,会抛出异常。那如果在 Service 中有很多需要吐司的地方,我们又不小心多调用了一个 Looper.prepare() ,这岂不是挂了。
以上是怎么在 Service 中怎么吐司的相关记录。记录下来。
参考:
Android中,在子线程使用Toast会报错? - Seasoninthesun的回答 - 知乎 https://www.zhihu.com/question/51099935/answer/124782042
Android中,在子线程使用Toast会报错? - 森羴的回答 - 知乎 https://www.zhihu.com/question/51099935/answer/125487934
https://blog.csdn.net/nmzkchina/article/details/15506373
下面这个链接是错误的,误人子弟的博客。。引以为戒
https://blog.csdn.net/pku_android/article/details/7438901#commentBox
-----
最后,这是我写的简单 demo,参考一下。
//地址(等有空了传上去)