Toast在非UI主线程的其他线程中显示报错

本文探讨了在非主线程中使用Toast时遇到的“Can't create handler inside thread that has not called Looper.prepare()”异常问题。分析了Toast依赖Handler进行消息调度的工作原理,并给出了在非主线程正确展示Toast的方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


错误



看看

和Handler联系上了,报 Can't create handler inside thread that has not called Looper.prepare()这个运行时异常。
应该是这里,库scr:
    /**
     * Use the {@link Looper} for the current thread with the specified callback interface
     * and set whether the handler should be asynchronous.
     *
     * Handlers are synchronous by default unless this constructor is used to make
     * one that is strictly asynchronous.
     *
     * Asynchronous messages represent interrupts or events that do not require global ordering
     * with represent to synchronous messages.  Asynchronous messages are not subject to
     * the synchronization barriers introduced by {@link MessageQueue#enqueueSyncBarrier(long)}.
     *
     * @param callback The callback interface in which to handle messages, or null.
     * @param async If true, the handler calls {@link Message#setAsynchronous(boolean)} for
     * each {@link Message} that is sent to it or {@link Runnable} that is posted to it.
     *
     * @hide
     */
    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;
    }

往前看,是在Toast构造中报的。

    /**
     * Construct an empty Toast object.  You must call {@link #setView} before you
     * can call {@link #show}.
     *
     * @param context  The context to use.  Usually your {@link android.app.Application}
     *                 or {@link android.app.Activity} object.
     */
    public Toast(Context context) {
        mContext = context;
        mTN = new TN();
        mTN.mY = context.getResources().getDimensionPixelSize(
                com.android.internal.R.dimen.toast_y_offset);
        mTN.mGravity = context.getResources().getInteger(
                com.android.internal.R.integer.config_toastDefaultGravity);
    }


        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.format = PixelFormat.TRANSLUCENT;
            params.windowAnimations = com.android.internal.R.style.Animation_Toast;
            params.type = WindowManager.LayoutParams.TYPE_TOAST;
            params.setTitle("Toast");
            params.flags = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
                    | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
                    | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
        }

他的显示隐藏靠Handler。

   private static class TN extends ITransientNotification.Stub {
        final Runnable mShow = new Runnable() {
            @Override
            public void run() {
                handleShow();
            }
        };

        final Runnable mHide = new Runnable() {
            @Override
            public void run() {
                handleHide();
                // Don't do this in handleHide() because it is also invoked by handleShow()
                mNextView = null;
            }
        };

        private final WindowManager.LayoutParams mParams = new WindowManager.LayoutParams();
        final Handler mHandler = new Handler();    

        int mGravity;
        int mX, mY;
        float mHorizontalMargin;
        float mVerticalMargin;


        View mView;
        View mNextView;

        WindowManager mWM;

        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.format = PixelFormat.TRANSLUCENT;
            params.windowAnimations = com.android.internal.R.style.Animation_Toast;
            params.type = WindowManager.LayoutParams.TYPE_TOAST;
            params.setTitle("Toast");
            params.flags = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
                    | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
                    | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
        }

        /**
         * schedule handleShow into the right thread
         */
        @Override
        public void show() {
            if (localLOGV) Log.v(TAG, "SHOW: " + this);
            mHandler.post(mShow);
        }

        /**
         * schedule handleHide into the right thread
         */
        @Override
        public void hide() {
            if (localLOGV) Log.v(TAG, "HIDE: " + this);
            mHandler.post(mHide);
        }

        public void handleShow() {
            if (localLOGV) Log.v(TAG, "HANDLE SHOW: " + this + " mView=" + mView
                    + " mNextView=" + mNextView);
            if (mView != mNextView) {
                // remove the old view if necessary
                handleHide();
                mView = mNextView;
                Context context = mView.getContext().getApplicationContext();
                if (context == null) {
                    context = mView.getContext();
                }
                mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
                // We can resolve the Gravity here by using the Locale for getting
                // the layout direction
                final Configuration config = mView.getContext().getResources().getConfiguration();
                final int gravity = Gravity.getAbsoluteGravity(mGravity, config.getLayoutDirection());
                mParams.gravity = gravity;
                if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.FILL_HORIZONTAL) {
                    mParams.horizontalWeight = 1.0f;
                }
                if ((gravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.FILL_VERTICAL) {
                    mParams.verticalWeight = 1.0f;
                }
                mParams.x = mX;
                mParams.y = mY;
                mParams.verticalMargin = mVerticalMargin;
                mParams.horizontalMargin = mHorizontalMargin;
                if (mView.getParent() != null) {
                    if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);
                    mWM.removeView(mView);
                }
                if (localLOGV) Log.v(TAG, "ADD! " + mView + " in " + this);
                mWM.addView(mView, mParams);
                trySendAccessibilityEvent();
            }
        }

        private void trySendAccessibilityEvent() {
            AccessibilityManager accessibilityManager =
                    AccessibilityManager.getInstance(mView.getContext());
            if (!accessibilityManager.isEnabled()) {
                return;
            }
            // treat toasts as notifications since they are used to
            // announce a transient piece of information to the user
            AccessibilityEvent event = AccessibilityEvent.obtain(
                    AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED);
            event.setClassName(getClass().getName());
            event.setPackageName(mView.getContext().getPackageName());
            mView.dispatchPopulateAccessibilityEvent(event);
            accessibilityManager.sendAccessibilityEvent(event);
        }        

        public void handleHide() {
            if (localLOGV) Log.v(TAG, "HANDLE HIDE: " + this + " mView=" + mView);
            if (mView != null) {
                // note: checking parent() just to make sure the view has
                // been added...  i have seen cases where we get here when
                // the view isn't yet added, so let's try not to crash.
                if (mView.getParent() != null) {
                    if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);
                    mWM.removeView(mView);
                }

                mView = null;
            }
        }
    }




