1. 程式人生 > >android-如何在子執行緒中更新ui

android-如何在子執行緒中更新ui

正如我們知道的,android是不讓在子執行緒中更新ui的。在子執行緒中更新ui會直接丟擲異常

  • Only the original thread that created a view hierarchy can touch its views
  • 那麼這種檢查機制在什麼時候發生的呢?
  • 那麼真的不能在子執行緒中更新ui麼?我們帶著這個疑問來看一下系統程式碼

我們知道android中的view的更新(大小,位置,內容)全部都交給了WindowManager,那麼我們帶著疑問來看下WindowMagager介面的實現類WindowManagerImpl,中如何控制對view的更新的

  • 我們知道WindowManager中有三個常用方法 addView(),removeView()和updateViewLayout();
  • 接下來我們只分析updateViewLayout()方法。

    public void updateViewLayout(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
        applyDefaultToken(params);
        mGlobal.updateViewLayout(view, params);
    }
    
    • applyDefaultToken(params);方法和Window的層級有關係,這裡和我們探討的view的跟新沒有關係,因此跳過
    • mGlobal.updateViewLayout(view, params); 發現windowManager的更新其實是交給了mGlobal來操作了,那麼mGlobal是什麼呢?

      private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();
      
    • 發現mGlobal其實是WindowManaerImpl一個成員變數,而且還是單例。其實WindowManagerImpl的跟新委託給了WindowManagerGlobal
    • 那麼WindowManagerGlobal的updateViewLayout()方法裡面完成了什麼功能呢?

        public void updateViewLayout(View view, ViewGroup.LayoutParams params) {
      if (view == null) {
          throw new IllegalArgumentException("view must not be null");
      }
      if (!(params instanceof WindowManager.LayoutParams)) {
          throw new IllegalArgumentException("Params must be WindowManager.LayoutParams");
      }
      
      final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams)params;
      
      view.setLayoutParams(wparams);
      
      synchronized (mLock) {
          int index = findViewLocked(view, true);
          ViewRootImpl root = mRoots.get(index);
          mParams.remove(index);
          mParams.add(index, wparams);
          root.setLayoutParams(wparams, false);
      }
      

      }

    • 前半部分是異常判斷,跳過
    • 下面是給view設定佈局引數,新的佈局引數。

       final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams)params;
      
      view.setLayoutParams(wparams);
      
    • 下面是找到viewRootImpl,給root重新設定佈局引數。

          int index = findViewLocked(view, true);
          ViewRootImpl root = mRoots.get(index);
          mParams.remove(index);
          mParams.add(index, wparams);
          root.setLayoutParams(wparams, false);
      
    • 那麼ViewRootImpl是什麼呢?其實是android系統中view和WindowManager通訊的橋樑。比如測量 佈局 繪製 時間分發 都是在這裡傳遞給view的
    • 接下來我們分析 root.setLayoutParams(wparams, false);這段程式碼。

      if (newView) {
          mSoftInputMode = attrs.softInputMode;
          requestLayout();
      }
      
    • 程式碼比較長,這裡擷取部分程式碼 requestLayout();
    • 那麼requestLayout中做了什麼操作呢?

      public void requestLayout() {
      if (!mHandlingLayoutInLayoutRequest) {
          checkThread();
          mLayoutRequested = true;
          scheduleTraversals();
      }
      

      }

    • 終於到了重點 checkThread(),在這個方法中做了一個判斷,就是當前更新ui的執行緒是否和ViewRootImpl建立的執行緒是否是同一個,不是則丟擲異常
    • 下面是checkThread程式碼

        void checkThread() {
      if (mThread != Thread.currentThread()) {
          throw new CalledFromWrongThreadException(
                  "Only the original thread that created a view hierarchy can touch its views.");
      }
      

      }

    • 那麼mThread是什麼時候建立的呢?下面我們看下ViewRootImpl的構造方法


