温故知新:探究Android UI 绘制刷新流程

一、说明:

  1. 基于之前的了解知道ui的绘制最终会走到Android的ViewRootImplscheduleTraversals进行发送接收vsync信号绘制,在ViewRootImpl中还会进行主线程检测,也就是我们所谓子线程更新ui会抛出异常。

  2. 像我们常用的刷新ui,invalidate,requestLayout方法,(按我之前的理解在ViewRootImpl初始化添加后,在子线程中刷新ui一定会崩溃:如下图)

二、问题:invalidate一定会导致异常崩溃?

2.1、例子:子线程更新TextView文本(注意这里是TextView,为什么是它而不是ImageView,因为我的背景就是使用的TextView,使用它的时候发现了invalidate,requestLayout方法的区别 )

某天我在onResume中利用子线程更新了TextView的一段代码,发现并没有抛出异常崩溃,代码如下:

 override fun onResume() {super.onResume()mBind.btTest.setOnClickListener{lifecycleScope.launch(Dispatchers.IO) {mBind.btTest.text = "子线程点击改变:${Thread.currentThread().name}"}}}

我在想为什么呢?,看代码: 一步步debug:TextView控件中:

1.、
public final void setText(CharSequence text) {setText(text, mBufferType);
}
2.、
public void setText(CharSequence text, BufferType type) {setText(text, type, true, 0);if (mCharWrapper != null) {mCharWrapper.mChars = null;}
}
3.、
private void setText(CharSequence text, BufferType type,boolean notifyBefore, int oldlen) {...省略if (mLayout != null) {checkForRelayout();}...省略}

以上主要看第三步中的 checkForRelayout()检测是否需要重绘,方法如下

@UnsupportedAppUsage
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)) {// Static width, so try making a new text layout.int oldht = mLayout.getHeight();int want = mLayout.getWidth();int hintWant = mHintLayout == null ? 0 : mHintLayout.getWidth();/** No need to bring the text into view, since the size is not* changing (unless we do the requestLayout(), in which case it* will happen at measure).*/makeNewLayout(want, hintWant, UNKNOWN_BORING, UNKNOWN_BORING,mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight(),false);//1.检测文本的显示类型,就是我们的过长省略号这种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;}// Dynamic height, but height has stayed the same,// so use our new text layout.if (mLayout.getHeight() == oldht&& (mHintLayout == null || mHintLayout.getHeight() == oldht)) {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();}
}

从上面的checkForRelayout()方法中的if (mEllipsize != TextUtils.TruncateAt.MARQUEE)条件知道成立,因为我们没有设置过mEllipsize = 跑马灯效果,所以走了invalidate()方法然后直接return截断,不会走后面的requestLayout()方法,至于requestLayout() 与 invalidate()的区别我就不讲了

2.2、分析requestLayout方法

基于之前的知识我知道调用requestLayout()方法会崩溃,至于为什么调用requestLayout()方法会崩溃?

我们先看requestLayout()方法,暂停一会invalidate()的跟进

requestLayout()方法代码如下:

public void requestLayout() {if (mMeasureCache != null) mMeasureCache.clear();if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == null) {// Only trigger request-during-layout logic if this is the view requesting it,// not the views in its parent hierarchyViewRootImpl viewRoot = getViewRootImpl();if (viewRoot != null && viewRoot.isInLayout()) {if (!viewRoot.requestLayoutDuringLayout(this)) {return;}}mAttachInfo.mViewRequestingLayout = this;}mPrivateFlags |= PFLAG_FORCE_LAYOUT;mPrivateFlags |= PFLAG_INVALIDATED;if (mParent != null && !mParent.isLayoutRequested()) {mParent.requestLayout();}if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == this) {mAttachInfo.mViewRequestingLayout = null;}
}

requestLayout()方法中会循环递归调用 mParent.requestLayout()方法,直到找到ViewRootImpl中的requestLayout()方法,而它的方法做了线程检测如下图:这就是requestLayout()方法会崩溃的原因。

验证猜想:TextView设置跑马灯属性,使上面的if (mEllipsize != TextUtils.TruncateAt.MARQUEE)不成立,走下面的requestLayout()方法,代码如下:

 override fun onResume() {super.onResume()mBind.btTest.ellipsize = TextUtils.TruncateAt.valueOf("MARQUEE")mBind.btTest.setOnClickListener{lifecycleScope.launch(Dispatchers.IO) {mBind.btTest.text = "子线程点击改变:${Thread.currentThread().name}"}}}

果然点击后崩溃:

2.3、继续分析invalidate()方法,为什么不会导致textview的更新崩溃

看代码在View.java文件中

public void invalidate() {invalidate(true);
}