没看出有什么问题,没看到调用Handler构造函数的。

但是。。。。。。。。。。。。。。TN类有个成员:
final Handler mHandler = new Handler();
这就是了。。。d
由于非主线程不像主线程已经调用了


public class Activity extends ContextThemeWrapper
        implements LayoutInflater.Factory2,
        Window.Callback, KeyEvent.Callback,
        OnCreateContextMenuListener, ComponentCallbacks2 {
    private static final String TAG = "Activity";
    private static final boolean DEBUG_LIFECYCLE = false;

    /** Standard activity result: operation canceled. */
    public static final int RESULT_CANCELED    = 0;
    /** Standard activity result: operation succeeded. */
    public static final int RESULT_OK           = -1;
    /** Start of user-defined activity results. */
    public static final int RESULT_FIRST_USER   = 1;

    static final String FRAGMENTS_TAG = "android:fragments";

    private static final String WINDOW_HIERARCHY_TAG = "android:viewHierarchyState";
    private static final String SAVED_DIALOG_IDS_KEY = "android:savedDialogIds";
    private static final String SAVED_DIALOGS_TAG = "android:savedDialogs";
    private static final String SAVED_DIALOG_KEY_PREFIX = "android:dialog_";
    private static final String SAVED_DIALOG_ARGS_KEY_PREFIX = "android:dialog_args_";

    private static class ManagedDialog {
        Dialog mDialog;
        Bundle mArgs;
    }
    private SparseArray<ManagedDialog> mManagedDialogs;

    // set by the thread after the constructor and before onCreate(Bundle savedInstanceState) is called.
    private Instrumentation mInstrumentation;
    private IBinder mToken;
    private int mIdent;
    /*package*/ String mEmbeddedID;
    private Application mApplication;
    /*package*/ Intent mIntent;
    private ComponentName mComponent;
    /*package*/ ActivityInfo mActivityInfo;
    /*package*/ ActivityThread mMainThread;
    Activity mParent;
    boolean mCalled;
    boolean mCheckedForLoaderManager;
    boolean mLoadersStarted;
    /*package*/ boolean mResumed;
    private boolean mStopped;
    boolean mFinished;
    boolean mStartedActivity;
    private boolean mDestroyed;
    private boolean mDoReportFullyDrawn = true;
    /** true if the activity is going through a transient pause */
    /*package*/ boolean mTemporaryPause = false;
    /** true if the activity is being destroyed in order to recreate it with a new configuration */
    /*package*/ boolean mChangingConfigurations = false;
    /*package*/ int mConfigChangeFlags;
    /*package*/ Configuration mCurrentConfig;
    private SearchManager mSearchManager;
    private MenuInflater mMenuInflater;

ActivityThread

   public static final void main(String[] args) {
        SamplingProfilerIntegration.start();
        Process.setArgV0("<pre-initialized>");
        Looper.prepareMainLooper();
        ActivityThread thread = new ActivityThread();
        thread.attach(false);
        Looper.loop();
        if (Process.supportsProcesses()) {
            throw new RuntimeException("Main thread loop unexpectedly exited");
        }
        thread.detach();
        String name = (thread.mInitialApplication != null)
            ? thread.mInitialApplication.getPackageName()
            : "<unknown>";
        Slog.i(TAG, "Main thread of " + name + " is now exiting");
    }

 通过Looper.prepareMainLooper(); Looper.loop();已经有自己的Looper了。

结论

因此,由于Toast依赖一个Handler来消息队列,非主线程需要为Toast准备Looper。
类似这样调用:
Looper.prepare();
Toast.makeText(mActivity, R.string.toast_login_fail, Toast.LENGTH_SHORT).show();
//Looper.loop(); 

Looper.loop();会导致后面不执行


								//我是主线程的Handler,可以通过这个
								Looper refMainLooper = mActivity.getMainLooper();
								//等价Looper refMainLooper = Looper.getMainLooper();
								final Handler loginHandlerInMainThreadLooper = new Handler(refMainLooper, new Callback() {

										@Override
										public boolean handleMessage(Message msg)
										{
											// TODO Auto-generated method stub
											Toast volumeToast = (Toast) msg.obj;
											if (volumeToast != null)
												volumeToast.cancel();
											Toast.makeText(mActivity, R.string.toast_login_fail, Toast.LENGTH_SHORT).show();
											
											return false;
										}

									});
								final Thread loginThread = new Thread(new Runnable() {

										@Override
										public void run()
										{
											Looper.prepare();//创建该线程的Looper对象 
											Toast volumeToast = new Toast(mActivity);
											volumeToast.setGravity(Gravity.CENTER, 0, 0);
											volumeToast.setView(inflater.inflate(R.layout.dialog_myprogress, null, false));
											volumeToast.setDuration(30000);

											volumeToast.show();
											Message msg = loginHandlerInMainThreadLooper.obtainMessage(0, volumeToast);
											loginHandlerInMainThreadLooper.sendMessageDelayed(msg, 5000);
											Log.w("Looper", "Looper.loop()");
											//Looper.loop(); 
											
											Log.w("Looper", "Looper end");
										}
									});
								loginThread.start();




### Toast 报错的含义及原因 Toast 是 Android 中用于短暂显示提示信息的一种机制。当开发者遇到 Toast 报错时,通常是因为某些特定条件未满足而导致的功能异常。 #### 1. **子线程中调用 Toast** 在 Android 的设计中,UI 操作必须运行在主线程中。如果尝试在子线程中直接调用 `Toast` 方法,则会抛出类似于 “Can't create handler inside thread that has not called Looper.prepare()” 的错误[^2]。这是因为 `Toast` 内部依赖于 `Handler` 来管理消息队列,而子线程默认情况下并没有初始化 `Looper` 对象及其关联的消息队列。 ##### 解决方案 为了修复此问题,可以将 `Toast` 调用切换到主线程执行。以下是实现方法之一: ```java new Handler(Looper.getMainLooper()).post(new Runnable() { @Override public void run() { Toast.makeText(context, "This is a toast message", Toast.LENGTH_SHORT).show(); } }); ``` 通过这种方式,确保了 `Toast` 显示逻辑始终运行在主线程环境中。 --- #### 2. **权限不足导致的报错** 在 Android 8.0 及更高版本中,使用 `WindowManager.LayoutParams.TYPE_TOAST` 类型可能会触发安全限制,从而引发崩溃或异常行为[^3]。这是由于 Google 加强了对系统窗口类型的访问控制,部分操作可能需要额外声明权限(如 `SYSTEM_ALERT_WINDOW`),或者调整为更合适的窗口类型。 ##### 解决方案 针对此类情况,建议改用官方推荐的安全方式替代自定义窗口设置。例如,在对话框场景下可考虑标准布局配置而强制指定特殊窗口类型;对于文件共享功能则应遵循现代 API 设计指南[^4]。 --- #### 3. **插件集成不当引起的失效** 除了原生开发中的潜在陷阱外,基于框架扩展(如 Vue.js 集成第三方组件库)也可能遭遇类似的兼容性难题。假如发现 JavaScript 层面下的 `$toast` 函数不可用,请核查是否遗漏必要的初始化步骤——即确认目标 SDK 已被正确加载并全局挂载至应用实例之上[^1]。 示例代码片段展示如何规范化引入 Element UI 提供的通知工具: ```javascript import Vue from 'vue'; import { Message } from 'element-ui'; Vue.prototype.$message = Message; // 或者单独启用 Toast 组件... ``` > 注:此处仅作为类比说明用途,并不涉及跨平台混合编程细节。 --- ### 总结 综上所述,无论是移动端还是前端领域,“Toast 报错”的根本诱因往往集中表现为以下几个方面: - 违反单线程模型原则; - 缺少适当授权或许可声明; - 第三方资源绑定失败等问题。 采取针对性措施即可有效规避上述风险点。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值