AndroidUI进阶-为什么不能在子线程更新UI
Posted 嘴巴吃糖了
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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页面的结构
当开发一个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,这样报错的调用栈就清晰了。
页面绘制
回过头看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%免费】↓↓↓
以上是关于AndroidUI进阶-为什么不能在子线程更新UI的主要内容,如果未能解决你的问题,请参考以下文章
网络操作不能直接写在主线程中 以及 为什么不能在子线程中更新UI控件的属性