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。

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

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

相关文章

【Manus资料合集】激活码内测渠道+《Manus Al:Agent应用的ChatGPT时刻》(附资源)

DeepSeek 之后&#xff0c;又一个AI沸腾&#xff0c;冲击的不仅仅是通用大模型。 ——全球首款通用AI Agent的破圈启示录 2025年3月6日凌晨&#xff0c;全球AI圈被一款名为Manus的产品彻底点燃。由Monica团队&#xff08;隶属中国夜莺科技&#xff09;推出的“全球首款通用AI…

Python----计算机视觉处理(opencv:像素,RGB颜色,图像的存储,opencv安装,代码展示)

一、计算机眼中的图像 像素 像素是图像的基本单元&#xff0c;每个像素存储着图像的颜色、亮度和其他特征。一系列像素组合到一起就形成 了完整的图像&#xff0c;在计算机中&#xff0c;图像以像素的形式存在并采用二进制格式进行存储。根据图像的颜色不 同&#xff0c;每个像…

SQLiteStudio:一款免费跨平台的SQLite管理工具

SQLiteStudio 是一款专门用于管理和操作 SQLite 数据库的免费工具。它提供直观的图形化界面&#xff0c;简化了数据库的创建、编辑、查询和维护&#xff0c;适合数据库开发者和数据分析师使用。 功能特性 SQLiteStudio 提供的主要功能包括&#xff1a; 免费开源&#xff0c;可…

【软考网工-实践篇】DHCP 动态主机配置协议

一、DHCP简介 DHCP&#xff0c;Dynamic Host Configuration Protocol&#xff0c;动态主机配置协议。 位置&#xff1a;DHCP常见运行于路由器上&#xff0c;作为DHCP服务器功能&#xff1a;用于自动分配IP地址及其他网络参数给网络中的设备作用&#xff1a;简化网络管理&…

【Linux学习笔记】Linux用户和文件权限的深度剖析

【Linux学习笔记】Linux用户和文件权限的深度剖析 &#x1f525;个人主页&#xff1a;大白的编程日记 &#x1f525;专栏&#xff1a;Linux学习笔记 前言 文章目录 【Linux学习笔记】Linux用户和文件权限的深度剖析前言一. Linux权限管理1.1 文件访问者的分类&#xff08;人)…

Centos离线安装openssl-devel

文章目录 Centos离线安装openssl-devel1. openssl-devel是什么&#xff1f;2. openssl-devel下载地址3. openssl-devel安装4. 安装结果验证 Centos离线安装openssl-devel 1. openssl-devel是什么&#xff1f; openssl-devel 是 Linux 系统中与 OpenSSL 加密库相关的开发包&…

深度学习篇---Opencv中Haar级联分类器的自定义

文章目录 1. 准备工作1.1安装 OpenCV1.2准备数据集1.2.1正样本1.2.2负样本 2. 数据准备2.1 正样本的准备2.1.1步骤2.1.2生成正样本描述文件2.1.3示例命令2.1.4正样本描述文件格式 2.2 负样本的准备2.2.1步骤2.2.2负样本描述文件格式 3. 训练分类器3.1命令格式3.2参数说明 4. 训…

Smart Time Plus smarttimeplus-MySQLConnection SQL注入漏洞(CVE-2024-53544)

免责声明 本文所描述的漏洞及其复现步骤仅供网络安全研究与教育目的使用。任何人不得将本文提供的信息用于非法目的或未经授权的系统测试。作者不对任何由于使用本文信息而导致的直接或间接损害承担责任。如涉及侵权,请及时与我们联系,我们将尽快处理并删除相关内容。 0x01…

58.Harmonyos NEXT 图片预览组件架构设计与实现原理

温馨提示&#xff1a;本篇博客的详细代码已发布到 git : https://gitcode.com/nutpi/HarmonyosNext 可以下载运行哦&#xff01; Harmonyos NEXT 图片预览组件架构设计与实现原理 文章目录 Harmonyos NEXT 图片预览组件架构设计与实现原理效果预览一、组件架构概述1. 核心组件层…

虚拟机下ubuntu进不了图形界面

6.844618] piix4_smbus 0000:07.3: SMBus Host ContrFoller not enabled! 7.859836] sd 2:0:0:0:0: [sda] Assuming drive cache: wirite through /dev/sda1: clean, 200424/1966080 files, 4053235/7864064 blocks ubuntu启动时&#xff0c;卡在上面输出位置 当前遇到的原因…

