面試官:子線程 真的不能更新UI ?

咱們從一個異常提及:java

android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
        at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:8820)
        at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:1530)
        at android.view.View.requestLayout(View.java:24648)
        at android.widget.TextView.checkForRelayout(TextView.java:9752)
        at android.widget.TextView.setText(TextView.java:6326)
        at android.widget.TextView.setText(TextView.java:6154)
        at android.widget.TextView.setText(TextView.java:6106)
        at com.hfy.demo01.MainActivity$9.run(MainActivity.java:414)
        at android.os.Handler.handleCallback(Handler.java:888)
        at android.os.Handler.dispatchMessage(Handler.java:100)
        at android.os.Looper.loop(Looper.java:213)
        at android.app.ActivityThread.main(ActivityThread.java:8147)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:513)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1101)
複製代碼

通常狀況,咱們在子線程直接操做UI,沒有用handler切到主線程,就會報這個錯。android

==那若是我說,我這裏的這個錯誤就發生在 主線程,你信嗎?==安全

下面是具體代碼,handleAddWindow()按在MainActivity 的onCreate中執行。app

private void handleAddWindow() {

        //子線程建立window,只能由這個子線程訪問 window的view
        Button button = new Button(MainActivity.this);
        button.setText("添加到window中的button");
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                MyToast.showMsg(MainActivity.this, "點了button");
            }
        });

        new Thread(new Runnable() {
            @Override
            public void run() {
				//由於添加window是IPC操做,回調回來時,須要handler切換線程,因此須要Looper
                Looper.prepare();

                addWindow(button);

                new Handler().postDelayed(new Runnable() {
                    @Override
                    public void run() {
                        button.setText("文字變了!!!");
                    }
                },3000);
                
				//開啓looper,循環取消息。
                Looper.loop();
            }
        }).start();

        //這裏執行就會報錯:Only the original thread that created a view hierarchy can touch its views.
        new Handler().postDelayed(new Runnable() {
            @Override
            public void run() {
                button.setText("文字 you 變了!!!");
            }
        },4000);
    }

    private void addWindow(Button view) {
        WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams(
                WindowManager.LayoutParams.WRAP_CONTENT,
                WindowManager.LayoutParams.WRAP_CONTENT,
                0, 0,
                PixelFormat.TRANSPARENT
        );
        // flag 設置 Window 屬性
        layoutParams.flags= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;
        // type 設置 Window 類別(層級)
        layoutParams.type = WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            layoutParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
        }

        layoutParams.gravity = Gravity.TOP | Gravity.LEFT;
        layoutParams.x = 100;
        layoutParams.y = 100;

        WindowManager windowManager = getWindowManager();
        windowManager.addView(view, layoutParams);
    }
複製代碼

主要是:開了個子線程,而後添加了一個系統window,window中只有一個button。而後3秒後在子線程中直接改變Button的文字,而後又過一秒,在主線程中再改變button文字。ide

(其中涉及知識有handlerwindow。可點擊查看相關知識)oop

執行效果以下,可見 打開App後,左上角的Button,3秒後變了,接着一秒後crash了。 post

在這裏插入圖片描述

那爲啥 子線程更新UI沒報錯,主線程報錯呢?ui

首先,咱們看報錯緣由的描述: Only the original thread that created a view hierarchy can touch its views. 翻譯就是說 只有建立了view樹的線程,才能訪問它的子view。並無說子線程必定不能訪問UI。那能夠猜測到,button的確實是在子線程被添加到window中的,子線程確實能夠直接訪問,而主線程訪問確實會拋出異常。看來能夠解釋這個錯誤的緣由了。 下面就具體分析下。this

錯誤的發生在ViewRootImpl的checkThread方法中,且UI的更新都會走到這個方法:spa

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

