AndroidUI Advanced - Why can't the UI be updated in the child thread

Why can't I update UI in child thread

    android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
        at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:8798)
        at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:1606)
        at android.view.View.requestLayout(View.java:25390)
复制代码

Everyone has seen the exception "android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views". If the UI is updated in the sub-thread, an error will be reported, so why can't the UI be updated in the sub-thread? Is it really impossible to update the UI in the sub-thread?

When you see the error report, you will usually click directly to the error line. Here, let's take a look at the requestLayout and checkThread of this ViewRootImpl.

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

This checkThread method is executed inside and an exception is thrown

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

Many people here will think that this method checks the main thread ActivityThread, but looking at the source code, it is found that mThread is the current thread, so we have to look at the assignment of mThread

mThread = Thread.currentThread();
复制代码

It can be seen that mThread is the thread when the constructor is called, so whether this method is called by the main thread depends on the creation process of ViewRootImpl, and if the method is called in ActivityThread, it is not the main thread. Well, here's a question.

When analyzing the structure of ViewRootImpl, I saw the requestlayout and the checkThread method, and some people would look at this method directly according to the error report, but looking at the error code, we can see that the requestLayout of the View is being continuously executed, how can the requestLayout of the ViewRootImpl be executed? What about?

View::requestLayout

public void requestLayout() {
    ...
    if (mParent != null && !mParent.isLayoutRequested()) {
        mParent.requestLayout();
    }
复制代码

Looking at the requestLayout of the View, I found that this method is constantly calling the requestLayout of the parent, so is the final parent of the View the ViewRootImpl? How is this done?

To answer this question, you need to understand the structure of Activity first.

Activity page structure

setContentView-ActivityUI.png 当开发一个Activity的时候,首先要在onCreate里setContentView,把资源文件传入。

Activity::setContentView 注意这里分析的不是AppCompatActivity

public void setContentView(@LayoutRes int layoutResID) {
    getWindow().setContentView(layoutResID);
    initWindowDecorActionBar();
}
复制代码

实际上Activity去调用了getWindow()的setContentView,Activity首先持有了一个Window,而window是一个抽象类,它有一个唯一实现就是PhoneWindow,可以认为每个Activity里首先是含有一个PhoneWindow。这里还有一个DecorActionBar,这是一个ViewStub用于设置页面是否含有ActionBar。ViewStub比设置invisible性能更高。

PhoneWindow::setContentView

public void setContentView(int layoutResID) {
    if (mContentParent == null) {
        installDecor();
    } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
        mContentParent.removeAllViews();
    }
    ...
    mLayoutInflater.inflate(layoutResID, mContentParent);
复制代码

判断是否含有contentParent,没有的话就去installDecor。

private void installDecor() {
    mForceDecorInstall = false;
    if (mDecor == null) {
        mDecor = generateDecor(-1);
        mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
        mDecor.setIsRootNamespace(true);
        if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
            mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
        }
    } else {
        mDecor.setWindow(this);
    }
    if (mContentParent == null) {
        mContentParent = generateLayout(mDecor);
复制代码

这里最终把generateLayout(mDecor)给到了contentParent,generateDecor方法new了一个DecorView。之后仍然在PhoneWindow的setContentView方法中调用了layoutInflater.inflate方法把xml资源文件进行inflate。

简单总结一下Activity内置一个PhoneWindow,PhoneWindow最外层是个DecorView,上面有个ActionBar是个ViewStub。 那么再回到上面的问题,是否DecorView的parent就是ViewRootImpl,这就得看ViewRootImpl的创建过程了。

ViewRootImpl的创建过程

追溯源码直接说结论,ActivityThrad在handleResumeActivity里调用了performResumeActivity,然后执行了WindowManagerImpl的addView,WindowManagerGlobal的addView方法new了ViewRootImpl。 performResumeActivity方法内部调用了activity的performResume,然后执行了Instrumentation的callActivityOnResume,在这个方法里调用了Activity的onResume方法。在这里可以得出一个结论,activity在执行onResume的时候还没有创建好页面。 handleResumeActivity:

            r.window = r.activity.getWindow();
            View decor = r.window.getDecorView();
            decor.setVisibility(View.INVISIBLE);
            ViewManager wm = a.getWindowManager();
            WindowManager.LayoutParams l = r.window.getAttributes();
            ...
            wm.addView(decor, l);
复制代码

再看WMGlobal在addView之后又执行了ViewRootImpl的setView方法

root = new ViewRootImpl(view.getContext(), display);

view.setLayoutParams(wparams);

mViews.add(view);
mRoots.add(root);
mParams.add(wparams);

try {
    root.setView(view, wparams, panelParentView, userId);
复制代码

而ViewRootImpl的setView方法中有一个很关键的方法,requestlayout,这也是在一开始的异常报错看见的方法,然后checkThread,初始化过程thread当然是一致的,这里也回答了上面的问题mThread是不是主线程,在这里这个调用栈很明显是主线程。在requestLayout里有个很关键的方法 scheduleTraversals

void scheduleTraversals() {
    if (!mTraversalScheduled) {
        mTraversalScheduled = true;
        mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
        mChoreographer.postCallback(
                Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
        notifyRendererOfFramePending();
        pokeDrawLockIfNeeded();
    }
}
复制代码

可以看到是执行了一个异步任务去执行mTraversalRunnable这个Runnanble也就是doTraversal

void doTraversal() {
    if (mTraversalScheduled) {
        mTraversalScheduled = false;
        mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);

        if (mProfile) {
            Debug.startMethodTracing("ViewAncestor");
        }

        performTraversals();

        if (mProfile) {
            Debug.stopMethodTracing();
            mProfile = false;
        }
    }
}
复制代码

而performTraversals就是view的核心方法,测量、绘制、布局。 再回到setview,下面还有一行

view.assignParent(this);
复制代码

把ViewRootImpl给了这个view,这个view是setview的参数,而setview的参数是WMGlobal的addView的参数也就是ActivityThread调用的把DecorView传递过来了

r.window = r.activity.getWindow();
View decor = r.window.getDecorView();
...
wm.addView(decor, l);
复制代码

至此,ViewRootImpl就成为了DecorView的parent,这样报错的调用栈就清晰了。 ViewRootImpl-ViewRootImpl.png

页面绘制

回过头看performTraversals

boolean layoutRequested = mLayoutRequested && (!mStopped || mReportNextDraw);
if (layoutRequested) {
...
// Ask host how big it wants to be
windowSizeMayChange |= measureHierarchy(host, lp, res,
   desiredWindowWidth, desiredWindowHeight);
复制代码

里面有一个变量mLayoutRequested,在每次调用layoutRequest和第一次setView的时候会设置为true,这个true了就会重新布局,在measureHierarchy里调用了getRootMeasureSpec

根据rootDimension设置measureSpec,设置好了宽高以后调用performMeasure,在里面调用了DecorView的measure方法,在这里完成了控件树的第一次测量。

然后进行第二次测量,原理同wrapcontent,因为一次测量没法确定大小

回到performTraversals方法,后面调用了performLayout

        final boolean didLayout = layoutRequested && (!mStopped || mReportNextDraw);
        boolean triggerGlobalLayoutListener = didLayout
                || mAttachInfo.mRecomputeGlobalAttributes;
        if (didLayout) {
            performLayout(lp, mWidth, mHeight);
复制代码

在里面调用了DecorView的layout方法

        if (triggerGlobalLayoutListener) {
            mAttachInfo.mRecomputeGlobalAttributes = false;
            mAttachInfo.mTreeObserver.dispatchOnGlobalLayout();
        }
复制代码

之后使用ViewTreeObserver的dispatchOnGlobalLayout回调控件大小信息

这里mLayoutRequested 设置为false,如果因为异常重新调用这个方法不会重新测量直接绘制

再回到PerformTraversals

        boolean cancelDraw = mAttachInfo.mTreeObserver.dispatchOnPreDraw() || !isViewVisible;

        if (!cancelDraw) {
            if (mPendingTransitions != null && mPendingTransitions.size() > 0) {
                for (int i = 0; i < mPendingTransitions.size(); ++i) {
                    mPendingTransitions.get(i).startChangingAnimations();
                }
                mPendingTransitions.clear();
            }

            performDraw();
复制代码

当页面不是不可见的时候,就调用performDraw进行绘制

performDraw里调用了ViewRootImpl的draw方法,在这里有硬件加速的设置,硬件加速方法是ThreadedRender的draw,软件是DecorView的draw方法

如何在子线程更新UI

现在知道了为什么不能在子线程更新UI以后,那么如果就一定要在子线程更新UI需要怎么做呢?

requestLayout

回到View的requestLayout

mPrivateFlags |= PFLAG_FORCE_LAYOUT;
mPrivateFlags |= PFLAG_INVALIDATED;

if (mParent != null && !mParent.isLayoutRequested()) {
    mParent.requestLayout();
}
复制代码

里面有flag叫PFLAG_FORCE_LAYOUT,在requestlayout里面会进行判断,而layout完成后会清除该flag,如果在主线程里写了requestlayout方法,那么这个flag就会被设置为true,只要在这个view的flag为false的时候才会去调用parent的requestlayout。所以就不会调用到decorview的requestlayout,自然就不会去执行checkThread就不会报错。

手写addView

前面看到checkThread方法其实判断的是addView,Decorview初始化的时候的那个线程,一般来说是主线程,但是可以自己调用windowmanager去写addView,这个时候因为需要一个looper,所以还需要自己在子线程去启动一个looper。这样就可以子线程更新UI了。

SurfaceView

毕竟UI的异步和延迟会导致很多显示和交互的问题,如果说上面两种属于有风险的歪门邪道的话,Android官方提供的SurfaceView就不是了。在SurfaceView里有个holder,可以获取到canvas对象,自己用canvas去绘制UI就可以在子线程进行了,正因如此SurfaceView具备较高的性能,在游戏、音视频场景比较常用。

Guess you like

Origin juejin.im/post/7079428970063593480