Android进阶(十六)子线程调用Toast报Can‘t create handler inside thread that has not called Looper.prepare() 错误

本文详细阐述了在子线程中如何正确地使用Toast显示信息,包括遇到的错误、解决方法以及背后的原理。通过分析Looper和Handler的作用,提供了在子线程中实现UI操作的有效途径。

一、前言

原子线程调用Toast报Can't create handler inside thread that has not called Looper.prepare() 错误

今天用子线程调Toast报了一个Can't create handler inside thread that has not calledLooper.prepare()错误。

因为toast的实现需要在activity的主线程才能正常工作,所以传统的非主线程不能使toast显示在actvity上,通过Handler可以使自定义线程运行于Ui主线程。

前几次碰到这个问题,确实郁闷了很久... 

java.lang.RuntimeException: Can't create handler inside thread that has not calledLooper.prepare()

二、解决方案

解决办法很简单:

Looper.prepare();

Toast.makeText(getApplicationContext(), "test", Toast.LENGTH_LONG).show();

Looper.loop();

为什么要加这两句,看了源码就了解了

Toast 

    public void show() {

      ...

        service.enqueueToast(pkg, tn, mDuration);   //把这个toast插入到一个队列里面

        ...

    }

Looper

public static final void prepare() {

        if (sThreadLocal.get() != null) {

            throw new RuntimeException("Only one Looper may be created per thread");

        }

       sThreadLocal.set(new Looper());  //在当前线程中创建一个Looper

    }

private Looper() {

        mQueue = new MessageQueue();  //关键在这,创建Looper都干了什么。 其实是创建了消息队列

        mRun = true;

        mThread = Thread.currentThread();

    }

一般如果不是在主线程中又开启了新线程的话,一般都会碰到这个问题。

原因是在创建新线程的时候默认情况下不会去创建新的MessageQueue

总结下:Toast 显示的必要条件:Toast 显示需要出现在一个线程的消息队列中.... 很隐蔽

三、Android中HandlerThread类的解释

Android应用中的消息循环由Looper和Handler配合完成,Looper类用于封装消息循环,类中有个MessageQueue消息队列;Handler类封装了消息投递和消息处理等功能。

系统默认情况下只有主线程(即UI线程)绑定Looper对象,因此在主线程中可以直接创建Handler的实例,但是在子线程中就不能直接new出Handler的实例了,因为子线程默认并没有Looper对象,此时会抛出RuntimeException异常:

浏览下Handler的默认构造函数就一目了然了:

如果需要在子线程中使用Handler类,首先需要创建Looper类实例,这时可以通过Looper.prepare()和Looper.loop()函数来实现的。阅读Framework层源码发现,Android为我们提供了一个HandlerThread类,该类继承Thread类,并使用上面两个函数创建Looper对象,而且使用wait/notifyAll解决了多线程中子线程1获取子线程2的Looper对象为空的问题。

Toast创建时需要创建一个Handler,但是这个Handler需要获得Looper的实例,而在子线程中是没有这个实例的,需要手动创建。

附Toast部分源码:

    public Toast(Context context) {

        mContext = context;

        mTN = new TN();

        mTN.mY = context.getResources().getDimensionPixelSize(

                com.android.internal.R.dimen.toast_y_offset);

    }

    private static class TN extends ITransientNotification.Stub {

        ……

        final Handler mHandler = new Handler();    

        ……

    }

Handler源码:

   /**

     * Default constructor associates this handler with the queue for the

     * current thread.

     *

     * If there isn't one, this handler won't be able to receive messages.

     */

    public Handler() {

        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 = null;

    }

 