public void invalidate(boolean invalidateCache) {invalidateInternal(0, 0, mRight - mLeft, mBottom - mTop, invalidateCache, true);
}

void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,boolean fullInvalidate) {if (mGhostView != null) {mGhostView.invalidate(true);return;}if (skipInvalidate()) {return;}// Reset content capture cachesmPrivateFlags4 &= ~PFLAG4_CONTENT_CAPTURE_IMPORTANCE_MASK;mContentCaptureSessionCached = false;if ((mPrivateFlags & (PFLAG_DRAWN | PFLAG_HAS_BOUNDS)) == (PFLAG_DRAWN | PFLAG_HAS_BOUNDS)|| (invalidateCache && (mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == PFLAG_DRAWING_CACHE_VALID)|| (mPrivateFlags & PFLAG_INVALIDATED) != PFLAG_INVALIDATED|| (fullInvalidate && isOpaque() != mLastIsOpaque)) {if (fullInvalidate) {mLastIsOpaque = isOpaque();mPrivateFlags &= ~PFLAG_DRAWN;}mPrivateFlags |= PFLAG_DIRTY;if (invalidateCache) {mPrivateFlags |= PFLAG_INVALIDATED;mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID;}// Propagate the damage rectangle to the parent view.final AttachInfo ai = mAttachInfo;final ViewParent p = mParent;if (p != null && ai != null && l < r && t < b) {final Rect damage = ai.mTmpInvalRect;damage.set(l, t, r, b);p.invalidateChild(this, damage);}// Damage the entire projection receiver, if necessary.if (mBackground != null && mBackground.isProjected()) {final View receiver = getProjectionReceiver();if (receiver != null) {receiver.damageInParent();}}}
}

核心代码是上面第三段invalidateInternal方法中的invalidateChild方法

它回调到ViewGroup中的invalidateChild方法

看:invalidateChild如下图:我们知道

if (attachInfo != null && attachInfo.mHardwareAccelerated) 条件成立attachInfo不为空 ,且硬件加速被开启(从API 14 (3.0)起。硬件加速默认开启)。 attachInfo 是一个view在attach至其父window被赋值的一系列信息。

所以条件成立后走的onDescendantInvalidated方法 如下:

@CallSuper
public void onDescendantInvalidated(@NonNull View child, @NonNull View target) {/** HW-only, Rect-ignoring damage codepath** We don't deal with rectangles here, since RenderThread native code computes damage for* everything drawn by HWUI (and SW layer / drawing cache doesn't keep track of damage area)*/// if set, combine the animation flag into the parentmPrivateFlags |= (target.mPrivateFlags & PFLAG_DRAW_ANIMATION);if ((target.mPrivateFlags & ~PFLAG_DIRTY_MASK) != 0) {// We lazily use PFLAG_DIRTY, since computing opaque isn't worth the potential// optimization in provides in a DisplayList world.mPrivateFlags = (mPrivateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DIRTY;// simplified invalidateChildInParent behavior: clear cache validity to be safe...mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID;}// ... and mark inval if in software layer that needs to repaint (hw handled in native)if (mLayerType == LAYER_TYPE_SOFTWARE) {// Layered parents should be invalidated. Escalate to a full invalidate (and note that// we do this after consuming any relevant flags from the originating descendant)mPrivateFlags |= PFLAG_INVALIDATED | PFLAG_DIRTY;target = this;}if (mParent != null) {mParent.onDescendantInvalidated(this, target);}
}

上面一段代码核心是 mParent.onDescendantInvalidated(this, target); 类似于requestLayout()方法 onDescendantInvalidated中会循环递归调用 mParent.onDescendantInvalidated(this, target);方法,直到找到ViewRootImpl中的onDescendantInvalidated(this, target)方法,而它的方法没做线程检测如下图:这就是开了硬件加速后invalidate方法不会崩溃的原因。如下图:直接走scheduleTraversals绘制刷新有兴趣可看:

而关闭硬件加速后会怎样呢? 继续看invalidateChild方法

