Android面试题——为什么子线程里面不能弹Toast?

最近跟朋友讨论了面试的时候碰到的一道面试题:可以在子线程里面弹Toast吗?为什么

Toast每天都在用,用的理所当然,却没有进去看一眼源码,就这个问题,我搜了网上的资料,然后自己也进去看了一下源码。给同行的朋友们分享下,同时也当做自己的笔记。感谢此篇博客博主的无私分享

https://blog.csdn.net/sinat_17314503/article/details/53015163

首先看这个

public void click(View view) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                Toast.makeText(MainActivity.this,"helloworld",Toast.LENGTH_SHORT).show();
            }
        }).start();
    }

运行报错

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

平时我们用Toast也是直接调用这行代码,那么?直接调用和在子线程里面区别在哪里?

我们看Toast的show方法

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

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

        try {
            service.enqueueToast(pkg, tn, mDuration);
        } catch (RemoteException e) {
            // Empty
        }
    }
static private INotificationManager getService() {
        if (sService != null) {
            return sService;
        }
        sService = INotificationManager.Stub.asInterface(ServiceManager.getService("notification"));
        return sService;
    }

这里涉及到AIDL进程间通讯,这里有个TN类,TN继承自远程NotificationService的代理类,实际弹Toast的工作是在TN这个类里面完成的。

再看TN代理类:

private static class TN extends ITransientNotification.Stub {
        final Runnable mHide = new Runnable() {
            @Override
            public void run() {
                handleHide();
         
                mNextView = null;
            }
        };

      
        final Handler mHandler = new Handler() {
            @Override
            public void handleMessage(Message msg) {
                IBinder token = (IBinder) msg.obj;
                handleShow(token);
            }
        };

        @Override
        public void show(IBinder windowToken) {
            if (localLOGV) Log.v(TAG, "SHOW: " + this);
            mHandler.obtainMessage(0, windowToken).sendToTarget();
        }

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

        public void handleShow(IBinder windowToken) {
            //省略
        }

        public void handleHide() {
           //省略
        }
    }
}

这里TN里的show 和hide方法实际上都是通过handler发送消息,执行相应的show 和hide任务。

重点来了!!!:

弹Toast过程实际上是TN类向当前线程发送消息,执行相应show 和hide任务过程,这里又涉及到Handler的消息机制:

Handler的消息机制涉及到以下三个重要的类Handler、消息队列MessageQueue、轮询器Looper,该消息机制要完整运行这三个必不可少,而Handler的创建过程中:

 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;
    }

必要条件之一是需要一个轮询器Looper, 这里大家已经看到开头的那个报错的异常了--没错,是因为缺少looper轮询器。那为什么主线程不会报错呢?因为主线程在创建的时候已经默认执行了Looper.prepare()这个方法并且调用Looper.loop()使其开始轮询工作。而子线程是没有默认给创建这个Looper的 ,这也解释了为什么 mLooper = Looper.myLooper();返回的是一个空的对象

好的,那么我是不是也可以模仿主线程,在子线程中创建一个Looper对象并使之开始工作呢,TN类创建Handler的条件不就有了吗?尝试一下:

public void click(View view) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                Looper.prepare();
                Toast.makeText(MainActivity.this,"helloworld",Toast.LENGTH_SHORT).show();
                Looper.loop();
            }
        }).start();
    }

加上Looper的创建和开始轮询,在运行 ,现在你的子线程同样也可以弹Toast消息了。

感谢博主分享的博客https://blog.csdn.net/sinat_17314503/article/details/53015163。以前只做伸(tai)手(lan)党(l),其中有何不对之处,忘朋友们多多指正!

猜你喜欢

转载自blog.csdn.net/fisheryujie/article/details/81544909