更新fielddata为true_线程与更新UI,细谈原理

前言

相信不少读者都阅读过相类似的文章了,但是我还是想完整的把这之间的关系梳理清楚,细节聊好,希望你也能从中学到一些。

进入正题,大家应该都听过这样一句话——“UI更新要在主线程,子线程更新UI会崩溃”。久而久之就感觉这是个真理,甚至被认为是“官方结论”。

但是如果问你,官方什么时候在哪里说过这句话,你会不会有点懵。而且就算是官方说的,也就不一定对的是吧,众所周知,Google官方文档一直都有点说的不清不楚,需要我们进行大量实践得出实际的结论。

就好比之前的Android11更新文档,我也是看了好久,通过一个个实践才写出了适配指南,然后就发现其中一个比较明显的BUGGoogle官方有说过这样一句:

下面是首先需要关注的行为变更 (无论您应用的 targetSdkVersion 是多少):  外部存储访问权限  - 应用无法再访问外部存储空间中其他应用的文件。

其实经过实践会发现,外部存储访问权限还是会和targetSdkVersion有关,具体可以看这篇Android11适配指南。

废话有点多了,今天还是通过实践案例,看看这个关于线程和UI更新的 “官方结论” 正确吗?

案例一,子线程更新button文字

1)onCreate方法中更新了按钮显示文字,修改Button的宽度为固定或者wrap_content,都不崩溃。


            android:id="@+id/btn_ui"
        android:layout_width="100dp"
        android:layout_height="70dp"
        android:layout_centerInParent="true"
        android:text="我是一个按钮"
        />

        //或者

            android:id="@+id/btn_ui"
        android:layout_width="wrap_content"
        android:layout_height="70dp"
        android:layout_centerInParent="true"
        android:text="我是一个按钮"
        />        


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_ui)

        thread {
            btn_ui.text="年轻人要讲武德"
        }
    }

2)onCreate方法中更新了按钮显示文字,加了延时。

Button的宽度为固定不崩溃。Button的宽度为wrap_content,崩溃报错——Only the original thread that created a view hierarchy can touch its views


            android:id="@+id/btn_ui"
        android:layout_width="100dp"
        android:layout_height="70dp"
        android:layout_centerInParent="true"
        android:text="我是一个按钮"
        />

        //或者

            android:id="@+id/btn_ui"
        android:layout_width="wrap_content"
        android:layout_height="70dp"
        android:layout_centerInParent="true"
        android:text="我是一个按钮"
        />   
        

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_ui)

        thread {
         Thread.sleep(3000)
            btn_ui.text="年轻人要讲武德"
        }
    }

案例一分析

有点懵的感觉,不慌,来看看崩溃信息。

崩溃是在按钮宽度为wrap_content,也就是根据内容设定宽度,然后3秒之后去更新按钮文字,发生了崩溃。相比之下,有两个崩溃影响点需要注意下:

  • 宽度wrap_content。如果设置为固定值,是不会崩溃的,见案例2,所以是不是跟布局改变的逻辑有关呢?
  • 延时3秒。如果不延时的话,即使是wrap_content也不会崩溃,见案例1,所以是不是跟某些类的加载进度有关呢?

带着这些疑问去源码中找找答案。先看看崩溃日志:


android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
        at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:9219)
        at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:1600)
        at android.view.View.requestLayout(View.java:24884)

可以看到是ViewRootImplrequestLayout中检查线程的时候报错了,那我们就看看这个方法:

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

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

在解开谜底之前,我们先了解下ViewRootImpl

ViewRootImpl

Activity从创建到我们看到界面,其实是经历了两个过程:加载布局和绘制

  • 加载布局

加载布局其实就是我们常用的setContentView(int layoutResID)方法,这个方法主要做的就是新建了一个DecorView,然后根据activity设置的主题(theme)或者特征(Feature)加载不同的根布局文件,最后再加载layoutResID资源文件。为了方便大家理解,画了一张图:

33f2cf13b5734b59f3b0387d8e5cb0e1.png
加载布局流程