@Deprecated
@Override
public final void invalidateChild(View child, final Rect dirty) {final AttachInfo attachInfo = mAttachInfo;if (attachInfo != null && attachInfo.mHardwareAccelerated) {// HW accelerated fast pathonDescendantInvalidated(child, child);return;}ViewParent parent = this;if (attachInfo != null) {...do {....parent = parent.invalidateChildInParent(location, dirty);....} while (parent != null);}
}

上面一段核心是 parent = parent.invalidateChildInParent(location, dirty);方法 同理while循环不停调用 invalidateChildInParent方法直到找到ViewRootImpl中的invalidateChildInParent(int[] location, Rect dirty)方法,如下图内部进行了线程检测

**验证猜想关闭硬件加速:android:hardwareAccelerated="false"**果然崩溃了。

三、总结

这就是我遇到的问题:单纯的根据TextView在子线程可以更新得出的结论,总的来说要想不崩溃还得绕过ViewRootImpl中的checkThread的检测。至于研究它有什么用,只有知道理解源码的流程,才能写出更好的东西。

Android 学习笔录

Android 性能优化篇:https://qr18.cn/FVlo89
Android Framework底层原理篇:https://qr18.cn/AQpN4J
Android 车载篇:https://qr18.cn/F05ZCM
Android 逆向安全学习笔记:https://qr18.cn/CQ5TcL
Android 音视频篇:https://qr18.cn/Ei3VPD
Jetpack全家桶篇(内含Compose):https://qr18.cn/A0gajp
OkHttp 源码解析笔记:https://qr18.cn/Cw0pBD
Kotlin 篇:https://qr18.cn/CdjtAF
Gradle 篇:https://qr18.cn/DzrmMB
Flutter 篇:https://qr18.cn/DIvKma
Android 八大知识体:https://qr18.cn/CyxarU
Android 核心笔记:https://qr21.cn/CaZQLo
Android 往年面试题锦:https://qr18.cn/CKV8OZ
2023年最新Android 面试题集:https://qr18.cn/CgxrRy
Android 车载开发岗位面试习题:https://qr18.cn/FTlyCJ
音视频面试题锦:https://qr18.cn/AcV6Ap

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

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

相关文章

【Git】git的安装与使用教程

&#x1f389;&#x1f389;欢迎来到我的CSDN主页&#xff01;&#x1f389;&#x1f389; &#x1f3c5;我是Java方文山&#xff0c;一个在CSDN分享笔记的博主。&#x1f4da;&#x1f4da; &#x1f31f;推荐给大家我的专栏《Git》。&#x1f3af;&#x1f3af; &#x1f449…

麒麟KYLINIOS软件仓库搭建03-软件仓库添加新版本的软件包

原文链接&#xff1a;麒麟KYLINIOS软件仓库搭建03-软件仓库添加新版本的软件包 hello&#xff0c;大家好啊&#xff0c;今天给大家带来麒麟桌面操作系统软件仓库搭建的文章03-软件仓库添加新版本的软件包&#xff0c;本篇文章主要给大家介绍了如何在麒麟桌面操作系统2203-x86版…

蓝桥杯每日一题2023.11.9

包子凑数 - 蓝桥云课 (lanqiao.cn) 题目描述 题目分析 对于此题是一个简单DP的翻版问题&#xff0c;若能凑出当前的包子数&#xff0c;则凑出之前一定为dp[i - a[j]]&#xff0c;若表示出的dp[i]不是0则说明是一定存在数可以被凑出的&#xff0c;由题意&#xff1a;若凑不出的…

金融工作怎么做?低代码如何助力金融行业

10月30日至31日&#xff0c;中央金融工作会议在北京举行。金融是国民经济的“血脉”&#xff0c;是国家核心竞争力的重要组成部分。会议指出&#xff0c;党的十八大以来&#xff0c;在党中央集中统一领导下&#xff0c;金融系统有力支撑经济社会发展大局&#xff0c;坚决打好防…

Vue集成海康websdk实现摄像头预览

选择以及下载相应的websdk&#xff1a; 从海康开放平台下载相应的sdk&#xff0c;web3.0不支持高版本浏览器&#xff0c;web3.2需要摄像头支持摄像头取流&#xff0c;web3.3支持高版本浏览器 我这选择的是3.3的。可以先测试下开发包是否可以成功访问&#xff0c;修改用ip、户名…

android开发布局知识

插件开发的视频笔记&#xff1a;

算法进阶指南图论 通信线路

通信线路 思路&#xff1a;我们考虑需要升级的那条电缆的花费&#xff0c;若其花费为 w &#xff0c;那么从 1 到 n 的路径上&#xff0c;至多存在 k 条路径的价值大于 w &#xff0c;这具有一定的单调性&#xff0c;当花费 w 越大&#xff0c;我们路径上价值大于 w 的花费会越…

读者自荐的 4 个 GitHub 项目

本期推荐的 4 个开源项目&#xff0c;为读者在开源项目 Awesome-GitHub-Repo 的评论区自推的, 如果你开源了不错的项目&#xff0c;想让大家看到&#xff0c;也可以去 Awesome-GitHub-Repo 进行投稿。 本期推荐开源项目目录&#xff1a; 1. DB-GPT 2. 定制中国传统节日头像 3. …

Java / Android 多线程和 synchroized 锁

s AsyncTask 在Android R中标注了废弃 synchronized 同步 Thread: thread.start() public synchronized void start() {/*** This method is not invoked for the main method thread or "system"* group threads created/set up by the VM. Any new functionali…

超全大厂UI库分享,可免费套用!

今天我们要给大家分享的是TDesign、Arco Design、Ant Design、Material design等6个优秀的大厂UI库&#xff0c;一次性打包送给大家&#xff0c;通通免费用。大厂UI库都是经过无数次的事件检验的&#xff0c;扛住了许多种使用场景和突发情况的组件资源库&#xff0c;是前人的经…

HarmonyOS NEXT 调优工具 Smart Perf Host 高效使用指南

在软件开发的过程中&#xff0c;很多开发者都经常会遇到一些性能问题&#xff0c;比如应用启动慢、点击滑动卡顿、应用后台被杀等&#xff0c;想要解决这些问题势必需要收集大量系统数据。而在收集数据的过程中&#xff0c;开发者则需要在各种工具和命令之间来回切换&#xff0…

k8s 部署mqtt —— 筑梦之路

mqtt是干嘛的&#xff0c;网上有很多资料&#xff0c;这里就不再赘述。 --- apiVersion: apps/v1 kind: Deployment metadata:labels:app: mqttname: mqttnamespace: default spec:replicas: 1selector:matchLabels:app: mqttstrategy:rollingUpdate:maxSurge: 25%maxUnavaila…

华为认证 | 11月底这门HCIP认证即将发布!

非常荣幸地通知您&#xff0c;华为认证HCIP-Storage V5.5&#xff08;中文版&#xff09;预计将于2023年11月30日正式对外发布。为了帮助您做好学习、培训和考试计划&#xff0c;现进行预发布通知&#xff0c;请您关注。 01 发布概述 基于“平台生态”战略&#xff0c;围绕“云…

Django快速入门(一)

Django三板斧 1. 基本使用 三板斧: HttpResponse,render,redirect from django.shortcuts import HttpResponse,render,redirect# 一. 返回字符串类型的数据 return HttpResponse(字符串) # 二. 返回HTML文件 # 1. 动态HTML页面: return render(request,login.html) def ab…

西门子S7-1200PLC混合通信编程(ModbusTcp和UDP通信)

S7-1200PLC的MODBUS-TCP通信 西门子PLC ModbusTcp通信访问网关后从站(SCL语言轮询状态机)-CSDN博客文章浏览阅读305次。西门子PLC的ModbusTcp通信在专栏已有很多文章介绍,所不同的是每个项目的通信需求都略有不同,今天我们以访问网关后的三个从站数据来举例,给出轮询的推荐…

iOS 16.4 之后真机与模拟器无法使用Safari调试H5页面问题

背景 iOS 16.4之后用真机调试H5时候发现&#xff0c;Safari中开发模块下面无法调试页面 解决方案 在WKWebView中设置以下代码解决 if (available(iOS 16.4, *)) {[_webView setInspectable:YES];}然后再次调试就可以了

[git] cherry pick 将某个分支的某次提交应用到当前分支

功能&#xff1a;将某个分支的某次提交应用到当前分支 应用场景&#xff1a; 在合并分支时&#xff0c;是将源分支的所有内容都合并到目标分支上&#xff0c;有的时候我们可能只需要合并源分支的某次或某几次的提交&#xff0c;这个时候我们就需要使用到git的cherry-pick操作…

边缘计算多角色智能计量插座:用电监测和资产管理的未来智能化引擎

目前主流的智能插座涵盖了红外遥控&#xff08;控制空调和电视等带有红外标准的电器&#xff09;&#xff0c;配备着测温、测湿等仓库应用场景&#xff0c;配备了人体红外或者毫米波雷达作为联动控制&#xff0c;但是大家有没有思考一个问题&#xff0c;就是随着对接的深入&…

易点易动固定资产管理系统:实现全生命周期闭环式管理和快速盘点

固定资产管理对于企业来说至关重要&#xff0c;它涉及到资产的采购、领用、使用、维护和报废等各个环节。然而&#xff0c;传统的固定资产管理方式往往繁琐、耗时&#xff0c;容易导致信息不准确和资源浪费。为了解决这些问题&#xff0c;我们引入易点易动固定资产管理系统&…

DevOps简介

DevOps简介 1、DevOps的起源2、什么是DevOps3、DevOps的发展现状4、DevOps与虚拟化、容器 1、DevOps的起源 上个世纪40年代&#xff0c;世界上第一台计算机诞生。计算机离不开程序&#xff08;Program&#xff09;驱动&#xff0c;而负责编写程序的人&#xff0c;被称为程序员&…