Android 8.1.0 Service 中怎么弹出 Toast

        本来想在一个文章里面记录一下在 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,参考一下。

//地址(等有空了传上去)

 

 

 

 

你可能感兴趣的:(Android 8.1.0 Service 中怎么弹出 Toast)