这里的最后一步是调用了LayoutInflaterinflate()方法,这个方法只做了一件事,就是解析xml文件,然后根据节点生成了view对象。最后形成了一个完整的DOM结构,返回最顶层的根布局View。(DOM是一种文档对象模型,他的层次结构是除了顶级元素,所有元素都被包括到另外的元素节点中,有点像家谱树结构,很典型的就是html代码解析)

到这里,一个有完整view结构的DecorView就创建出来了,但是它还没有被绘制,也没有被显示到手机界面上。

  • 绘制

绘制的流程发生在handleResumeActivity中,熟悉app启动流程的朋友应该知道,handleResumeActivity方法是用来触发onResume方法的,这里也完成了DecorView的绘制。再来一张图:

b33e0d9d4c6ed231bbb61808f7c9f425.png
绘制流程
  • 总结

由此我们可以得出一些结论:
1)setContentView用来新建DecorView并加载布局的资源文件。
2)onResume方法之后,会新建一个ViewRootImpl,作为DecorViewparentDecorView进行测量,布局和绘制等操作。
3)PhoneWindow作为Window的唯一子类,存储了DecorView变量,并对其进行管理,属于ActivityView交互的中间层。

分析崩溃

好了。再回来看看崩溃的原因:


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

可以看到是因为当前线程currentThread不是mThread的时候,就会崩溃,报的错误是 “只有创建视图层次结构的原始线程才能触摸它的视图” ,看到这里是不是猜到一些了,这个mThread难道就是“创建视图的原始线程”?

通过查找,其实这个mThread是在ViewRootImpl被创建的时候赋值的:

public ViewRootImpl(Context context, Display display) {
    mThread = Thread.currentThread();
}

而通过上方分析Activity加载布局过程得知,ViewRootImpl实例化发生在onResume之后,用来绘制DecorViewwindow上。

所以我们就可以得知崩溃的真正原因,就是当前线程不是ViewRootImpl创建时候的线程就会崩溃。翻译的还是比较准确的,只有创建视图的原始线程才能修改这个视图,听起来也蛮有道理的,我创造了你才有权利改变你,有那味了。

然后再看看前面的案例:

  • 案例一,在onCreate中修改Button,这时候只是在修改DecorView,都没创建ViewRootImpl,也就没走到所以checkThread方法,当然不会崩溃了。ViewRootImpl的创建是在onResume之后。

  • 案例二,延时3秒之后,界面也绘制完成了,创建ViewRootImpl显然是在主线程完成的,所以mThread为主线程,而改变Button的线程为子线程,所以setText方法会触发requestLayout方法重新绘制,最终导致崩溃。

但是,Button的宽度设置为固定值咋又不崩溃了?难道就不会执行checkThread方法了?奇怪。

找找setText的源码可以发现,有一个方法是负责检查是否需要新的布局——checkForRelayout()


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)) {
            

            if (mEllipsize != TextUtils.TruncateAt.MARQUEE) {
                // In a fixed-height view, so use our new text layout.
                if (mLayoutParams.height != LayoutParams.WRAP_CONTENT
                        && mLayoutParams.height != LayoutParams.MATCH_PARENT) {
                    autoSizeText();
                    invalidate();
                    return;
                }

               //...
            }

            // 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();
        }
    }

可以看到,如果布局大小没有改变的话,我们是不会去执行requestLayout方法重新进行布局绘制的,只会调用autoSizeText方法计算文字大小,invalidate绘制文字本身,所以当我们宽高设置为固定值,setText()方法就不会执行到requestLayout()方法了,自然也就执行不到checkThread()方法了。

反思

解决了问题,还需要反思下,为什么需要checkThread检查线程呢?

  • 检查线程,其实就是检查更新UI操作的当前线程是不是当初创建UI的那个线程,这样就保证了线程安全,因为UI控件本身不是线程安全的,但是加锁又显得太重,会降低View加载效率,毕竟是跟交互相关的。所以就直接通过判断线程这一逻辑来形成一个单线程模型,保证View操作的线程安全。

案例二,子线程和主线程分别showToast

1)onCreate方法中弹出toast,崩溃——Can't toast on a thread that has not called Looper.prepare()


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_ui)

        thread {
            showToast("我去年买了个表")
        }
    }

