美文网首页Android
Android 源码分析问题(一)—— 子线程不能更新UI吗?

Android 源码分析问题(一)—— 子线程不能更新UI吗?

作者: JzyCc | 来源:发表于2019-02-12 20:35 被阅读0次

    问题引入

    Android 开发法则之一不能在子线程更新 UI,这个问题主要是 Android 关于 View 的一系列操作有一套十分复杂的逻辑。

    举个很简单的例子,如果一个 View 被修改,如果修改的是尺寸,那么势必引起一系列的重新测量,布局,绘制。这个过程是非常耗时的,如果在一个并发的情况下,那么就可能会引起各种问题,比如给一个 View 修改了尺寸,那么这个 View 会引起界面的重绘,这时另一个 View 也修改了尺寸,那么也会引起重绘,这时候一系列的计算和操作会在并发的情况下穿插进行,不说对程序会造成什么影响,但是每个过程中的计算肯定是错误百出的。

    所以 Google 在我们对 View 在子线程进行更新做了一个线程判断:如果修改 View 的线程不在主线程,那么便会抛出异常,引起程序崩溃。

    源码分析

    为什么不能在子线程更新UI

    我们先画一个 TextView

        <TextView
            android:id="@+id/tv_text"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Hello World!"/>
    

    我在 onCreate() 方法中为一个 textView 修改 text。

        new Thread(new Runnable() {
            @Override
            public void run() {
                try{
                    //为什么要睡眠后面会说
                    Thread.sleep(200);
                }catch (InterruptedException e){
                }
                textView.setText();
            }
        }).start(); 
    
        android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
            at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:7753)
            at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:1225)
    

    这里我贴出了相关的报错,可以看到其中报错方法中有一个很眼熟的 requestLayout() 方法。

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

    这里如果对 requestLayout() 方法不熟悉的朋友可以去查阅下,它在 ViewRootImpl 中的功能就是让 ViewRoot 开始 scheduleTraversals() 方法,这个方法里便包含了我们熟悉的测量,布局,绘制。

    如果我们修改了一个 View,如果修改结果影响了它的尺寸,那么就会触发这个方法,并开始新的测量、布局、绘制等操作,在它里面有个 checkThread() 方法。

    这时候我们看向 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 这个值是在什么时候被赋值的。

        //这里我对这句赋值代码的上下部分做了省略。
        public ViewRootImpl(Context context, Display display) {
            ...
            mThread = Thread.currentThread();
            ...
        }
    

    可以看到 mThread 是在 ViewRootImpl 的构造方法中被赋值,此时的 Thread.currentThread() 就是主线程。

    看到这里已经可以知道为什么不能在子线程更新UI了。

    在子线程更新UI

    我先解决一下上面的问题,为什么我在 onCreate() 方法中开启新线程先要让它 sleep 一会再更新 View 呢。

    如果有兴趣可以写一个这样的例子,分别运行下有无 sleep 的情况看下。

    我们去掉 sleep() 方法后可以发现,程序正常运行,并没有报错。

    这里我比较好奇的试了下,不 sleep 子线程的情况下分别在 onStart()、onResume() 、 onPause()等方法依次尝试。

    神奇的发现,在 onPause() 方法中,才会引起程序崩溃。

    这时候可以简单的分析一下,首先我们是在 ViewRootImpl 中判断是否在主线程的,而 ViewRootImpl 中的 mThread 是在构造方法中被赋值的,这时候我们应该可以猜测下 ViewRootImpl 是在 onResume() 回调和 onPause() 回调之间被构造。

    直接源码走到 ActivityThread 的 handleResumeActivity() 方法。

    这里只显示关键代码

    
        @Override
        public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward,
                String reason) {
            // TODO Push resumeArgs into the activity for consideration
            //其实在这里执行的 onResume() 回调
            final ActivityClientRecord r = performResumeActivity(token, finalStateRequest, reason);
            if (r == null) {
                // We didn't actually resume the activity, so skipping any follow-up actions.
                return;
            }
            
            final Activity a = r.activity;
            
            if (r.activity.mVisibleFromClient) {
                //这时候视图才可见
                r.activity.makeVisible();
            }
        }
    

    上面的注解引出了一个小知识点,这里不详解了。

    一个 Activity 在被用户可见时是执行了 makeVisible() 方法之后,进去一看。

        void makeVisible() {
            if (!mWindowAdded) {
                ViewManager wm = getWindowManager();
                //这里添加了 view
                wm.addView(mDecor, getWindow().getAttributes());
                mWindowAdded = true;
            }
            mDecor.setVisibility(View.VISIBLE);
        }
    

    进到 addView() 方法中一看。

        @Override
        public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
            applyDefaultToken(params);
            mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
        }
    

    这里又调用了 WindowManagerGlobal 中的 addView 方法。

     public void addView(View view, ViewGroup.LayoutParams params,
                Display display, Window parentWindow) {
                  view.setLayoutParams(wparams);
                ...
                root = new ViewRootImpl(view.getContext(), display);
                mViews.add(view);
                mRoots.add(root);
                mParams.add(wparams);
    
                // do this last because it fires off messages to start doing things
                try {
                    root.setView(view, wparams, panelParentView);
                } catch (RuntimeException e) {
                    // BadTokenException or InvalidDisplayException, clean up.
                    if (index >= 0) {
                        removeViewLocked(index, true);
                    }
                    throw e;
                }
                ...
        }
    

    可以看见这里终于创建了一个 ViewRootImpl,并且将所有 View 的根 View —— DecorView 传入(这里就不对 DecorView 介绍了)。

    总结?

    知道了 ViewRootImpl 的构造时机,我们终于知道了可以在 onResume() 以及其之间的回调在子线程更新 UI, 但这样我们就可以随意在子线程更新 UI 了吗,不,虽然不会有隐患,但是我们给 UI 的更新时序将会因为异步问题被打乱,并且如果其中有耗时操作,当 ViewRootImpl 被创建后,那么就会引起报错。。

    为什么总结后面有个问号? 不要以为这样就是最终答案了。

    我们换个思路,如何避开 checkThread() 方法的检测。

    怎么样更新 UI 不会被检测当前线程

    这是一个比较有意思的发现。

    这里我们将 TextView 的高宽改成固定大小。

        <TextView
            android:id="@+id/tv_text"
            android:layout_width="100dp"
            android:layout_height="100dp"
            android:text="Hello World!"/>
    

    这时候我们在 ViewRootImpl 构造完之后的时机修改它的文本内容。

    另人惊讶的是程序仍在正常运行,这是为什么呢?

    或许我们还可以调用 View 的 layout 方法修改它的布局,调用 invalidate() 让 View 重绘,但是我们发现程序并没出现我们想象的崩溃问题。

    还记得我们之前抛出的报错信息中的 requestLayout() 方法吗,这个方法中有一个 checkThread() 方法来检测线程。

    很明显,如果我们触发了 checkThread() 方法,那么一定会抛出异常。

    很明显,当我们更新 View 时,如果引起了它的尺寸变化,那么将会重新引起测量。需要重新测量时将会调用到 ViewRootImpl 的一系列 request 方法,在这里 ViewRootImpl 将会检测线程。

    总结 二

    当我们更新 UI 时不涉及到测量相关的操作,那么不会抛出异常。

    所以 Google 定义的这个规则主要是防止并发情况下测量导致数据错误等等一系列并发问题。

    所以我们的更新操作如果不涉及到尺寸变动,就可以放心的在子线程更新 UI 吗?

    我的答案依然是不,还是老实点比较好,说不定就凉了呢?

    那如果需要在子线程更新 UI 怎么办

    那可太简单了!

    在子线程调用 runOnUiThread()方法,使用 View 的 postInvalidate() 方法都可以解决问题。

    相关文章

      网友评论

        本文标题:Android 源码分析问题(一)—— 子线程不能更新UI吗?

        本文链接:https://www.haomeiwen.com/subject/avqleqtx.html