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對應一個消息隊列

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章