2)onCreate方法中弹出toast,增加Looper.prepare(),Looper.loop()方法。不崩溃。

加上延时3秒,不崩溃。


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_ui)

        thread {
            //Thread.sleep(3000)

            Looper.prepare()
            showToast("我去年买了个表")
            Looper.loop()
        }
    }

3)使用同一个Toast实例,在子线程中的Toast没消失之前点击按钮,在主线程中修改Toast文字并显示,则程序崩溃——Only the original thread that created a view hierarchy can touch its views.。

重新运行,在子线程中显示并消失后,点击按钮,不崩溃。

换个手机——三星s9,重新运行,在子线程中的Toast没消失之前点击按钮,不崩溃。

    lateinit var mToast: Toast

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_ui)

        thread {
            Looper.prepare()
            mToast=Toast.makeText(this@UIMainActivity,"我去年买了个表",Toast.LENGTH_LONG)
            mToast.show()
            Looper.loop()
        }

        btn_ui.setOnClickListener {
            mToast.setText("我今年买了个表")
            mToast.show()
        }
    }

案例二分析

在解开谜底之前,我们先了解下Toast

Toast原理

Toast.makeText(this,msg,Toast.LENGTH_SHORT).show()

简单又常用的一句代码,还是通过流程图的方式看看它是怎么创建并展示的。

a6c910b56ff01703c1ca2ae8784c9fdd.png
Toast流程图

DecorView加载绘制流程如出一辙,首先加载了布局文件,创建了View。然后通过addView方法,再次新建一个ViewRootImpl实例,作为parent,进行测量布局和绘制。

崩溃分析

1)首先,说下第一次崩溃——Can't toast on a thread that has not called Looper.prepare(),也就是在创建Toast的线程必须要有Looper在运行。

根据源码我们也得知Toast的显示和隐藏都是通过Handler传递消息的,所以必须要有Handler使用环境,也就是绑定Looper对象,并且通过loop方法开始循环处理消息。

2)第二次崩溃——Only the original thread that created a view hierarchy can touch its views

这里的崩溃和之前更新Button一样的报错,所以我们有理由怀疑也是一样的原因,在不同的线程调用了ViewRootImplrequestLayout方法。

我们看到点击按钮的时候,调用了mToast.setText()方法,咦,这不就跟案例一一模一样了吗。

setText方法中调用了TextViewsetText()方法,然后由于Toast中的TextView宽高都是wrap_content的,所以会触发requestLayout方法,最后会调用到最上层View也就是ViewRootImplrequestLayout方法。

所以崩溃的原因就是因为Toast在第一次在子线程中show的时候,新建了一个ViewRootImpl实例,绑定了当前线程也就是子线程到mThread变量。然后同一个Toast,在主线程调用setText方法,最终会调用到ViewRootImpl的requestLayout方法,引起线程检查,当前线程也就是主线程并不是当初那个创建ViewRootImpl实例的线程,所以导致崩溃。

3)那为什么等Toast消失之后,点击按钮又不崩溃了呢?

原因就在Toast的hide方法中,最终会调用到View的assignParent方法,将Toast的mParent设置为null,也就是ViewRootImpl设置为null了。所以调用setText方法的时候也就执行不到requestLayout方法了,也就不会到checkThread方法检查线程了。贴下代码:

public void handleHide() {
    if (mView != null) {
        if (mView.getParent() != null) {
            mWM.removeViewImmediate(mView);
        }
        mView = null;
    }
}

removeViewImmediate--->removeViewLockedprivate void removeViewLocked(int index, boolean immediate) {
    ViewRootImpl root = mRoots.get(index);
    View view = root.getView();
 
 //...
    if (view != null) {
        view.assignParent(null);
        if (deferred) {
            mDyingViews.add(view);
        }
    }
}

void assignParent(ViewParent parent) {
        if (mParent == null) {
            mParent = parent;
        } else if (parent == null) {
            mParent = null;
        } else {
            throw new RuntimeException("view " + this + " being added, but"+ " it already has a parent");
        }
}

4)但是但是,为啥换个手机又不崩溃了呢?

