Android子线程更新View的方法原理
对于所有的Android开发者来说,“View的更新必须在UI线程中进行”是一项最基本常识。
如果不在UI线程中更新View,系统会抛出CalledFromWrongThreadException异常。那么有没有什么办法可以不在UI线程中更新View?答案当然是有的!
一.ViewRootImpl渲染体系
在Android系统中,ViewRootImpl负责View的绘制调度、事件分发、窗口管理等功能。
各层级View遵循单一父View对应多个子View的关系,通过嵌套形成树形结构。
由于ViewRootImpl不是真正的View,因此ViewRootImpl只是View调度的根节点,并不是View树的根节点。View树真正的根节点是DecorView。DecorView继承自FrameLayout,是真正的View容器。ViewRootImpl通过管理DecorView,间接统筹管理所有层级的View。
1.DecorView的创建
当启动Activity时,系统会调用ActivityThread的handleLaunchActivity方法处理Activity的启动流程。
在ActivityThread的handleLaunchActivity方法中,会分别调用performLaunchActivity方法、handleStartActivity方法、handleResumeActivity方法,反射创建Activity,并回调Activity的生命周期,如下图所示:

在实际的开发过程中,通常会在Activity的onCreate方法中,调用setContentView方法,为Activity设置对应的View。在setContentView方法中,会调用installDecor方法,创建DecorView,如下图所示:

2.ViewRootImpl的创建
在ActivityThread的handleResumeActivity方法中,主要做了两件事:
1)回调Activity的onResume方法,切换生命周期。
2)调用Activity的makeVisible方法,创建ViewRootImpl与DecorView进行绑定。

在Activity的makeVisible方法中,会通过WindowManager创建ViewRootImpl对象,并与DecorView进行绑定,如下图所示:

在ViewRootImpl的setView方法中,ViewRootImpl会与DecorView进行双向绑定,如下图所示:

3.渲染体系与生命周期
在Activity的首次启动过程中:
- 回调onCreate方法时:调用setContentView方法,触发DecorView的创建。
- 回调onStart方法时:DecorView完成创建,ViewRootImpl未创建。
- 回调onResume方法时:DecorView完成创建,ViewRootImpl未创建。回调后立刻创建ViewRootImpl,并与DecorView完成绑定。
二.线程检测机制
1.异常产生
CalledFromWrongThreadException异常的抛出发生在ViewRootImpl类的checkThread方法中。当对View进行更新时,最终都会调用ViewRootImpl类的checkThread方法进行线程检测,代码如下:
void checkThread() {Thread current = Thread.currentThread();if (mThread != current) {throw new CalledFromWrongThreadException("Only the original thread that created a view hierarchy can touch its views."+ " Expected: " + mThread.getName()+ " Calling: " + current.getName());}
}
当判断调用checkThread方法的线程和mThread不一致时,会抛出CalledFromWrongThreadException异常。
2.检测路径
在ViewRootImpl中,共有13个方法在执行时会进行线程检测。如下所示:
- requestFitSystemWindows:请求调整View的布局以适应系统窗口。
- requestLayout:请求重新对View布局。
- invalidateChildInParent:通知父View某个子View需要重绘。
- setWindowStopped:设置Window的停止状态。
- requestTransparentRegion:请求计算View的透明区域。
- requestChildFocus:请求将焦点设置到某个子View上。
- clearChildFocus:清除子View焦点。
- focusableViewAvailable:通知父View某个子View可以获取焦点。
- recomputeViewAttributes:重新计算View的属性。
- playSoundEffect:播放与View交互相关的音效。
- focusSearch:在View树中搜索下一个可以获取焦点的View。
- keyboardNavigationClusterSearch:在键盘导航集群中搜索下一个可以获取焦点的View。
- doDie:销毁当前的ViewRootImpl。
但与View更新最为密切的是requestLayout方法和invalidateChildInParent方法。
在Android系统中,任何对View的更新操作,最终都要直接或间接调用View的invalidate方法或requestLayout方法。这两个方法会触发ViewRootImpl中的相应逻辑,在绘制调度前进行线程检测。
View的invalidate方法和requestLayout方法都会触发ViewRootImpl对View重新进行绘制调度(measure、layout、draw),但二者的区别在于:
- invalidate方法:标记当前区域为dirty,表示需要重新绘制,并在下一次绘制调度中触发draw流程,不会触发measure流程和layout流程。
- requestLayout方法:清除已经测量的数据,并在下一次绘制调度中触发measure流程和layout流程,如果在layout过程中发现View的大小发生变化,则会通过调用setFrame方法,间接触发调用一次invalidate方法,并在下一次绘制调度中触发draw流程。
1)invalidate方法触发线程检测
当调用View的invalidate方法时,invalidate方法内部会调用父View的invalidateChild方法,通过循环的方式,一层一层的获取父View,通知重新绘制,最终通知到ViewRootImpl,如下图所示:

