面试官:子线程 真的不能更新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();
复制代码

.

最后,欢迎留言讨论,若是你喜欢这篇文章,请帮忙 点赞、收藏和转发,感谢

欢迎关注个人 公 众 号

公众号:胡飞洋