<think>我们正在解决一个具体问题:在调用其他类的Toast时导致Android应用闪退。根据用户提供的引用,闪退的原因是:在子线程中直接显示Toast而没有正确处理LooperAndroid要求Toast必须在主线程(UI线程)中显示,否则会抛出异常:`Can't create handler inside thread that has not called Looper.prepare()`。 用户提供的引用中,解决方案是使用`Looper.prepare()`和`Looper.loop()`包裹Toast的显示。但是,这种方法在非主线程中创建了一个消息循环(Looper),需要手动退出,否则可能导致线程无法结束,进而可能引发其他问题(如内存泄漏)。 更优的解决方案应该是将Toast的显示切换到主线程。我们可以使用以下几种方法: 1. 使用Activity.runOnUiThread(Runnable) 2. 使用Handler(Looper.getMainLooper()) 3. 使用View.post(Runnable) 另外,用户提供的工具类`toastHelp`中,尝试在子线程中通过捕获异常然后调用`Looper.prepare()`和`Looper.loop()`来显示Toast。这种方法虽然可以避免闪退,但并不是最佳实践,因为它会阻塞子线程直到消息队列被处理(Looper.loop()是死循环,除非调用quit),而且可能造成多个Toast排队显示。 因此,我们建议修改工具类,使其能够安全地在任何线程调用,并且将Toast的显示切换到主线程。 下面我们重新设计一个安全的Toast工具类: 设计要点: - 使用Application Context避免内存泄漏(如之前讨论的) - 确保在主线程中显示Toast - 避免重复创建Toast对象(可选,因为Toast本身是轻量级的,但连续快速显示时可能需要取消上一个) 改进后的工具类: ```java public class ToastUtils { // 使用弱引用持有Context,避免内存泄漏 private static WeakReference<Context> sContextRef; // 初始化,建议在Application中调用一次,传入Application Context public static void init(Context context) { sContextRef = new WeakReference<>(context.getApplicationContext()); } public static void show(final String message) { // 获取主线程Handler Handler mainHandler = new Handler(Looper.getMainLooper()); mainHandler.post(new Runnable() { @Override public void run() { Context context = sContextRef != null ? sContextRef.get() : null; if (context == null) { // 可以在这里尝试获取当前Activity的Context,但更推荐使用Application Context // 如果init没有被调用,这里可以选择不显示或者使用其他方式获取Context(不推荐) return; } // 使用Application Context创建Toast Toast.makeText(context, message, Toast.LENGTH_SHORT).show(); } }); } } ``` 使用方式: 1. 在Application的onCreate中初始化: ```java public class MyApp extends Application { @Override public void onCreate() { super.onCreate(); ToastUtils.init(this); } } ``` 2. 在任何地方(包括子线程调用: ```java ToastUtils.show("Hello from any thread!"); ``` 但是,上述方法有一个缺点:如果用户没有在Application中初始化,则Toast不会显示。我们可以改进为允许在调用时传入Context,但要注意避免内存泄漏。另一种方式是每次调用时都传入Context,但要求传入的Context是Application Context或者使用弱引用。 改进版本(允许在未初始化时使用传入的Context,但要求传入Application Context): ```java public class ToastUtils { // 使用弱引用持有Application Context private static WeakReference<Context> sAppContextRef; public static void init(Context context) { if (context != null) { sAppContextRef = new WeakReference<>(context.getApplicationContext()); } } public static void show(final Context context, final String message) { // 确保在主线程显示 runOnUiThread(new Runnable() { @Override public void run() { Context ctx = null; // 优先使用初始化的Application Context if (sAppContextRef != null) { ctx = sAppContextRef.get(); } // 如果初始化过但被回收了,或者没有初始化,则使用传入的Context(但必须是Application Context) if (ctx == null) { if (context != null) { ctx = context.getApplicationContext(); } else { // 如果没有任何Context可用,则无法显示Toast return; } } Toast.makeText(ctx, message, Toast.LENGTH_SHORT).show(); } }); } // 重载:使用初始化过的Context(如果已经初始化) public static void show(final String message) { if (sAppContextRef == null || sAppContextRef.get() == null) { throw new IllegalStateException("ToastUtils not initialized with Context. Call ToastUtils.init(Context) first."); } show(sAppContextRef.get(), message); } private static void runOnUiThread(Runnable runnable) { if (Looper.myLooper() == Looper.getMainLooper()) { runnable.run(); } else { Handler mainHandler = new Handler(Looper.getMainLooper()); mainHandler.post(runnable); } } } ``` 这样,我们提供了两种使用方式: 1. 初始化后直接使用字符串消息(推荐): ```java ToastUtils.show("Hello"); ``` 2. 在未初始化时(或者需要临时使用)传入一个Context(注意:这个Context应该尽量使用Application Context,如果传入Activity Context,则可能因为Activity销毁后导致内存泄漏): ```java ToastUtils.show(getApplicationContext(), "Hello"); ``` 但是,为了避免内存泄漏,我们要求传入的Context必须是Application Context,或者我们在工具类内部强制转换为Application Context(通过调用getApplicationContext)。上面代码中,我们在使用传入的Context时已经调用了`context.getApplicationContext()`,所以即使传入Activity Context也是安全的。 另外,我们也可以提供一个不需要Context参数的show方法,但要求必须提前初始化。 总结:这样设计的工具类可以在任何线程中安全调用,并且避免了内存泄漏和子线程显示Toast的异常。 如果用户不想在Application中初始化,也可以在每个调用的地方传入Application Context(例如,在Activity中调用时传入`getApplicationContext()`)。 现在,回答用户的问题:如何解决调用其他类的Toast时导致的闪退? 答案:使用上述工具类,在任何线程调用ToastUtils.show来显示Toast,确保在主线程中执行,并且使用Application Context避免内存泄漏。 注意:如果用户之前使用的是自己封装的toastHelp类,并且出现了闪退,那么可能是因为在子线程调用时没有正确处理线程。我们的新工具类解决了这个问题。 另外,用户提供的引用[1]中的toastHelp类,在子线程中通过捕获异常然后调用`Looper.prepare()`和`Looper.loop()`,这种方法会使得子线程进入消息循环,如果不退出,线程会一直运行。而我们的方法是将Toast的显示切换到主线程,不会阻塞子线程,更加安全。 因此,建议用户使用我们提供的工具类替代原来的toastHelp类。 最后,我们生成相关问题:</think>### 解决Android应用在调用其他类Toast时闪退问题 在子线程中直接调用Toast会导致`Can't create handler inside thread that has not called Looper.prepare()`错误,引发应用闪退。以下是专业解决方案: #### 核心问题分析 1. **线程冲突** Toast必须在UI线程(主线程)中显示,子线程直接调用会导致线程安全异常[^1][^2] 2. **Looper缺失** 子线程默认没有消息循环(Looper),无法处理UI操作[^3][^4] 3. **上下文引用** 跨类调用时Context引用不当可能导致空指针异常 --- ### 专业解决方案 #### 方案1:使用Handler线程调度(推荐) ```java public class ToastHelper { public static void showSafe(final Context context, final String message) { // 获取主线程Handler Handler mainHandler = new Handler(Looper.getMainLooper()); mainHandler.post(new Runnable() { @Override public void run() { // 在主线程执行Toast Toast.makeText(context.getApplicationContext(), message, Toast.LENGTH_SHORT).show(); } }); } } // 在任何类中调用(包括子线程ToastHelper.showSafe(getApplicationContext(), "操作成功"); ``` **优势**: - 100%避免线程冲突 - 自动切换主线程执行 - 使用Application Context避免内存泄漏 #### 方案2:Looper手动管理(适用于纯子线程环境) ```java public class BackgroundThread extends Thread { @Override public void run() { // 准备消息循环 Looper.prepare(); // 安全显示Toast Toast.makeText(getApplicationContext(), "后台任务完成", Toast.LENGTH_SHORT).show(); // 启动消息循环 Looper.loop(); // 任务完成后退出Looper Looper.myLooper().quit(); } } ``` **适用场景**: - 长期运行的后台线程 - 需要多次显示Toast的场景 #### 方案3:Activity.runOnUiThread(Activity内调用) ```java // 在任意类中(需要Activity引用) public class NetworkUtil { public static void showResult(Activity activity, String msg) { activity.runOnUiThread(() -> { Toast.makeText(activity.getApplicationContext(), msg, Toast.LENGTH_SHORT).show(); }); } } ``` --- ### 最佳实践总结 1. **线程安全第一** 任何非UI线程调用Toast必须使用主线程调度器 2. **上下文使用规范** - 优先使用`getApplicationContext()` - 避免持有Activity强引用 3. **工具类封装** 创建全局Toast工具类统一管理显示逻辑 4. **Looper生命周期** 手动创建Looper后必须调用`quit()`释放资源 --- ### 完整线程安全工具类 ```java public class ToastUtils { private static WeakReference<Context> appContextRef; // 初始化(在Application中调用) public static void init(Context context) { appContextRef = new WeakReference<>(context.getApplicationContext()); } public static void show(final String message) { if (appContextRef == null || appContextRef.get() == null) { throw new IllegalStateException("ToastUtils not initialized!"); } new Handler(Looper.getMainLooper()).post(() -> { Context context = appContextRef.get(); if (context != null) { Toast.makeText(context, message, Toast.LENGTH_SHORT).show(); } }); } } // Application中初始化 public class MyApp extends Application { @Override public void onCreate() { super.onCreate(); ToastUtils.init(this); } } // 任何位置调用 new Thread(() -> { // 子线程安全调用 ToastUtils.show("从子线程安全显示"); }).start(); ``` > **关键原则**: > 1. UI操作必须位于主线程 > 2. 使用Application Context避免内存泄漏 > 3. 工具类统一管理Toast调用 > 4. 子线程使用Looper需配套退出机制[^3][^4]
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

No Silver Bullet

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值