在ViewRootImpl的invalidateChildInParent方法中,会进行线程检测,代码如下:
@Override
public ViewParent invalidateChildInParent(int[] location, Rect dirty) {// 线程检测checkThread();...return null;
}
2)requestLayout方法触发线程检测
当调用View的requestLayout方法时,会调用父View的requestLayout方法。通过一层一层的递归调用向上通知,最终通知到ViewRootImpl,如下图所示:

在ViewRootImpl的requestLayout方法中,会进行线程检测,代码如下:
@Override
public void requestLayout() {if (!mHandlingLayoutInLayoutRequest) {// 线程检测checkThread();mLayoutRequested = true;scheduleTraversals();}
}
三.子线程更新View
子线程更新View的方式分为两种:基于独立渲染体系和基于ViewRootImpl渲染体系。需要注意的是,尽管ViewRootImpl渲染体系支持在子线程更新View,但为了保证View状态的一致性,还是建议在UI线程更新View。
1.基于独立渲染体系
1)使用SurfaceView绘制
SurfaceView依靠自身维护BLASTBufferQueue获取Surface,在SurfaceFlinger中拥有独立的Layer。在绘制时不经过ViewRootImpl,详情参考:SurfaceView与TextureView的绘制渲染,代码如下:
class TestActivity : AppCompatActivity() {override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.layout_activity_test)// 获取SurfaceViewval view = findViewById<SurfaceView>(R.id.surface_view)// 创建调度器为IO线程的协程作用域val scope = CoroutineScope(Dispatchers.IO)// 监听Surface变化view.holder.addCallback(object : SurfaceHolder.Callback {override fun surfaceCreated(holder: SurfaceHolder) {// Surface创建时启动运行在IO线程的协程scope.launch {while (true) {// 每隔100ms绘制一次背景delay(100)val canvas = holder.lockCanvas()canvas.drawColor(Color.RED)holder.unlockCanvasAndPost(canvas)}}}override fun surfaceChanged(holder: SurfaceHolder,format: Int,width: Int,height: Int) {}override fun surfaceDestroyed(holder: SurfaceHolder) {// Surface销毁时取消作用域内的协程scope.cancel()}})}
}
2)使用TextureView绘制
TextureView依靠自身维护的SurfaceTexture获取Surface,在绘制时不经过ViewRootImpl。
但与SurfaceView不同的是,通过TextureView的Surface绘制后的内容,不会直接提交到SurfaceFlinger,而是通过回调的方式触发调用一次invalidate方法,并在下一次绘制时通过硬件加速层的方式挂在View树下一起绘制,详情参考:SurfaceView与TextureView的绘制渲染,代码如下:
class TestActivity : AppCompatActivity() {override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.layout_activity_test)// 获取TextureViewval view = findViewById<TextureView>(R.id.texture_view)// 创建调度器为IO线程的协程作用域val scope = CoroutineScope(Dispatchers.IO)// 监听SurfaceTexture变化view.surfaceTextureListener = object : TextureView.SurfaceTextureListener {override fun onSurfaceTextureAvailable(surface: SurfaceTexture,width: Int,height: Int) {// SurfaceTexture创建时启动运行在IO线程的协程scope.launch {while (true) {// 每隔100ms绘制一次背景delay(100)val canvas = view.lockCanvas() ?: continuecanvas.drawColor(Color.RED)view.unlockCanvasAndPost(canvas)}}}override fun onSurfaceTextureSizeChanged(surface: SurfaceTexture,width: Int,height: Int) {}override fun onSurfaceTextureDestroyed(surface: SurfaceTexture): Boolean {// SurfaceTexture销毁时取消作用域内的协程scope.cancel()return true}override fun onSurfaceTextureUpdated(surface: SurfaceTexture) {}}}
}
3)接管ViewRootImpl的Surface
当在Activity中调用Window的takeSurface方法,会接管ViewRootImpl的Surface,Activity的渲染会脱离ViewRootImpl渲染体系,相当于整个Activity都变成了SurfaceView,代码如下:
class TestActivity : AppCompatActivity() {override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)// 创建调度器为IO线程的协程作用域val scope = CoroutineScope(Dispatchers.IO)// 接管ViewRootImpl的Surfacewindow.takeSurface(object : SurfaceHolder.Callback2 {override fun surfaceCreated(holder: SurfaceHolder) {// Surface创建时启动运行在IO线程的协程scope.launch {while (true) {// 每隔100ms绘制一次背景delay(100)val canvas = holder.lockCanvas()canvas.drawColor(Color.RED)holder.unlockCanvasAndPost(canvas)}}}override fun surfaceChanged(holder: SurfaceHolder,format: Int,width: Int,height: Int) {}override fun surfaceDestroyed(holder: SurfaceHolder) {// Surface销毁时取消作用域内的协程scope.cancel()}override fun surfaceRedrawNeeded(holder: SurfaceHolder) {}})}
}
2.基于ViewRootImpl渲染体系
1)ViewRootImpl渲染体系形成前
当Activity首次启动并在onCreate方法内调用setContentView方法后,在onCreate方法、onStart方法、onResume方法中,使用非UI线程更新View,不会触发线程检测,代码如下:
class TestActivity : AppCompatActivity() {// 创建调度器为IO线程的协程作用域private val scope = CoroutineScope(Dispatchers.IO)// 标记在onResume方法中执行一次private var firstResume = true// 标记在onStart方法中执行一次private var firstStart = trueoverride fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.layout_activity_test)// onCreate方法中,启动运行在IO线程的协程scope.launch {// 更新TextView的文字内容findViewById<TextView>(R.id.text_view)?.text = "hello world"}}override fun onStart() {super.onStart()// 使用标志位,确保只在首次调用onStart时执行if(!firstStart) returnfirstStart = false// onStart方法中,启动运行在IO线程的协程scope.launch {// 更新TextView的文字内容findViewById<TextView>(R.id.text_view)?.text = "hello world !"}}override fun onResume() {super.onResume()// 使用标志位,确保只在首次调用onResume时执行if (!firstResume) returnfirstResume = false// onResume方法中,启动运行在IO线程的协程scope.launch {// 更新TextView的文字内容findViewById<TextView>(R.id.text_view)?.text = "hello world !!"}}
}
2)绑定ViewRootImpl渲染体系前
当动态创建完View后,在没有添加到与ViewRootImpl有关联的ViewGroup前,在非UI线程更新View,不会触发线程检测,代码如下:
class TestActivity : AppCompatActivity() {// 创建调度器为IO线程的协程作用域private val scope = CoroutineScope(Dispatchers.IO)override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.layout_activity_test)// 获取TextView,监听点击事件findViewById<TextView>(R.id.text_view)?.setOnClickListener {// 当点击TextView时,启动运行在IO线程的协程scope.launch {// 创建一个TextViewval view = TextView(this@TestActivity)// 设置文本内容view.text = "hello world"// 切换到UI线程withContext(Dispatchers.Main) {// 添加到DecorView中this@TestActivity.addContentView(view, ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,ViewGroup.LayoutParams.MATCH_PARENT))}}}}
}
4)硬件渲染模式下的invalidate方法
在软件渲染模式下,当调用View的invalidate方法时,会调用父类的invalidateChild方法。但在硬件渲染模式下,为了防止循环遍历耗时,会直接调用onDescendantInvalidated方法,代码如下:
@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;}...
}
在ViewGroup的onDescendantInvalidated方法中,会通过递归调用的方式,最终调用ViewRootImpl的onDescendantInvalidated方法,如下图所示:

