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

为什么不能在子线程更新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页面的结构

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

文末

要想成为架构师,那就不要局限在编码,业务,要会选型、扩展,提升编程思维。此外,良好的职业规划也很重要,学习的习惯很重要,但是最重要的还是要能持之以恒,任何不能坚持落实的计划都是空谈。

如果你没有方向,这里给大家分享一套由阿里高级架构师编写的《Android八大模块进阶笔记》,帮大家将杂乱、零散、碎片化的知识进行体系化的整理,让大家系统而高效地掌握Android开发的各个知识点。
在这里插入图片描述
相对于我们平时看的碎片化内容,这份笔记的知识点更系统化,更容易理解和记忆,是严格按照知识体系编排的。

一、架构师筑基必备技能

1、深入理解Java泛型
2、注解深入浅出
3、并发编程
4、数据传输与序列化
5、Java虚拟机原理
6、高效IO
……

在这里插入图片描述

二、Android百大框架源码解析

1.Retrofit 2.0源码解析
2.Okhttp3源码解析
3.ButterKnife源码解析
4.MPAndroidChart 源码解析
5.Glide源码解析
6.Leakcanary 源码解析
7.Universal-lmage-Loader源码解析
8.EventBus 3.0源码解析
9.zxing源码分析
10.Picasso源码解析
11.LottieAndroid使用详解及源码解析
12.Fresco 源码分析——图片加载流程

在这里插入图片描述

三、Android性能优化实战解析

  • 腾讯Bugly:对字符串匹配算法的一点理解
  • 爱奇艺:安卓APP崩溃捕获方案——xCrash
  • 字节跳动:深入理解Gradle框架之一:Plugin, Extension, buildSrc
  • 百度APP技术:Android H5首屏优化实践
  • 支付宝客户端架构解析:Android 客户端启动速度优化之「垃圾回收」
  • 携程:从智行 Android 项目看组件化架构实践
  • 网易新闻构建优化:如何让你的构建速度“势如闪电”?

在这里插入图片描述

四、高级kotlin强化实战

1、Kotlin入门教程
2、Kotlin 实战避坑指南
3、项目实战《Kotlin Jetpack 实战》

  • 从一个膜拜大神的 Demo 开始

  • Kotlin 写 Gradle 脚本是一种什么体验?

  • Kotlin 编程的三重境界

  • Kotlin 高阶函数

  • Kotlin 泛型

  • Kotlin 扩展

  • Kotlin 委托

  • 协程“不为人知”的调试技巧

  • 图解协程:suspend

在这里插入图片描述

五、Android高级UI开源框架进阶解密

1.SmartRefreshLayout的使用
2.Android之PullToRefresh控件源码解析
3.Android-PullToRefresh下拉刷新库基本用法
4.LoadSir-高效易用的加载反馈页管理框架
5.Android通用LoadingView加载框架详解
6.MPAndroidChart实现LineChart(折线图)
7.hellocharts-android使用指南
8.SmartTable使用指南
9.开源项目android-uitableview介绍
10.ExcelPanel 使用指南
11.Android开源项目SlidingMenu深切解析
12.MaterialDrawer使用指南
在这里插入图片描述

六、NDK模块开发

1、NDK 模块开发
2、JNI 模块
3、Native 开发工具
4、Linux 编程
5、底层图片处理
6、音视频开发
7、机器学习

在这里插入图片描述

七、Flutter技术进阶

1、Flutter跨平台开发概述
2、Windows中Flutter开发环境搭建
3、编写你的第一个Flutter APP
4、Flutter开发环境搭建和调试
5、Dart语法篇之基础语法(一)
6、Dart语法篇之集合的使用与源码解析(二)
7、Dart语法篇之集合操作符函数与源码分析(三)

在这里插入图片描述

八、微信小程序开发

1、小程序概述及入门
2、小程序UI开发
3、API操作
4、购物商场项目实战……

在这里插入图片描述

全套视频资料:

一、面试合集

二、源码解析合集


三、开源框架合集


欢迎大家一键三连支持,若需要文中资料,直接点击文末CSDN官方认证微信卡片免费领取【保证100%免费】↓↓↓
请添加图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值