震惊!Android子线程也能修改UI?

作者: CDF_cc7d | 来源:发表于2019-07-13 23:08 被阅读0次

    看到标题我想大部分人会觉得我是标题党,怎么可能在子线程里面修改UI。先别急,慢慢往下看:


    举例

    1. 首先我们来看个例子:
      public class MainActivity extends AppCompatActivity {
        private TextView mTvTest;
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            mTvTest = findViewById(R.id.tv_test);
            new Thread(new Runnable() {
                @Override
                public void run() {
                    mTvTest.setText("子线程修改UI成功");
                }
            }).start();
        }
    }
    

    上述代码就是新开了一个线程,然后在子线程里面给TextView设置文字。那么这段代码会报错么,我们运行一下试试:


    子线程修改TextView.jpeg

    竟然真的修改成功了,感觉这么多年Android白学了,这到底是怎么回事?

    1. 那么我们再来看另外一个例子:
    public class MainActivity extends AppCompatActivity {
        private TextView mTvTest;
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            mTvTest = findViewById(R.id.tv_test);
            new Thread(new Runnable() {
                @Override
                public void run() {
                    SystemClock.sleep(3000);
                    mTvTest.setText("子线程修改UI成功");
                }
            }).start();
        }
    }
    

    上面这个例子依然是子线程里面修改TextView,只不过这次先给它睡3秒钟时间,那么看下运行情况如何:

        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)
            at android.view.View.requestLayout(View.java:23093)
            at android.view.View.requestLayout(View.java:23093)
            at android.view.View.requestLayout(View.java:23093)
            at android.view.View.requestLayout(View.java:23093)
            at android.view.View.requestLayout(View.java:23093)
            at android.view.View.requestLayout(View.java:23093)
            at android.support.constraint.ConstraintLayout.requestLayout(ConstraintLayout.java:3172)
            at android.view.View.requestLayout(View.java:23093)
            at android.widget.TextView.checkForRelayout(TextView.java:8908)
            at android.widget.TextView.setText(TextView.java:5730)
            at android.widget.TextView.setText(TextView.java:5571)
            at android.widget.TextView.setText(TextView.java:5528)
            at com.xxx.myapplication.MainActivity$1.run(MainActivity.java:19)
            at java.lang.Thread.run(Thread.java:764)
    

    不出所料,报了一个异常,告诉我们只有主线程才能修改UI。


    探究

    那么问题来了,为什么第一个例子竟然可以在子线程修改UI。
    在讨论上面这个问题之前,首先我们来看下,这个异常是在哪里抛出的。从log信息可以看出来是在ViewRootImpl里面,那么我们打开代码一探究竟:

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

    打开代码看了下确实是在checkThread里面调用的,那么什么时候调用这个checkThread呢,

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

    看到这段代码相信大家并不陌生,这是requestLayout方法,是View里面请求重新测量布局的方法,那么这个类是继承View的么?

    public final class ViewRootImpl implements ViewParent,
            View.AttachInfo.Callbacks, ThreadedRenderer.DrawCallbacks {
    }
    

    发现并不是继承View,那么RequestLayout是谁调用的呢?
    此时我们将目光看回到TextView的setText方法:

    private void setText(CharSequence text, BufferType type,
                             boolean notifyBefore, int oldlen) {
            //代码省略
    
            if (mLayout != null) {
                //关键代码所在
                checkForRelayout();
            }
    
            sendOnTextChanged(text, 0, oldlen, textLength);
            onTextChanged(text, 0, oldlen, textLength);
    
            notifyViewAccessibilityStateChangedIfNeeded(AccessibilityEvent.CONTENT_CHANGE_TYPE_TEXT);
    
            if (needEditableForNotification) {
                sendAfterTextChanged((Editable) text);
            } else {
                notifyAutoFillManagerAfterTextChangedIfNeeded();
            }
    
            // SelectionModifierCursorController depends on textCanBeSelected, which depends on text
            if (mEditor != null) mEditor.prepareCursorControllers();
        }
    

    经过一系列的调用,最终会调用到这个setText方法里面,然后看下checkForRelayout方法做了什么操作:

       /**
         * Check whether entirely new text requires a new view layout
         * or merely a new text layout.
         */
        private void checkForRelayout() {
            // If we have a fixed width, we can just swap in a new text layout
            // if the text height stays the same or if the view height is fixed.
    
            if ((mLayoutParams.width != LayoutParams.WRAP_CONTENT
                    || (mMaxWidthMode == mMinWidthMode && mMaxWidth == mMinWidth))
                    && (mHint == null || mHintLayout != null)
                    && (mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight() > 0)) {
               //代码省略
    
                // We lose: the height has changed and we have a dynamic height.
                // Request a new view layout using our new text layout.
                requestLayout();
                invalidate();
            } else {
                // Dynamic width, so we have no choice but to request a new
                // view layout with a new text layout.
                nullLayouts();
                requestLayout();
                invalidate();
            }
        }
    
      public void requestLayout() {
            //代码省略
    
            if (mParent != null && !mParent.isLayoutRequested()) {
                mParent.requestLayout();
            }
           //代码省略
    

    由上面这段代码可知,不管是进入if还是else都会执行requestLayout方法,而RequestLayout方法会调用mParent.requestLayout方法。而这个mParent是一个ViewParent接口类,我们再回想下其实ViewRootImpl就是实现了这个接口类,所以这个地方最终会调用到ViewRootImpl的RequestLayout方法。
    那么这下看到上面那段代码,是不是一下子就有点想法了呢。只有在mParent不为空的时候才会进行线程的校验,那么我们只要在mParent没有初始化的时候就调用这个方法是不是就可以绕过线程校验直接更新UI呢?答案自然是肯定的。这也就是为什么在onCreate方法里面直接开启线程修改UI是不会报错的,但是休眠一段时间以后就报错了。那是因为休眠一段时间以后,mParent已经被初始化了,从这个时候开始就不能在子线程修改UI了。

    疑惑

    那么mParent是在什么时候被初始化的呢?
    这时我们就需要关注一个比较重要的类了"ActivityThread.java",这个类掌管这个Activity的生命周期。
    另外我们在学习Android的时候,经常看到这样一句话:onCreate 生命周期主要进行初始化操作,onResume执行完毕以后页面才会展示出来。
    所以我猜mParent初始化操作应该在onResume时期执行,这段时间正是绘制的时期。
    进入ActivityThread以后,我们主要关注resume:

        @Override
        public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward,
                String reason) {
            //代码省略
    
            // TODO Push resumeArgs into the activity for consideration
            final ActivityClientRecord r = performResumeActivity(token, finalStateRequest, reason);
          //代码省略
            if (r.window == null && !a.mFinished && willBeVisible) {
                r.window = r.activity.getWindow();
                View decor = r.window.getDecorView();
                decor.setVisibility(View.INVISIBLE);
                ViewManager wm = a.getWindowManager();
                WindowManager.LayoutParams l = r.window.getAttributes();
                a.mDecor = decor;
                l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
                l.softInputMode |= forwardBit;
                if (r.mPreserveWindow) {
                    a.mWindowAdded = true;
                    r.mPreserveWindow = false;
                    // Normally the ViewRoot sets up callbacks with the Activity
                    // in addView->ViewRootImpl#setView. If we are instead reusing
                    // the decor view we have to notify the view root that the
                    // callbacks may have changed.
                    ViewRootImpl impl = decor.getViewRootImpl();
                    if (impl != null) {
                        impl.notifyChildRebuilt();
                    }
                }
                if (a.mVisibleFromClient) {
                    if (!a.mWindowAdded) {
                        a.mWindowAdded = true;
                        wm.addView(decor, l);
                    } else {
                        // The activity will get a callback for this {@link LayoutParams} change
                        // earlier. However, at that time the decor will not be set (this is set
                        // in this method), so no action will be taken. This call ensures the
                        // callback occurs with the decor set.
                        a.onWindowAttributesChanged(l);
                    }
                }
    
                // If the window has already been added, but during resume
                // we started another activity, then don't yet make the
                // window visible.
            } else if (!willBeVisible) {
                if (localLOGV) Slog.v(TAG, "Launch " + r + " mStartedActivity set");
                r.hideForNow = true;
            }
        //代码省略
        }
    

    此处发现一个比较关键的信息 wm.addView(decor, l)。看方法名字,应该是添加View的作用,那么我们跟过去看下(此处的wm是WindowManagerImpl类),

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

    发现此时,addView操作交给了mGlobal这个代理类来处理,而这个代理类正是WindowManagerGlobal,那再跟过去看下:

                root = new ViewRootImpl(view.getContext(), display);
    
                view.setLayoutParams(wparams);
    
                mViews.add(view);
                mRoots.add(root);
                mParams.add(wparams);
                try {
                    root.setView(view, wparams, panelParentView);
                } catch (RuntimeException e) {
                    // BadTokenException or InvalidDisplayException, clean up.
                    if (index >= 0) {
                        removeViewLocked(index, true);
                    }
                    throw e;
                }
    
      view.assignParent(this);
    

    初始化一个ViewRootImpl类,然后调用setView方法,在setView放里面调用了assignParent方法,将ViewRootImpl传入到View当中。

    总结

    因此修改UI并不一定非得在主线程里面操作(虽然这种苛刻条件下子线程更新UI基本上没啥用),但是也算是拓宽了自己对于源码的认识

    相关文章

      网友评论

        本文标题:震惊!Android子线程也能修改UI?

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