在ViewRootImpl的onDescendantInvalidated方法中,会直接调用invalidate方法,跳过线程检查,代码如下:
private static boolean sToolkitEnableInvalidateCheckThreadFlagValue =Flags.enableInvalidateCheckThread();@Override
public void onDescendantInvalidated(@NonNull View child, @NonNull View descendant) {// Android Tool Kit为debug留的开关,默认为falseif (sToolkitEnableInvalidateCheckThreadFlagValue) {checkThread();}if ((descendant.mPrivateFlags & PFLAG_DRAW_ANIMATION) != 0) {mIsAnimating = true;}invalidate();
}@UnsupportedAppUsage
void invalidate() {mDirty.set(0, 0, mWidth, mHeight);if (!mWillDrawSoon) {// 启动绘制流程scheduleTraversals();}
}
Android系统默认的渲染模式为硬件渲染,这里在AndroidManifest中再手动声明一下,代码如下:
<manifest xmlns:tools="http://schemas.android.com/tools"xmlns:android="http://schemas.android.com/apk/res/android"package="com.test.ui">...<!-- 启动应用级别的硬件渲染模式 --><application android:hardwareAccelerated="true">...</application></manifest>
在代码使用上,硬件渲染与软件渲染基本没有差别。当开启硬件渲染模式后,在子线程直接或间接调用View的invalidate方法不会产生崩溃,代码如下:
class TestActivity : AppCompatActivity() {// 创建调度器为IO线程的协程作用域private val scope = CoroutineScope(Dispatchers.IO)// 文字大小private var size = 30foverride fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.layout_activity_test)}override fun onResume() {super.onResume()// 每次onResume时,启动一个运行在IO线程的协程scope.launch {// 更新文字大小size += 10f// 获取TextView,设置文字大小findViewById<TextView>(R.id.text_view)?.textSize = size}}
}
5)子线程中创建ViewRootImpl
实际上,Android系统并未要求View的更新必须在UI线程中进行。
通过分析CalledFromWrongThreadException异常抛出时的提示可以知道:View的更新必须在original thread中。而original thread就是ViewRootImpl中mThread字段保存的线程。
void checkThread() {Thread current = Thread.currentThread();if (mThread != current) {throw new CalledFromWrongThreadException("Only the original thread that created a view hierarchy can touch its views."+ " Expected: " + mThread.getName()+ " Calling: " + current.getName());}
}
在ViewRootImpl的构造方法中,会对mThread进行初始化,代码如下:
public ViewRootImpl(@UiContext Context context,Display display,IWindowSession session,WindowLayout windowLayout) {...// 获取当前的线程并保存mThread = Thread.currentThread();...
}
因此,Android系统要求View更新必须在UI线程执行,本质上是因为ViewRootImpl在UI线程被创建,并在构造方法中保存当前线程引用(mThread),并在每次操作时通过checkThread方法验证调用线程是否与mThread一致。
由于Activity的启动需要系统调度,系统会将Activity的启动安排在UI线程中进行,这也就导致无法在子线程中启动Activity,进而无法在子线程中创建ViewRootImpl。
但是在Android系统中,不仅Activity拥有ViewRootImpl,Dialog和PopupWindow等组件也各自拥有独立的ViewRootImpl。
如果在子线程中创建了Dialog或PopupWindow,那么后续对Dialog或PopupWindow中View的更新也必须在该子线程中进行,代码如下:
class TestActivity : AppCompatActivity() {// 创建HandlerThread,并启动子线程updateprivate val handleThread = HandlerThread("update").apply { start() }override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.layout_activity_test)// 创建子线程Handler,并向子线程update中提交一个任务Handler(handleThread.looper).post {// 获取容器Viewval parent = findViewById<ViewGroup>(R.id.container)// 通过加载XML的方式,创建一个子Viewval view = LayoutInflater.from(this).inflate(R.layout.layout_test_popup_window, parent, false)// 创建PopupWindow,并将子View添加进去val popupWindow = PopupWindow(view,ViewGroup.LayoutParams.MATCH_PARENT,ViewGroup.LayoutParams.MATCH_PARENT)// 从子View中获取TextViewval textView = popupWindow.contentView.findViewById<TextView>(R.id.pop_text)// 监听TextView的点击事件textView.setOnClickListener {// 更新TextView的文字内容,// 这里注意点击事件的回调线程变成了子线程updatetextView.text = "${Thread.currentThread()}"// 这里会产生CalledFromWrongThreadException异常// 因为没有在子线程update中更新window.decorView.post { textView.text = "${Thread.currentThread()}" }}// 这里先将任务提交到UI线程执行// 因为在onCreate方法中,容器View对应的Window还未创建好// 获取不到Window的Token,会产生异常window.decorView.post {// 切换到子线程,创建子线程Handler,并向子线程update中提交一个任务Handler(handleThread.looper).post {// 子线程中展示popupWindow,会触发ViewRootImpl在子线程update中创建popupWindow.showAtLocation(parent, Gravity.CENTER, 0, 0)}}}}
}
三.总结
1.View的更新必须在UI线程进行的原因
ViewRootImpl在UI线程中被创建,并在构造方法中保存了当前线程的引用(mThread)。在每次更新View时,通过调用View的invalidate方法或requestLayout方法触发ViewRootImpl的checkThread方法,验证调用线程是否与mThread一致。
2.Activity启动流程中渲染体系的创建
- 回调onCreate方法时:调用setContentView方法,触发DecorView的创建。
- 回调onStart方法时:DecorView完成创建,ViewRootImpl未创建。
- 回调onResume方法时:DecorView完成创建,ViewRootImpl未创建。回调后立刻创建ViewRootImpl,并与DecorView完成绑定。
3.invalidate方法与requestLayout方法的区别
View的invalidate方法和requestLayout方法都会触发ViewRootImpl对View重新进行绘制调度(measure、layout、draw),但二者的区别在于:
- invalidate方法:标记当前区域为dirty,表示需要重新绘制,并在下一次绘制调度中触发draw流程,不会触发measure流程和layout流程。
- requestLayout方法:清除已经测量的数据,并在下一次绘制调度中触发measure流程和layout流程,如果在layout过程中发现View的大小发生变化,则会通过调用setFrame方法,间接触发调用一次invalidate方法,并在下一次绘制调度中触发draw流程。
4.子线程更新View的方法
- 基于独立渲染体系
- 使用SurfaceView,直接对Surface进行绘制。
- 使用TextureView,直接对Surface进行绘制。
- 接管ViewRootImpl的Surface,直接对Surface进行绘制。
- 基于ViewRootImpl渲染体系
- 在ViewRootImpl渲染体系形成前,使用子线程更新View。
- 在绑定ViewRootImpl渲染体系前,使用子线程更新View。
- 硬件渲染模式下,子线程直接或间接调用View的invalidate方法。
- 对于独立拥有ViewRootImpl的组件,在子线程中触发组件创建ViewRootImpl,并在对应的子线程中更新View。
相关文章:
Android子线程更新View的方法原理
对于所有的Android开发者来说,“View的更新必须在UI线程中进行”是一项最基本常识。 如果不在UI线程中更新View,系统会抛出CalledFromWrongThreadException异常。那么有没有什么办法可以不在UI线程中更新View?答案当然是有的! 一…...
Kafka常用指令(详细)
Kafka常用指令(详细) 启停命令 前台启动 前台启动命令 ./bin/kafka-server-start.sh config/server.properties 后台启动方式1 后台启动命令加上参数-daemon,窗口关闭之后kafka后台程序继续运行 ./bin/kafka-server-start.sh -daemon co…...
Golang Channel 使用详解、注意事项与死锁分析
#作者:西门吹雪 文章目录 一、引言:Channel 在 Go 并发编程中的关键地位二、Channel 基础概念深度剖析2.1 独特特性2.2 类型与分类细解 三、Channel 基本使用实操指南3.1 声明与初始化3.3 单向 Channel 的运用 四、Channel 典型使用场景实战案例4.1 协程…...
使用LiteFlow实现阻塞审批工作流
在 LiteFlow 中实现阻塞的审批工作流,你可以使用异步处理与同步逻辑结合,实现节点的等待 使用 LiteFlow 实现阻塞审批工作流 下面是如何实现一个带有阻塞审批功能的 LiteFlow 工作流示例。 1. 引入依赖 确保您的 pom.xml 文件中已引入 LiteFlow 和 S…...
2025移动端软件供应链安全开源治理方案最佳实践
2025年3月13日,由中国软件评测中心、CAPPVD漏洞库联合主办的“第六期移动互联网APP产品安全漏洞技术沙龙”在海口成功召开。悬镜安全基于移动端数字供应链安全开源治理方案荣获中国软件评测中心“2024移动互联网APP产品安全漏洞治理”优秀案例,并获颁证书…...
Git Fast-forward 合并详解:原理、场景与最佳实践
在使用 Git 进行团队协作时,我们经常需要合并分支。合并方式有很多种,其中 Fast-forward(快速合并) 是一种最简单且无冲突的合并方式。本文将详细介绍 Fast-forward 的原理、适用场景、常见问题及最佳实践。 一、Fast-forward 合并…...
《C#上位机开发从门外到门内》2-3:SPI总线协议详解及应用实践
文章目录 一、引言二、SPI总线协议的基本原理三、SPI通信模式详解 —— CPOL与CPHA3.1 时钟极性(CPOL)3.2 时钟相位(CPHA)3.3 四种SPI模式 四、主从设备通信机制4.1 通信流程概述4.2 数据帧结构与传输细节4.3 主设备与从设备的协同…...
vscode出现:No module named ‘requests‘ 问题的解决方法
问题: ① No module named requests ② pip install requests:显示已经安装成功 运行失败原因: 我的失败原因是因为:我的python环境有两个,电脑C盘默认一个、pycharm下载后在它的路径下有一个。而vscode所运行的环境…...
【openwebui 搭建本地知识库(RAG搭建本地知识库)】
安装准备 openwebui 这个本地安装之前写过使用python安装。也可以直接用docker 命令 docker run --rm -d \-p 3080:8080 \-p 3081:8081 \-e WEBUI_AUTHtrue \-e DEFAULT_LOCALEcn \-e GLOBAL_LOG_LEVEL"INFO" \-e AIOHTTP_CLIENT_TIMEOUT100 \--privilegedtrue \-…...
Docker Compose 使用笔记
Docker Compose 是一个用于定义和运行多容器 Docker 应用程序的工具; docker-compose.yml 文件则是 Docker Compose 用来配置应用服务的核心文件,它以 YAML 格式编写。 YAML 文件用途: 服务定义:在 docker-compose.yml 文件中&…...
雷池WAF 处理 HTTP 请求的流程
项目介绍 SafeLine,中文名 "雷池",是一款简单好用, 效果突出的 Web 应用防火墙(WAF),可以保护 Web 服务不受黑客攻击。 雷池通过过滤和监控 Web 应用与互联网之间的 HTTP 流量来保护 Web 服务。可以保护 Web 服务免受 SQL 注入、…...
JAVA-Thread类实现多线程
引言: 本章博客涉及进程线程内容,如果不了解的可以看:什么是进程线程-CSDN博客 线程是操作系统的概念,操作系统提供的API供程序员使用操作。但是不同的操作系统(Winodws、Linux、Unix……差别很大),但是做为JAVA程序员就不需要担心…...
【算法】DFS、BFS、拓扑排序
⭐️个人主页:小羊 ⭐️所属专栏:算法 很荣幸您能阅读我的文章,诚请评论指点,欢迎欢迎 ~ 目录 持续更新中...1、DFS2、BFSN 叉树的层序遍历二叉树的锯齿形层序遍历二叉树最大宽度 3、多源BFS腐烂的苹果 4、拓扑排序 持续更新中…...
MySQL中 IN 到底走不走索引?
文章目录 前言数据库表结构查询sqlEXPLAIN介绍EXPLAIN 的输出每列解释 强制走索引查询时添加条件(复合索引字段)查询小时查询分钟 总结 前言 在 MySQL 中,IN 语句是否能够利用索引取决于多个因素,包括但不限于查询的具体形式、表的统计信息、索引的选择…...
centos没有ll
vi /etc/bashrc alias ll‘ls -l’ source /etc/bashrc...
腾讯云低代码开发应用
创建客户端应用 如上所示,登录腾讯云微搭低代码业务控制台,开始搭建企业官网应用 如上所示,在腾讯云微搭低代码业务控制台中,开始创建企业官网应用 如上所示,在腾讯云微搭低代码业务控制台中,开始编辑企业官…...
医疗APP开发如何实现跨机构数据互通
医疗APP开发如何实现跨机构数据互通 在数字化医疗时代,医疗APP开发已成为连接医疗机构、患者和医疗资源的重要桥梁。然而,如何实现跨机构的数据互通,成为医疗APP开发中的一大挑战。本文将探讨如何通过医疗APP开发实现跨机构数据互通,提升医疗服务效率和患者体验。我们将涵…...
深度学习项目--基于DenseNet网络的“乳腺癌图像识别”,准确率90%+,pytorch复现
🍨 本文为🔗365天深度学习训练营 中的学习记录博客🍖 原作者:K同学啊 前言 如果说最经典的神经网络,ResNet肯定是一个,从ResNet发布后,很多人做了修改,denseNet网络无疑是最成功的…...
级联树SELECTTREE格式调整
步骤: 1、将全部列表设置成Map<Long, List<Obejct>> map的格式,方便查看每个父级对应的子列表,减少循环次数 2、对这个map进行递归,重新进行级联树的集合调整,将子集放置在对应的childs里面。 public Dyna…...
编译RTTR 0.9.6 (CMake + vs2019)解决std::iterator对rapidjson编译事项
RTTR编译 使用CMake和VS2019 x64编译RTTR 0.9.6指南一、下载RTTR 0.9.6并配置CMake二、在VS2019上编译RTTR 0.9.6解决rapidjson与C17兼容性问题 三、安装RTTR四、最简单的还是用vcpkg 使用CMake和VS2019 x64编译RTTR 0.9.6指南 本文将指导您完成从下载RTTR 0.9.6到使用CMake生…...
深入理解JavaScript构造函数与原型链:从原理到最佳实践
一、开篇:为什么需要理解原型链? 在JavaScript开发中,90%以上的"诡异"bug都与原型链机制相关。理解构造函数与原型链的运行原理,不仅能帮助我们写出更优雅的代码,还能在框架源码阅读、性能优化等场景中游刃…...
【Linux 指北】常用 Linux 指令汇总
第一章、常用基本指令 # 注意: # #表示管理员 # $表示普通用户 [rootlocalhost Practice]# 说明此处表示管理员01. ls 指令 语法: ls [选项][目录或文件] 功能:对于目录,该命令列出该目录下的所有子目录与文件。对于文件…...
第27周JavaSpringboot 前后端联调
电商前后端联调课程笔记 一、项目启动与环境搭建 1.1 项目启动 在学习电商项目的前后端联调之前,需要先掌握如何启动项目。项目启动是整个开发流程的基础,只有成功启动项目,才能进行后续的开发与调试工作。 1.1.1 环境安装 环境安装是项…...
QT中的布局管理
在 Qt 中,布局管理器(如 QHBoxLayout 和 QVBoxLayout)的构造函数可以接受一个 QWidget* 参数,用于指定该布局的父控件。如果指定了父控件,布局会自动将其管理的控件添加到父控件中。 在你的代码中,QHBoxLa…...
.net 6.0 webapi支持 xml返回xml json返回json
// 添加控制器并配置格式化器 var builder WebApplication.CreateBuilder(); builder.Services.AddControllers(options > {options.Filters.Add<ContentTypeFilter>();options.ReturnHttpNotAcceptable true; // 强制要求Accept头匹配// 添加 XML 格式化器options.…...
docker 搭建alpine下nginx1.26/mysql8.0/php7.4环境
docker 搭建alpine下nginx1.26/mysql8.0/php7.4环境 docker-compose.yml services:mysql-8.0:container_name: mysql-8.0image: mysql:8.0restart: always#ports:#- "3306:3306"volumes:- ./etc/mysql/conf.d/mysql.cnf:/etc/mysql/conf.d/mysql.cnf:ro- ./var/log…...
Android7上移植I2C-tools
一,下载源码 cd hardware/libhardware/tests git clone https://git.kernel.org/pub/scm/utils/i2c-tools/i2c-tools.git 二, 在 i2c-tools 目录添加 Android.mk 编译文件 LOCAL_PATH: $(call my-dir)################### i2c-tools ###############…...
Centos 7 修改语言和输入源为中文+修改终端快捷键复制为Ctrl+C、粘贴为Ctrl+V
目录 修改语言和输入源为中文 1、设置 2、Region & Language(区域和语言) 3、Add an Input Source(添加输入源) 4、修改语言为中文 5、Restart(重启) 6、Log Out (注销) …...
DeepSeek-进阶版部署(Linux+GPU)
前面几个小节讲解的Win和Linux部署DeepSeek的比较简单的方法,而且采用的模型也是最小的,作为测试体验使用是没问题的。如果要在生产环境使用还是需要用到GPU来实现,下面我将以有一台带上GPU显卡的Linux机器来部署DeepSeek。这里还只是先体验单…...
疯狂安卓入门,crayandroid
系列文章目录 文章目录 系列文章目录第一组 ViewGroup 为基类帧布局约束布局 第二组 TextView 及其子类button时钟 AnalogClock 和 TextClock计时器 第三组 ImageView 及其子类第四组 AdapterView 及其子类AutoCompleteTextView 的功能和用法ExapndaleListViewAdapterViewFlipp…...