这是我偶然发现的,在我的三星S9手机上,运行时不会崩溃的,而且界面给我的反馈并不是修改当前页面上Toast上的文字,而是像新建了一个Toast展示,即时代码中写的是setText方法。

所以我猜测在部分手机上,应该是改变了Toast的设置,当调用setText方法的时候,就会马上结束当前的Toast展示,调用hide方法。然后再进行Toast文字修改并展示,也就是刚才第三点的做法。

当然这只是我的猜测,有研究过手机源码的大神也可以补充下。

总结

任何线程都可以更新UI,也都有更新UI导致崩溃的可能。

其中的关键就是view被绘制到界面时候的线程(也就是最顶层ViewRootImpl被创建时候的线程)和进行UI更新时候的线程是不是同一个线程,如果不是就会报错。

参考

https://www.jianshu.com/p/1cdd5d1b9f3d

https://www.cnblogs.com/fangg/p/12917235.html

拜拜

有一起学习的小伙伴可以关注下我的公众号——码上积木❤️❤️ 每日三问知识点/面试题,积少成多。

c4a01b947400a1c9ce007a952021f92a.png

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/245574.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

linux sublime3 插件安装插件,手动安装sublimeText3插件

就在今天下午,我花了一个小时的时间安装sublime3插件stylus,就是为了让stylus文件能够高亮显示。网上找了很多方法,可以通过package control安装,然而,我的sublime package control能够正常显示,插件列表也…

vbs打开软件光标停在第一个输入框_三维设计软件,3DMAX最全快捷键大全,赶快收藏哦...

文章后有获取软件的方式。基本快捷键A-角度捕捉开关 B-切换到底视图C-切换到摄象机视图D-封闭视窗E-切换到轨迹视图F-切换到前视图G-切换到网格视图H-显示通过名称选择对话框I-交互式平移J-选择框显示切换K-切换到背视图L-切换到左视图M-材质编辑器N-动画模式开关O-自适应退化开…

linux通过延长器改变分辨率,HDMI延长器的优势及常见问题(转载)(转载)

HDMI延长器普遍用于一切需要进行HDMI信号调配和组合的领域,比如:军工、多媒体教学、电视电话会议、大屏幕显示、会展、金融、科研,天气、超市、会议系统等领域。以达到确保信号长间隔无衰减的传输。HDMI延长器的优势1、HDMI单网线延长器, 直接…

mfc读取txt文件并显示_Python入门丨文件读写

文件读写文件读写,是Python代码调用调用电脑文件的主要功能,能被用于读取和写入文本记录、音频片段、Excel文档、保存邮件以及任何保存在电脑上的东西。读取文件读取文件三个步骤:准备工作:首先在桌面新建了一个test文件夹&#x…

c语言最简单程序实例,C语言第一个简单实例

在信息化、智能化的世界里,可能很早很早 我们就听过许多IT类的名词,C语言也在其中,我们侃侃而谈,到底C程序是什么样子?让我们先看简单的一个例子:#include /*引入头文件*/int main(void) …

c语言 多个线程对同一变量执行memcpy_手把手带你实现线程池

执行与任务分离的组件— 线程池wangbojing/threadpool​github.com多线程技术主要解决了处理器单元内多个线程执行的问题,它可以显著的减少处理器单元的闲置时间,增加处理器单元的吞吐能力。线程池是多线程编程的一个必要组件,并且对于很多编…

android shape 自定义,Android自定义shape的使用