(ViewRootImpl相關知識能夠戳這裏View的工做原理

經過window的相關知識,咱們知道,調用windowManager.addView添加window時會給這個window建立一個ViewRootImpl實例:

public void addView(View view, ViewGroup.LayoutParams params, Display display, Window parentWindow) {
        ...

        	ViewRootImpl root;
        	View panelParentView = null;
        ...
            root = new ViewRootImpl(view.getContext(), display);
            view.setLayoutParams(wparams);
            mViews.add(view);
            mRoots.add(root);
            mParams.add(wparams);
...
        }
    }
複製代碼

而後ViewRootImpl構造方法中會拿到當前的線程,

public ViewRootImpl(Context context, Display display) {
        mContext = context;
        ...
        mThread = Thread.currentThread();
        ...
    }
複製代碼

因此在ViewRootImpl的checkThread()中,確實是 拿 當前想要更新UI的線程 和 添加window時的線程做比較,不是同一個線程機會報錯。

經過window的相關知識,咱們還知道,Activity也是一個window,window的添加是在ActivityThread的handleResumeActivity()。ActivityThread就是主線程,因此Activity的view訪問只能在主線程中

通常狀況,UI就是指Activity的view,這也是咱們一般稱主線程爲UI線程的緣由,其實嚴謹叫法應該是activity的UI線程。而咱們這個例子中,這個子線程也能夠稱爲button的UI線程。

那爲啥要必定須要checkThread呢?根據handler的相關知識:

由於UI控件不是線程安全的。那爲啥不加鎖呢?一是加鎖會讓UI訪問變得複雜;二是加鎖會下降UI訪問效率,會阻塞一些線程訪問UI。因此乾脆使用單線程模型處理UI操做,使用時用Handler切換便可。

咱們再看一個問題,Toast能夠在子線程show嗎答案是能夠的

new Thread(new Runnable() {
            @Override
            public void run() {
                //由於添加window是IPC操做,回調回來時,須要handler切換線程,因此須要Looper
                Looper.prepare();

                addWindow(button);

                new Handler().postDelayed(new Runnable() {
                    @Override
                    public void run() {
                        button.setText("文字變了!!!");
                    }
                },3000);

                Toast.makeText(MainActivity.this, "子線程showToast", Toast.LENGTH_SHORT).show();

                //開啓looper,循環取消息。
                Looper.loop();
            }
        }).start();
複製代碼

在上面的例子,線程中showToast,運行發現確實能夠的。由於根據window的相關知識,知道Toast也是window,show的過程就是添加Window的過程。

另外注意1,這個線程中Looper.prepare()和Looper.loop(),這是必要的。 由於添加window的過程是和WindowManagerService進行IPC的過程,IPC回來時是執行在binder線程池的,而ViewRootImpl中是默認有Handler實例的,這個handler就是用來切換binder線程池的消息到當前線程。 另外Toast還與NotificationMamagerService進行IPC,也是須要Handler實例。既然須要handler,那因此線程是須要looper的。另另外Activity還與ActivityManagerService進行IPC交互,而主線程是默認有Looper的。 擴展開,想在子線程show Toast、Dialog、popupWindow、自定義window,只要在先後調Looper.prepare()和Looper.loop()便可。

另外注意2,在activity的onCreate到首次onResume的時期,建立子線程在其中更新UI也是能夠的。這不是違背上面的結論了嗎?其實沒有,上面說了,由於Activity的window添加在首次onResume以後執行的的,那ViewRootImpl的建立也是在這以後,因此也就沒法checkThread了。實際上這個時期也不checkThread,由於View根本尚未顯示出來。

onCreate()中執行是OK的:

new Thread(new Runnable() {
    @Override
    public void run() {
        tv.setText("text");
    }
}).start();
複製代碼

.

最後,歡迎留言討論,若是你喜歡這篇文章,請幫忙 點贊、收藏和轉發,感謝

歡迎關注個人 公 衆 號

公衆號:胡飛洋
相關文章
相關標籤/搜索