首页 > 其他分享 >AndroidUI进阶-为什么不能在子线程更新UI

AndroidUI进阶-为什么不能在子线程更新UI

时间:2023-06-22 14:04:13浏览次数:34  
标签:调用 requestLayout 进阶 AndroidUI 在子 ViewRootImpl 线程 方法 view


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

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)

"android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views"这句异常大家都见过。如果在子线程更新UI就会报错,那么为什么不能在子线程更新UI呢,就真的不可以在子线程更新UI吗?

大家看到报错一般都会直接点到报错行,这里就直接来看一下这个ViewRootImpl的requestLayout和checkThread。

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

里面执行了这个checkThread方法,抛出了异常

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

很多人在这里就会认为这个方法检查了主线程ActivityThread,但看源码发现是判断mThread是不是当前线程,所以要去看mThread的赋值

mThread = Thread.currentThread();

可以看到mThread是构造函数被调用的时候的 线程,那么这个方法是不是被主线程调用的就得看ViewRootImpl的创建过程,而该方法如果是在ActivityThread里被调用的,不当然是主线程了吗,这里留个疑问。

当分析ViewRootImpl的构造的时候看到了requestlayout,看到了这个checkThread方法,也有人会直接根据报错来看这个方法,不过看报错代码可以看到是在不断执行View的requestLayout,怎么就执行到了ViewRootImpl的requestLayout了呢?

View::requestLayout

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

看了View的requestLayout发现这个方法实在不断调用parent的requestLayout,那么View最终的parent就是ViewRootImpl了吗?这又是如何做到的呢。

要回答这个问题就需要先了解Activity的结构。

Activity页面的结构

AndroidUI进阶-为什么不能在子线程更新UI_android

当开发一个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,这样报错的调用栈就清晰了。

AndroidUI进阶-为什么不能在子线程更新UI_java_02

页面绘制

回过头看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具备较高的性能,在游戏、音视频场景比较常用。

标签:调用,requestLayout,进阶,AndroidUI,在子,ViewRootImpl,线程,方法,view
From: https://blog.51cto.com/u_16163453/6534781

相关文章

  • Vue进阶(幺叁柒):动态表单校验
    在前期博文《Vue进阶(三十):vue中使用element-ui进行表单验证》、《Vue进阶(幺幺叁):关于vue.jselementui表单验证this.$refs[formName].validate()的问题》、《Vue进阶(幺贰幺):表单校验注意事项》中主要讲解了form表单校验应遵守的约定及常见问题解决方法。在实现动态表单,且表单项为后......
  • C语言-指针进阶详解(万字解析)
    前言本篇内容主要针对指针的进阶详解,如果不懂指针的含义要自行去看书看视频了解一下。指针指针是个特殊的变量,其功能就是来存放地址,地址唯一标识一块内存空间。指针的大小有两种一种是32位操作系统下的4个字节,一种是64位操作系统的8个字节。同时指针是有类型的,不同的类型决定了指针......
  • 音视频开发进阶|第七讲:分辨率与帧率·下篇
     在视频系列的上一篇推文中,我们简单总结了色彩、像素、图像和视频等基础概念之间的关系。并且主要关注了两个组合:像素和图像,图像和视频之间的构成逻辑。我们先来简单回顾一下:从像素到图像:一定数量、记录了不同色彩信息的像素组合,得到一帧完整的图像;从图像到视频:一帧帧图像按一定频......
  • 音视频开发进阶|第七讲:分辨率与帧率·下篇
    ​在视频系列的上一篇推文中,我们简单总结了色彩、像素、图像和视频等基础概念之间的关系。并且主要关注了两个组合:像素和图像,图像和视频之间的构成逻辑。我们先来简单回顾一下:从像素到图像:一定数量、记录了不同色彩信息的像素组合,得到一帧完整的图像;从图像到视频:一帧帧图像按一......
  • 强化学习从基础到进阶-常见问题和面试必知必答[2]:马尔科夫决策、贝尔曼方程、动态规划
    强化学习从基础到进阶-常见问题和面试必知必答[2]:马尔科夫决策、贝尔曼方程、动态规划、策略价值迭代1.马尔科夫决策核心词汇马尔可夫性质(Markovproperty,MP):如果某一个过程未来的状态与过去的状态无关,只由现在的状态决定,那么其具有马尔可夫性质。换句话说,一个状态的下一个状态......
  • 强化学习从基础到进阶-常见问题和面试必知必答[2]:马尔科夫决策、贝尔曼方程、动态规划
    强化学习从基础到进阶-常见问题和面试必知必答[2]:马尔科夫决策、贝尔曼方程、动态规划、策略价值迭代1.马尔科夫决策核心词汇马尔可夫性质(Markovproperty,MP):如果某一个过程未来的状态与过去的状态无关,只由现在的状态决定,那么其具有马尔可夫性质。换句话说,一个状态的下一个状态......
  • Python进阶-上下文管理器
    上下文管理器定义包装任意代码确保执行的一致性语法with语句__enter__和__exit__方法classContextManager(object):def__init__(self):self.entered=Falsedef__enter__(self):self.entered=Truereturnself......
  • CodeStar2023年春第12周周赛普及进阶组
    T1:SequenceMatching本题难度中等,序列匹配问题,一般都可以考虑用类似公共子序列的DP方法。本题正是如此,考虑数组\(a\)的前缀和数组\(b\)的前缀匹配时,\(x+y\)的最小值,推出状态转移即可记dp[i][j]表示将\(a_1\sima_i\)与\(b_1\simb_j\)中删掉\(x\)个后不同位置个......
  • rust进阶手册(1)
    目录安装管理和配置工具项目管理类型字面值格式输出位置参数格式化文本命名参数类型转换浮点字面值字符类型数组元组安装不管OS是否带有rust,都应使用rustup来安装rustlinux/freebsdcurlhttps://sh.rustup.rs-sSf|shwindowshttps://www.rust-lang.org/tools/install......
  • Ruby进阶手册(1)
    目录关于Rubyrbenvrbenv是类Unix系统上Ruby编程语言的版本管理工具使用程序包管理器安装ruby安装gems卸载Ruby版本设置path安装rails关于Ruby想知道Ruby为什么会如此受欢迎吗?在粉丝眼中,Ruby是一门优美而巧妙的语言,他们还认为Ruby易于使用,能解决实际问题。想知道受到这些......