Handler--为什么在子线程中可以弹吐司却不能UI绘制

    private void initDeal() {
       Thread hanMeiMeiThread = new Thread(){
           @Override
           public void run() {
               Looper.prepare();
               mHandler = new Handler(){
                   @Override
                   public void handleMessage(Message msg) {
                       Log.e(TAG, "hanMeiMei receiver message: " + ((String) msg.obj));
//                       mShowTv.setText((String) msg.obj);
                       Toast.makeText(HomeActivity.this, ((String) msg.obj), Toast.LENGTH_SHORT).show();
                   }
               };
               Looper.loop();
           }
       };

        Thread liLeiThread = new Thread() {
            @Override
            public void run() {
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                Message message = mHandler.obtainMessage();
                message.obj = "Hi MeiMei";
                mHandler.sendMessage(message);
            }
        };
        hanMeiMeiThread.setName("韩梅梅 Thread");
        hanMeiMeiThread.start();
        liLeiThread.setName("李雷 Thread");
        liLeiThread.start();
    }

以上代码是线程间通信,使用Looper方式在子线程中创建handler,但是为什么可以在子线程中弹出toast却不能以此进行ui更改呢?

 

主要是以下原因:

     因为Handler是依赖于Looper的,所以创建Handler的时候必须先能够获取Looper,在工程启动的时候,会进入ActivityThread的main方法中,其中已经通过Looper.prepareMainLooper()方法进行了创建了全局唯一的UI looper,所以在主线程中,可以直接创建handler,而子线程却需要Looper.prepare()创建,通过Looper.loop()不断轮训执行消息,回到上面代码当中,虽然通过mHandler.sendMessage(message) 发送了消息,但是进入到了子线程中创建的handler,而UI控件不是线程安全的,如果在多线程中并发访问可能会导致UI控件处于不可预期的状态,所以会有线程检查,因此会抛出异常

   

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

 上述代码中Toast.show(),可以看到是创建了TN对象,放入enqueueToast中,让我们进入TN中

TN(String packageName, @Nullable Looper looper) {
            // 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;

            mPackageName = packageName;

            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) {
                    switch (msg.what) {
                        case SHOW: {
                            IBinder token = (IBinder) msg.obj;
                            handleShow(token);
                            break;
                        }
                        case HIDE: {
                            handleHide();
                            // Don't do this in handleHide() because it is also invoked by
                            // handleShow()
                            mNextView = null;
                            break;
                        }
                        case CANCEL: {
                            handleHide();
                            // Don't do this in handleHide() because it is also invoked by
                            // handleShow()
                            mNextView = null;
                            try {
                                getService().cancelToast(mPackageName, TN.this);
                            } catch (RemoteException e) {
                            }
                            break;
                        }
                    }
                }
            };
        }
看到Switch中的Show中,handleShow() 
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);
                // 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;
                mParams.packageName = packageName;
                mParams.hideTimeoutMilliseconds = mDuration ==
                    Toast.LENGTH_LONG ? LONG_DURATION_TIMEOUT : SHORT_DURATION_TIMEOUT;
                mParams.token = windowToken;
                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);
                // Since the notification manager service cancels the token right
                // after it notifies us to cancel the toast there is an inherent
                // race and we may attempt to add a window after the token has been
                // invalidated. Let us hedge against that.
                try {
                    mWM.addView(mView, mParams);
                    trySendAccessibilityEvent();
                } catch (WindowManager.BadTokenException e) {
                    /* ignore */
                }
            }

到了这里我们可以发现

Toast本质是通过window显示和绘制的(操作的是window),而主线程不能更新UI 是因为ViewRootImpl的checkThread方法在Activity维护的View树的行为。 Toast中TN类使用Handler是为了用队列和时间控制排队显示Toast,所以为了防止在创建TN时抛出异常,需要在子线程中使用Looper.prepare();和Looper.loop();

 

 

 

线程(Thread)、循环器(Looper)、处理器(Handler)之间的关系如下:

  • 一个线程(Thread)只能绑定一个循环器(Looper);但一个Thread可以有多个处理器(Handler)。
  • 一个循环器(Looper)可绑定多个处理器(Handler)。
  • 一个处理器(Handler)只能绑定一个循环器(Looper)

handler:绑定到一个线程上,一个线程可以有多个handler

looper:线程跟looper是一一对应的,所以looper不能被调用两次否则会抛出异常

messge:handler利用message来携带消息

messagQueue:用来状态message,一个looper对应一个消息队列

你可能感兴趣的:(Android,Toast,Handler)