MainActivity如下:package cn.testshape;import android.os.Bundle;import android.app.Activity;/*** Demo描述:* 自定义shape的使用*/public class MainActivity extends Activity {Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstan…

python 爬虫库 beautifulsoup4_Python爬虫之BeautifulSoup4

The Dormouses storyOnce upon a time there were three little sisters; and their names were , Lacie and Tillie; and they lived at the bottom of a well.... """ # 创建 Beautiful Soup 对象,指定lxml解析器 soup BeautifulSoup(html, "…

python手势识别_Python|使用opencv进行简单的手势检测

简单的手势识别,基本思路是基于皮肤检测,皮肤的颜色在HSV颜色空间下与周围环境的区分度更高,从RGB转换到HSV颜色空间下针对皮肤颜色进行二值化,得到mask: defHSVBin(img):hsvcv2.cvtColor(img,cv2.COLOR_RGB2HSV) lowe…

android程序root权限,android – 如何从源代码授予对特定应用程序的root访问权限而不是root权限?...

我正在从源代码编译Android ROM,我有一个应用程序,我希望它预先安装并让它以root权限运行.如何在不支持整个ROM的情况下授予对此特定应用程序的root访问权限?解决方法:希望你不需要root …典型的股票Android ROM为很少的东西提供root权限,与principle of least priv…

android 弹出网格菜单,在android中的recyclerView中显示弹出按钮的确...

as per my above comment可以使用Popup MenuAndroid Popup Menu displays the menu below the anchor text if space is available otherwise above the anchor text. It disappears if you click outside the popup menu.试试这个创建菜单文件文件:poupup_menu.xml…

人工智能选go还是python_深圳人工智能学Python还是go,真实经历分享

深圳人工智能学Python还是go,进行选择深圳Python培训 的时候,第一要思考的就是该机构的口碑如何。如果该家机构没有一定的口碑信誉,就等于搬起石头砸了自己的招牌。为什么突然就那么火了,Python的工资待遇,人工智能&am…

python做excel数据分析统计服_Python也能做到Excel那样,条件统计轻松解决工作需求...

此系列文章收录在公众号中:数据大宇宙 > 数据处理 >E-pd 转发本文并私信我"python",即可获得Python资料以及更多系列文章(持续更新的) 经常听别人说 Python 在数据领域有多厉害,结果学了很长时间,连数据处理都麻烦…

android 系统倒计时显示时间,Android 依据系统时间整点、半点倒计时

activity_main.xml中的代码xmlns:android"http://schemas.android.com/apk/res/android"android:layout_width"match_parent"android:layout_height"match_parent"android:background"android:color/white">android:id"id/myt…

java贪吃蛇_如何用Java还原童年回忆?在线教你完成贪吃蛇小游戏

今天我就从零开始来完成这个小游戏,完成的方式也是一步一步的添加功能这样的方式来实现。额,不好意思,放错了,重来第一步完成的功能:写一个界面大家见到的贪吃蛇小游戏,界面肯定是少不了的。因此&#xff0…

android+建模工具,什么是适用于Android Studio的3D模型环境的最佳工具

首先,你已经链接的页面没有引用文字。它代替了this文本。 现在,关于文本本身:你正在脱离情境和/或误解它。这是一个完整的引文:The Android framework provides plenty ofstandard tools for creating attractive, functionalgrap…

python编写统计选票的程序_使用python编写微信公众号发稿统计程序

近日为学校公众号统计发稿情况,需统计本年度各部门分别所发稿件标题、时间、作者等。 首先考虑查看公众号后台有无统计功能,看到的界面如下: 每页只显示7天的消息,且没有部门作者信息。再看另一界面: 只统计总体的发稿…

android 刷windows10,微软计划在Android机上刷Windows 10

据外媒TechCrunch报道,目前微软正努力采取措施让Android设备运行Windows 10系统来提升自己的市场份额。目前,微软在公布Windows 10的发展战略时开始尝试与新的手机厂商合作,而这些厂商之前都至推出过Android系统手机。微软计划在Android机上刷…

jtextpane设置不能选中_在Bridge cc中不能使用camera raw 的编辑功能,解决方法

有时我们在Bridge cc中想对raw文件进行处理,这时需要启动camera raw功能,可是当我们启用时却显示不能编辑,下面说下如何启动使用camera raw 的编辑功能1.打开一张raw格式文件,在文件菜单或者右键选择在camera raw中打开&#xff0…

android icon命名规则,安卓手机的APP图标尺寸规范和图标命名规范

安卓手机的APP图标尺寸规范和图标命名规范点击查看原文android图标包括:程序启动图标、底部菜单图标、弹出对话框顶部图标、长列表内部列表项图标、底部和底部tab标签图标。1、安卓程序启动图标尺寸:ldpi(120dpi)小屏幕mdpi(160dpi)中屏幕hdpi(240dpi)大…