Appium高级操作--从源码角度解析--模拟复杂手势操作

书接上回&#xff0c;Android自动化--Appium基本操作-CSDN博客文章浏览阅读600次&#xff0c;点赞10次&#xff0c;收藏5次。书接上回&#xff0c;上一篇文章已经介绍了appium在Android端的元素定位方法和识别工具Inspector&#xff0c;本次要介绍使用如何利用Appium对找到的元…

SpringBoot学生宿舍管理系统的设计与开发

项目概述 幽络源分享的《SpringBoot学生宿舍管理系统的设计与开发》是一款专为校园宿舍管理设计的智能化系统&#xff0c;基于SpringBoot框架开发&#xff0c;功能全面&#xff0c;操作便捷。该系统涵盖管理员、宿管员和学生三大角色&#xff0c;分别提供宿舍管理、学生信息管…

爱普生温补晶振 TG5032CFN高精度稳定时钟的典范

在科技日新月异的当下&#xff0c;众多领域对时钟信号的稳定性与精准度提出了极为严苛的要求。爱普生温补晶振TG5032CFN是一款高稳定性温度补偿晶体振荡器&#xff08;TCXO&#xff09;。该器件通过内置温度补偿电路&#xff0c;有效抑制环境温度变化对频率稳定性的影响&#x…

【原创】在高性能服务器上,使用受限用户运行Nginx,充当反向代理服务器[未完待续]

起因 在公共高性能服务器上运行OllamaDeepSeek&#xff0c;如果按照默认配置启动Ollama程序&#xff0c;则自己在远程无法连接你启动的Ollama服务。 如果修改配置&#xff0c;则会遇到你的Ollama被他人完全控制的安全风险。 不过&#xff0c;我们可以使用一个方向代理&#…

Bash和Zsh的主要差异是?

Bash&#xff08;GNU Bourne-Again Shell&#xff09; 和 Zsh&#xff08;Z Shell&#xff09; 都是功能强大的Unix/Linux Shell&#xff0c;广泛用于交互式使用和脚本编写。 尽管它们有很多相似之处&#xff0c;但在功能、语法、配置选项等方面也存在一些显著的区别。 是Bas…

芯科科技推出的BG29超小型低功耗蓝牙®无线SoC,是蓝牙应用的理想之选

具有扩大的内存和超低功耗特性的超小型BG29是互联健康设备的理想之选 低功耗无线领域内的领导性创新厂商Silicon Labs&#xff08;亦称“芯科科技”&#xff0c;NASDAQ&#xff1a;SLAB&#xff09;今日宣布&#xff1a;推出全新的第二代无线开发平台产品BG29系列无线片上系统…

【数据挖掘】知识蒸馏(Knowledge Distillation, KD)

1. 概念 知识蒸馏&#xff08;Knowledge Distillation, KD&#xff09;是一种模型压缩和知识迁移技术&#xff0c;旨在将大型复杂模型&#xff08;称为教师模型&#xff09;中的知识传递给一个较小的模型&#xff08;称为学生模型&#xff09;&#xff0c;以减少计算成本&…

选型消息队列(MQ):ActiveMQ、RabbitMQ、RocketMQ、Kafka对比

选型消息队列&#xff08;MQ&#xff09;&#xff1a;ActiveMQ、RabbitMQ、RocketMQ、Kafka对比 选型消息队列&#xff08;MQ&#xff09;1. 引言2. 消息队列核心指标3. MQ 技术对比分析4. 详细分析及案例4.1 ActiveMQ&#xff1a;传统企业级 MQ 方案4.2 RabbitMQ&#xff1a;高…

AWK 入门教程:强大的文本处理工具

AWK 是一种强大的文本处理工具&#xff0c;广泛用于 Linux/Unix 系统中对文本文件或数据流进行操作。它能够基于条件筛选、统计字段、重新排列数据等。主要特点包括&#xff1a; 2. AWK 的基本语法 2.1 AWK 程序的结构 AWK 程序的结构: awk pattern { action } file 2.2 常…

mysql select distinct 和 group by 哪个效率高

在有索引的情况下&#xff0c;SELECT DISTINCT和GROUP BY的效率相同&#xff1b;在没有索引的情况下&#xff0c;SELECT DISTINCT的效率高于GROUP BY‌。这是因为SELECT DISTINCT和GROUP BY都会进行分组操作&#xff0c;但GROUP BY可能会进行排序&#xff0c;触发filesort&…