* 那麼viewRootImpl物件什麼時候建立的呢?其實在WindowManagerImpl的addview中呼叫了WindowManagerGlobal的addview。在WindowManagerGlobal的addView的時候建立了ViewRootImpl物件

  • 現在我們終於理清楚了,不能在子執行緒中更新ui的原因。
    • 如果ViewRootImpl是在更新ui的時候,做了一個判斷。判斷建立自己的執行緒和更新ui的執行緒是否是同一個,不是,直接異常。
    • 那麼我們能否手動的建立一個子執行緒,在這個執行緒中建立一個viewRootImpl呢?
    • 下面我們帶著疑問寫一個demo
    • 先看效果圖

  • 下面是我們點選之後。在子執行緒中更新ui的效果圖

  • 程式碼的原理是,我們在子執行緒中通過WindowManager新增一個view,而這個window所有的層級是系統層級。因此有懸浮效果。而我們建立的這個view因為是在子執行緒中直接建立了一個window,這個window的級別比較高,所以能顯示在其他應用上面。而這個window又沒有父window,因此其會單獨建立ViewRootImpl物件,而這個物件又是在子執行緒中建立的,那麼我們更新ui的時候,在這個子執行緒中更新能夠成功
  • 下面是核心程式碼,我們將會一步一步對其進行分析

       new Thread() {
        @Override
        public void run() {
            Looper.prepare();
            wm = (WindowManager) MyApplication.ctx.getSystemService(WINDOW_SERVICE);
            view = View.inflate(MainActivity.this, R.layout.item, null);
            tv = (TextView) view.findViewById(R.id.tv);
            params = new WindowManager.LayoutParams();
            params.type = WindowManager.LayoutParams.TYPE_SYSTEM_ERROR;// 設定最大的層級 以便顯示在其他應用的上面
            // 設定不攔截焦點
            params.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;
            params.width = (int) (60 * getResources().getDisplayMetrics().density);
            params.height = (int) (60 * getResources().getDisplayMetrics().density);
            params.gravity = Gravity.LEFT | Gravity.TOP;// 且設定座標系 左上角
            params.format = PixelFormat.TRANSPARENT;
            width = wm.getDefaultDisplay().getWidth();
            height = wm.getDefaultDisplay().getHeight();
            params.y = height / 2 - params.height / 2;
            wm.addView(view, params);
    
            view.setOnTouchListener(new View.OnTouchListener() {
    
                private int y;
                private int x;
    
                @Override
                public boolean onTouch(View v, MotionEvent event) {
                    switch (event.getAction()) {
                        case MotionEvent.ACTION_DOWN:
                            x = (int) event.getRawX();
                            y = (int) event.getRawY();
                            break;
                        case MotionEvent.ACTION_MOVE:
                            int minX = (int) (event.getRawX() - x);
                            int minY = (int) (event.getRawY() - y);
                            params.x = Math.min(width - params.width, Math.max(0, minX + params.x));
                            params.y = Math.min(height - params.height, Math.max(0, minY + params.y));
                            wm.updateViewLayout(view, params);
                            x = (int) event.getRawX();
                            y = (int) event.getRawY();
                            break;
                        case MotionEvent.ACTION_UP:
                            if (params.x > 0 && params.x < width - params.width) {
                                int x = params.x;
                                if (x > (width - params.width) / 2) {
                                    params.x = width - params.width;
                                } else {
                                    params.x = 0;
                                }
                                wm.updateViewLayout(view, params);
    
                            } else if (params.x == 0 || params.x == (width - params.width)) {
                                Toast.makeText(MainActivity.this, "被電擊了", Toast.LENGTH_SHORT).show();
                                tv.setText("abcd");
                            }
                            break;
                    }
                    return true;
                }
            });
            Looper.loop();
        }
    }.start();
    
    • 首先準備Looper,之後loop。因為更新view的時候會在當前的子執行緒中使用handler。而使用handler必須要looper。
    • 接下來拿到windowManager wm = (WindowManager) MyApplication.ctx.getSystemService(WINDOW_SERVICE);
    • 填充view
    • WindowManager.LayoutParams.TYPE_SYSTEM_ERROR; 設定type,將window的級別設定較大,能夠顯示在其他的window之上
    • params.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;這裡是設定window透傳,也就是當前view所在的window不阻礙底層的window獲得觸控事件。
    • 接下來設定window的寬度和高度
    • params.format = PixelFormat.TRANSPARENT;設定透明 否則的話 圓形view後面顯示一層黑色,預設效果是黑色。需要設定,才能體現出圓形。
    • 接下來就是設定Gravity了,這裡比較簡單,因為想實現懸浮視窗的拖拽效果,因此需要修改WindowManager的LayoutParams的x,y值。因此需要和gravity配合使用
    • 接下來就是將view新增到WindowManager中了
    • 剩下的就是觸控事件了
    • 在鬆手的時候判斷了,更新了view中顯示的ui
    • 下面是更新效果圖

  • 初始文字為Click

因此能否在子執行緒中更新ui,由ViewRootImpl在哪個執行緒中建立決定。因此我們更應該將能更新ui的執行緒成為ui執行緒而不是主執行緒。