关于Toast不能在没有Looper的子线程使用

本文探讨了Android Toast不能在没有Looper的线程使用的原因,通过源码分析发现,Toast的显示涉及到NotificationManagerService和Binder机制。在makeText方法中传递的looper在show过程中用于线程切换,保证回调在正确的线程执行,这与AIDL和binder的线程模型有关。

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

  Andorid中的Toast是一种比较常见的系统提示框。因为常见,所以常常忽略了其细节。经常使用的人都知道,Toast不能在没有Looper的线程显示提示框。那么下面通过源码来探究其原理。
  先来看看其makeText方法:

public static Toast makeText(Context context, CharSequence text, @Duration int duration) {
        return makeText(context, null, text, duration);
    }

public static Toast makeText(@NonNull Context context, @Nullable Looper looper,
            @NonNull CharSequence text, @Duration int duration) {
        Toast result = new Toast(context, looper);//创建对象

        LayoutInflater inflate = (LayoutInflater)
                context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        View v = inflate.inflate(com.android.internal.R.layout.transient_notification, null);
        TextView tv = (TextView)v.findViewById(com.android.internal.R.id.message);
        tv.setText(text);

        result.mNextView = v;
        result.mDuration = duration;

        return result;
    }

  好像看不出来什么端倪。再来看看show方法:

public void show() {
        if (mNextView == null) {
            throw new RuntimeException("setView must have been called");
        }

        INotificationManager service = getService();//NotificationManagerService
        String pkg = mContext.getOpPackageName();
        TN tn = mTN;//...
        tn.mNextView = mNextView;

        try {
            service.enqueueToast(pkg, tn, mDuration);//IPC调用
        } catch (RemoteException e) {
            // Empty
        }
    }

static private INotificationManager getService() {
        if (sService != null) {
            return sService;
        }
        sService = INotificationManager.Stub.asInterface(ServiceManager.getService("notification"));
        return sService;
    }

  发现有两个令人非常在意的地方。一个是其show方法明显调用的是代理类的方法。通过getService可以看出来,真正的enqueueToast是在NotificationManagerService中实现的。
  第二个是入参,tn是我们未知的,那么回头去找一找,发现在刚才的makeText方法中有一个new Toast()的操作,回去看一看:

public Toast(@NonNull Context context, @Nullable Looper looper) {
        mContext = context;
        mTN = new TN(context.getPackageName(), looper);//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);
    }

  找到了mTN,那么这个mTN是何方神圣呢?看下面:

TN(String packageName, @Nullable Looper looper) {
            ......

            if (looper == null) {
                // Use Looper.myLooper() if looper is not specified.
                looper = Looper.myLooper();//如果当前线程调用了Looper.prepare(),那么这里是会拿到当前线程的looper的。详情参考Handler有关文章。
                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) {
                    switch (msg.what) {
                        case SHOW: {
                            IBinder token = (IBinder) msg.obj;
                            handleShow(token);
                            break;
                        }
                        case HIDE: {
                            handleHide();
                            mNextView = null;
                            break;
                        }
                        case CANCEL: {
                            handleHide();
                            mNextView = null;
                            try {
                                getService().cancelToast(mPackageName, TN.this);
                            } catch (RemoteException e) {
                            }
                            break;
                        }
                    }
                }
            };
        }

  这下找到原因了,原来异常是在这里抛出来的。这个looper是我们在makeText方法中一路传过来的。

return makeText(context, null, text, duration);

  那么为什么要这么做呢?为什么这里要特别对looper做要求呢?回到刚才的show方法,是不是把tn当作参数传给了NotificationManagerServer。了解过AIDL和binder机制之后,我有一个大胆的想法,那就是这个tn是一个回调接口,NotificationManagerServer会调用其对应的方法,由于binder运行在binder线程池,所以有这里的线程切换要求。
  结合TN这个类来看,果然如此。·

private static class TN extends ITransientNotification.Stub{
    ......
    @Override
        public void show(IBinder windowToken) {
            if (localLOGV) Log.v(TAG, "SHOW: " + this);
            mHandler.obtainMessage(SHOW, windowToken).sendToTarget();
        }
    ......
}

  看TN的父类形式就是一个典型的BinderServer,这里的show方法和Toast里面的show方法不同,这个方法由
NotificationManagerServer调用,然后会切换到调用Toast.makeText的线程,然后调用handlerShow方法。如下:

public void handleShow(IBinder windowToken) {
            if (localLOGV) Log.v(TAG, "HANDLE SHOW: " + this + " mView=" + mView
                    + " mNextView=" + mNextView);
            // If a cancel/hide is pending - no need to show - at this point
            // the window token is already invalid and no need to do any work.
            if (mHandler.hasMessages(CANCEL) || mHandler.hasMessages(HIDE)) {
                return;
            }
            if (mView != mNextView) {
                // remove the old view if necessary
                handleHide();
                mView = mNextView;
                Context context = mView.getContext().getApplicationContext();
                String packageName = mView.getContext().getOpPackageName();
                if (context == null) {
                    context = mView.getContext();
                }
                mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
                ......
                try {
                    mWM.addView(mView, mParams);
                    trySendAccessibilityEvent();
                } catch (WindowManager.BadTokenException e) {
                    /* ignore */
                }
            }
        }

  这里就是常规的addView操作。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值