Android端使用无障碍服务实现远程、自动刷短视频

最近在做一个基于无障碍自动刷短视频的APP,需要支持用任意蓝牙遥控器远程控制, 把无障碍服务流程大致研究了一下,从下面3个部分做一下小结。

1、需要可调整自动上滑距离和速度以适配不同的屏幕和应用

智能适配99%机型,滑动参数可自由调节。

默认的距离和速度可能在个别手机上无法达到滑屏的要求,表现就是屏幕可见滑动了下,但还是停留回当前界面。所以需要给用户一种自定义调整的方式,这里以【辅助触控】APP为例,提供了屏幕配置的实现,可以自行拖动滑动起始点,然后调整滑动参数。

编辑屏幕的时候支持增删按键映射(12个可编程功能键 (F1-F12)),并自定义如下参数:

  • ➕ 单击/双击/连击/长按
  • ⚙️ 自定义间隔(0.1-30秒)
  • ⏱️ 按压停留时长设置

支持手势轨迹定制如下参数:

  • 📍 起点/终点坐标设置
  • ⏲️ 自定义间隔(3-30秒)和滑动速度
  • 📐 四向滑动独立配置

在定义好按键映射后,还可以对其进行组合控制,编写一组相关动作然后执行。

其中还可以进一步定义文本识别后要执行的动作,比如单击文本节点、返回、上滑等。

2、监听TYPE_WINDOW_STATE_CHANGED事件,在图形验证码出现时停止,需要能识别出带有关键文本的视图元素

比如支付宝看视频领红包活动,会一定机率跳出图形验证码,需要用户手动点选,如果此界面继续滑屏,很容易被系统识别到正在进行自动化脚本刷屏。需要对界面内容识别,比如文本 “请依次点击下面的图案”。

在自动滑屏期间检测到该事件,说明有窗口焦点切换,一般就是切换到了不同的窗口,比如 Dialog、PopupWindow等。有可能就是这个验证框,这时候我们需要拿到getRootInActiveWindow(),然后通过无障碍API findAccessibilityNodeInfosByText找出包含上面文本的Node。

//这里是我们要找的可能的文本
val ocrTexts = listOf("请在下图依次点击")
for(ocrTry in 0 until 4) {for (text in ocrTexts) {//找包含text的那些节点,这些节点要么是能呈现指定文本(text、hint)的视图,要么是包含指定内容描述(content description)的视图var nodes = rootInActiveWindow?.findAccessibilityNodeInfosByText(text)nodes?.forEach { nodeInfo ->//找到了验证框,停止滑屏,并发出声音和震动提示用户,需要手动验证。autoRepeatIntervalJob?.cancel()playBeepSoundAndVibrate(5000)return}}if (ocrTry < 3) {//延迟一下,有可能文本内容还没加载Thread.sleep(500)}
}

在循环中每次我们都重新获取rootInActiveWindow, 否则可能获取到的不是当前界面,不用担心性能问题,只要没有新的TYPE_WINDOW_STATE_CHANGED事件发生,都会使用缓存。所以每次获取的好处就是即使事件发生了,我下一个循环就能得到新界面。

实测发现,如下图支付宝这个验证框使用findAccessibilityNodeInfosByText居然找不到

难道他是图片?带着怀疑我用uiautomatorviewer看了下布局,发现只是一个TextView, 文本也是“请在下图依次点击”,和我们检索字符串一样,只是它的ImportantForAccessibility属性是false, 我们的AccessibilityService的config里也添加了flagIncludeNotImportantViews(包括不重要的视图),照理应该能找到并返回。然后我又尝试了下遍历的方式:

fun AccessibilityService.findTextByTraversal(text: String, include: Boolean = false): List<AccessibilityNodeInfo> {val result = mutableListOf<AccessibilityNodeInfo>()traverseNodes(result, rootInActiveWindow, text, include)return result
}
private fun traverseNodes(result: MutableList<AccessibilityNodeInfo>,node: AccessibilityNodeInfo?,searchText: String,include: Boolean = false,
) {node?.let {if (node.text != null && node.text.isNotEmpty()) {if (include && node.text.contains(searchText)) {result.add(node)} else if (node.text == searchText) {result.add(node)}if (DebugUtils.DEBUG) DebugUtils.logD(TAG, "traverseNodes find $node")}for (i in 0 until node.childCount) {traverseNodes(result, node.getChild(i), searchText, include)}}
}

竟然能找到这个node,这样的话先修改一下逻辑,优先使用findAccessibilityNodeInfosByText,找不到再递归找。

第3部分我们通过源码梳理一下AccessibilityService和AccessibilityManagerService之间的通信过程,尝试分析一下findAccessibilityNodeInfosByText是怎样进行查找的?

3、AccessibilityService和AccessibilityManagerService之间的通信过程

参考源码

https://xrefandroid.com/android-11.0.0_r48/

首先我们需要简单了解一下AccessibilityService启动流程。

AccessibilityService启动流程

在SystemServer主进程服务启动阶段,AccessibilityManagerServiceAMS)作为系统服务被初始化,负责管理全局无障碍服务生命周期及事件分发‌。

// frameworks/base/services/java/com/android/server/SystemServer.java
private static final String ACCESSIBILITY_MANAGER_SERVICE_CLASS = "com.android.server.accessibility.AccessibilityManagerService$Lifecycle";
private void startOtherServices(@NonNull TimingsTraceAndSlog t) {...try {mSystemServiceManager.startService(ACCESSIBILITY_MANAGER_SERVICE_CLASS);} catch (Throwable e) {reportWtf("starting Accessibility Manager", e);}}

实例化AccessibilityManagerService$Lifecycle对象并调用其onStart()

将AMS发布出来,之后就可以通过Context.getSystemService(Context.ACCESSIBILITY_SERVICE)获取对应的AccessibilityManager来和AMS通信。

AMS init初始化时注册 PackageMonitor 监听应用安装/卸载事件,动态维护已注册的无障碍服务列表‌,注册ACTION_USER_PRESENT读取所有已安装应用的无障碍服务信息查询所有已安装应用中的无障碍服务信息:

//frameworks/base/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java

其中只要有somethingChanged = true, 比如下面这个,读取到已安装应用发生了变更

则调用onUserStateChangedLocked更新相关信息:

包括绑定App的AccessibilityService, 创建一个AccessibilityServiceConnection来绑定服务和完成两者之间的通信。

绑定成功后,会回调onServiceConnected(ComponentName componentName, IBinder service)

我们知道Service的绑定过程是,被绑定的服务会启动,然后在onBind(Intent)返回一个IBinder对象给绑定者,   绑定者在onServiceConnected中可以获取到一个用于和Service进行IPC通信的接口对象IBinder。

比如前面提到的TYPE_WINDOW_STATE_CHANGED事件,传递过程为:

AMS sendAccessibilityEvent(AccessibilityEvent event, int userId)  ->  AMS notifyAccessibilityServicesDelayedLocked(AccessibilityEvent event, boolean isDefault) -> AccessibilityServiceConnection.notifyAccessibilityEvent(event) -> mServiceInterface.onAccessibilityEvent(event, serviceWantsEvent) ···IPC···> IAccessibilityServiceClientWrapper.onAccessibilityEvent(event, serviceWantsEvent) -> AccessibilityService.onAccessibilityEvent(event)

 上面是AMS到AccessibilityService的通信,AccessibilityService到AMS则是通过AccessibilityInteractionClient。 

前面已经提到在onBind回调的时候,我们返回了一个IAccessibilityServiceClientWrapper IBinder给AMS, AMS在绑定服务成功后拿到service IBinder,调用了initializeService,将AMS端的AccessibilityServiceConnection回传给了AccessibilityService,如下代码所示:

public void onServiceConnected(ComponentName componentName, IBinder service) {...mServiceInterface = IAccessibilityServiceClient.Stub.asInterface(service); //service就是IAccessibilityServiceClientWrapper IBinder...mMainHandler.sendMessage(obtainMessage(AccessibilityServiceConnection::initializeService, this));...
}
private void initializeService() {...serviceInterface.init(this, mId, mOverlayWindowTokens.get(Display.DEFAULT_DISPLAY));...
}//frameworks/base/core/java/android/accessibilityservice/AccessibilityService$IAccessibilityServiceClientWrapper
public void init(IAccessibilityServiceConnection connection, int connectionId,   IBinder windowToken) {Message message = mCaller.obtainMessageIOO(DO_INIT, connectionId,  connection, windowToken);mCaller.sendMessage(message);
}public void executeMessage(Message message) {...case DO_INIT: {mConnectionId = message.arg1;SomeArgs args = (SomeArgs) message.obj;IAccessibilityServiceConnection connection = (IAccessibilityServiceConnection) args.arg1;IBinder windowToken = (IBinder) args.arg2;args.recycle();if (connection != null) {//关联 IAccessibilityServiceConnectionAccessibilityInteractionClient.getInstance().addConnection(mConnectionId, connection);mCallback.init(mConnectionId, windowToken);mCallback.onServiceConnected();}...}...
}

IAccessibilityServiceClientWrapper 将AMS传来的IAccessibilityServiceConnection添加到AccessibilityInteractionClient中缓存起来,后续用来和AMS通信。

到这里我们的AccessibilityService与AMS的通道就建好了:

AccessibilityService -> AccessibilityInteractionClient -> IAccessibilityServiceConnection  ···IPC···> AccessibilityServiceConnection -> AMS 

现在回头来看findAccessibilityNodeInfosByText, 一般我们需要先getRootInActiveWindow获取root节点。

getRootInActiveWindow获取root节点

public AccessibilityNodeInfo getRootInActiveWindow() {return AccessibilityInteractionClient.getInstance().getRootInActiveWindow(mConnectionId);}//AccessibilityInteractionClient
public AccessibilityNodeInfo getRootInActiveWindow(int connectionId) {//这里使用固定的ACTIVE_WINDOW_ID和ROOT_NODE_ID,在AMS那边会对应到当前可交互窗口的root,return findAccessibilityNodeInfoByAccessibilityId(connectionId,AccessibilityWindowInfo.ACTIVE_WINDOW_ID, AccessibilityNodeInfo.ROOT_NODE_ID,  false, AccessibilityNodeInfo.FLAG_PREFETCH_DESCENDANTS, null);
}public @Nullable AccessibilityNodeInfo findAccessibilityNodeInfoByAccessibilityId(int connectionId, @NonNull IBinder leashToken, long accessibilityNodeId,boolean bypassCache, int prefetchFlags, Bundle arguments) {if (leashToken == null) {return null;}int windowId = -1;try {//获取前面关联的缓存在线程中的IAccessibilityServiceConnectionIAccessibilityServiceConnection connection = getConnection(connectionId);if (connection != null) {windowId = connection.getWindowIdForLeashToken(leashToken);} else {if (DEBUG) {Log.w(LOG_TAG, "No connection for connection id: " + connectionId);}}} catch (RemoteException re) {Log.e(LOG_TAG, "Error while calling remote getWindowIdForLeashToken", re);}if (windowId == -1) {return null;}return findAccessibilityNodeInfoByAccessibilityId(connectionId, windowId,accessibilityNodeId, bypassCache, prefetchFlags, arguments);
}public AccessibilityNodeInfo findAccessibilityNodeInfoByAccessibilityId(int connectionId,int accessibilityWindowId, long accessibilityNodeId, boolean bypassCache,int prefetchFlags, Bundle arguments) {...//向AMS connection发请求, 传入AccessibilityInteractionClient自身作为callback,用于接收结果回调packageNames = connection.findAccessibilityNodeInfoByAccessibilityId(accessibilityWindowId, accessibilityNodeId, interactionId, this,prefetchFlags, Thread.currentThread().getId(), arguments);...//等待AMS 返回结果List<AccessibilityNodeInfo> infos = getFindAccessibilityNodeInfosResultAndClear(interactionId);...
}//frameworks/base/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java
public String[] findAccessibilityNodeInfoByAccessibilityId(int accessibilityWindowId, long accessibilityNodeId, int interactionId,IAccessibilityInteractionConnectionCallback callback, int flags,long interrogatingTid, Bundle arguments) throws RemoteException {...// 解析当前的windowIdresolvedWindowId = resolveAccessibilityWindowIdLocked(accessibilityWindowId);...//找到当前窗口和AMS之间的交互连接对象connection = mA11yWindowManager.getConnectionLocked(mSystemSupport.getCurrentUserIdLocked(), resolvedWindowId);...//将请求通过IPC发给连接的远程端connection.getRemote().findAccessibilityNodeInfoByAccessibilityId(accessibilityNodeId, partialInteractiveRegion, interactionId, callback,mFetchFlags | flags, interrogatingPid, interrogatingTid, spec, arguments);...
}

这里经过查阅源码,发现connection是应用通过ViewRootImpl创建新窗口(如 Activity、Dialog、PopupWindow 等)时,会通过IPC向AMS进行addAccessibilityInteractionConnection() 调用,从而注册窗口与AMS之间的交互连接对象,

connection.getRemote()就是这个对象 ,如下源码所示的AccessibilityInteractionConnection:

//frameworks/base/core/java/android/view/ViewRootImpl.java
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView,int userId) {...if (mAccessibilityManager.isEnabled()) {mAccessibilityInteractionConnectionManager.ensureConnection();}...
}final class AccessibilityInteractionConnectionManagerimplements AccessibilityStateChangeListener {...public void ensureConnection() {final boolean registered = mAttachInfo.mAccessibilityWindowId!= AccessibilityWindowInfo.UNDEFINED_WINDOW_ID;if (!registered) {mAttachInfo.mAccessibilityWindowId =mAccessibilityManager.addAccessibilityInteractionConnection(mWindow,mLeashToken,mContext.getPackageName(),new AccessibilityInteractionConnection(ViewRootImpl.this));}}...
}static final class AccessibilityInteractionConnection extends IAccessibilityInteractionConnection.Stub {private final WeakReference<ViewRootImpl> mViewRootImpl;AccessibilityInteractionConnection(ViewRootImpl viewRootImpl) {mViewRootImpl = new WeakReference<ViewRootImpl>(viewRootImpl);}...@Overridepublic void findAccessibilityNodeInfoByAccessibilityId(long accessibilityNodeId,Region interactiveRegion, int interactionId,IAccessibilityInteractionConnectionCallback callback, int flags,int interrogatingPid, long interrogatingTid, MagnificationSpec spec, Bundle args) {ViewRootImpl viewRootImpl = mViewRootImpl.get();if (viewRootImpl != null && viewRootImpl.mView != null) {viewRootImpl.getAccessibilityInteractionController().findAccessibilityNodeInfoByAccessibilityIdClientThread(accessibilityNodeId,interactiveRegion, interactionId, callback, flags, interrogatingPid,interrogatingTid, spec, args);} else {// We cannot make the call and notify the caller so it does not wait.try {callback.setFindAccessibilityNodeInfosResult(null, interactionId);} catch (RemoteException re) {/* best effort - ignore */}}}...}

所以,最终AMS会调用到了应用端,同时传递了回调callback用于接收结果:

viewRootImpl.getAccessibilityInteractionController().findAccessibilityNodeInfoByAccessibilityIdClientThread(accessibilityNodeId,interactiveRegion,interactionId, callback, flags, interrogatingPid,interrogatingTid,spec,args);

//frameworks/base/core/java/android/view/AccessibilityInteractionController.java

我们之前在AccessibilityService中调用getRootInActiveWindow使用的accessibilityIdAccessibilityNodeInfo.ROOT_NODE_ID,这里得到的就是mViewRootImpl.mView,即窗口的根视图DecorView

如果此时root view已经可见,则封装并返回root的无障碍节点信息:

//frameworks/base/core/java/android/view/AccessibilityInteractionController$AccessibilityNodePrefetcher
public void prefetchAccessibilityNodeInfos(View view, int virtualViewId, int fetchFlags,List<AccessibilityNodeInfo> outInfos, Bundle arguments) {...AccessibilityNodeInfo root = view.createAccessibilityNodeInfo();if (root != null) {...outInfos.add(root);...}...
}

之后将找到的节点通过回调callback.setFindAccessibilityNodeInfosResult(infos, interactionId)传回给请求方。 

AMS端传递的callback对应的是AccessibilityService端的AccessibilityInteractionClient这个Binder 结果也就传到了AccessibilityInteractionClient,即IPC调用过程如下:

<发起请求>

AccessibilityService  -> IAccessibilityServiceConnection ···IPC···> AccessibilityServiceConnection -> AMS -> IAccessibilityInteractionConnection ···IPC···> AccessibilityInteractionConnection -> 当前窗口应用的ViewRootImpl

<返回结果>

当前窗口应用的ViewRootImpl  -> callback ···IPC···> AccessibilityService

返回的是一个列表,我们使用第一个作为找到的root节点。

findAccessibilityNodeInfosByText

和前面的IPC调用过程一样,我们直接去ViewRootImpl去找对应的方法:

@Override
public void findAccessibilityNodeInfosByText(long accessibilityNodeId, String text,Region interactiveRegion, int interactionId,IAccessibilityInteractionConnectionCallback callback, int flags,int interrogatingPid, long interrogatingTid, MagnificationSpec spec) {ViewRootImpl viewRootImpl = mViewRootImpl.get();if (viewRootImpl != null && viewRootImpl.mView != null) {viewRootImpl.getAccessibilityInteractionController().findAccessibilityNodeInfosByTextClientThread(accessibilityNodeId, text,interactiveRegion, interactionId, callback, flags, interrogatingPid,interrogatingTid, spec);} else {// We cannot make the call and notify the caller so it does not wait.try {callback.setFindAccessibilityNodeInfosResult(null, interactionId);} catch (RemoteException re) {/* best effort - ignore */}}
}//AccessibilityInteractionController.java
private void findAccessibilityNodeInfosByTextUiThread(Message message) {...List<AccessibilityNodeInfo> infos = null;final View root = findViewByAccessibilityId(accessibilityViewId);ArrayList<View> foundViews = mTempArrayList;foundViews.clear();//首先找出包含检索字符串的viewroot.findViewsWithText(foundViews, text, View.FIND_VIEWS_WITH_TEXT| View.FIND_VIEWS_WITH_CONTENT_DESCRIPTION| View.FIND_VIEWS_WITH_ACCESSIBILITY_NODE_PROVIDERS);if (!foundViews.isEmpty()) {infos = mTempAccessibilityNodeInfoList;infos.clear();final int viewCount = foundViews.size();for (int i = 0; i < viewCount; i++) {//依次遍历找到的view,满足条件则生成AccessibilityNodeInfo,添加到结果列表中View foundView = foundViews.get(i);if (isShown(foundView)) {provider = foundView.getAccessibilityNodeProvider();if (provider != null) {//这里最可能是隐藏不重要节点的地方,通过自定义AccessibilityNodeProvider实现,返回null或者空即可List<AccessibilityNodeInfo> infosFromProvider = provider.findAccessibilityNodeInfosByText(text,
AccessibilityNodeProvider.HOST_VIEW_ID);if (infosFromProvider != null) {infos.addAll(infosFromProvider);}} else  {infos.add(foundView.createAccessibilityNodeInfo());}}}}...//通知callback结果updateInfosForViewportAndReturnFindNodeResult(infos, callback, interactionId, spec, interactiveRegion);}

主要看root.findViewsWithText

//ViewGroup实现

//View默认实现

默认情况下,View 类的 getAccessibilityNodeProvider() 返回 null。 

 //TextView实现

根据代码或者注释,我们知道了匹配规则,系统会遍历View树,只要view可见,定义了content description或text,并且包含我们要查找的文本(忽略大小写),这个view就认为是需要的。所有符合条件的view依次封装为AccessibilityNodeInfo,添加到结果列表infos中:

infos.add(foundView.createAccessibilityNodeInfo());

之后返回结果给callback。

callback.setFindAccessibilityNodeInfosResult(infos, interactionId);

到目前为止并没有看到根据view的importantForAccessibility=no来过滤视图,唯一可能得地方就是foundView自定义了AccessibilityNodeProvider进行了过滤,如源码所示:

provider = foundView.getAccessibilityNodeProvider();
if (provider != null) {List<AccessibilityNodeInfo> infosFromProvider =provider.findAccessibilityNodeInfosByText(text, AccessibilityNodeProvider.HOST_VIEW_ID);if (infosFromProvider != null) {infos.addAll(infosFromProvider);}
}

只要provider.findAccessibilityNodeInfosByText此时返回null即可。

而遍历节点树的方式只要我们的AccessibilityServie申明了包含不重要视图这个flag, View就能在节点树里找到。

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

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

相关文章

Spark和Hadoop的区别和联系

Hadoop 和 Spark 的区别 1. 架构 Hadoop&#xff1a;基于 HDFS&#xff08;分布式文件系统&#xff09;和 MapReduce&#xff08;分布式计算框架&#xff09;。HDFS 负责数据的分布式存储&#xff0c;而 MapReduce 是其主要的计算框架&#xff0c;通过 Map 和 Reduce 任务进行…

【版本控制】idea中使用git

大家好&#xff0c;我是jstart千语。接下来继续对git的内容进行讲解。也是在开发中最常使用&#xff0c;最重要的部分&#xff0c;在idea中操作git。目录在右侧哦。 如果需要git命令的详解&#xff1a; 【版本控制】git命令使用大全-CSDN博客 一、配置git 要先关闭项目&#xf…

论文阅读:2023 arxiv A Survey of Reinforcement Learning from Human Feedback

A Survey of Reinforcement Learning from Human Feedback https://arxiv.org/pdf/2312.14925 https://www.doubao.com/chat/3506943124865538 速览 这篇论文是关于“从人类反馈中进行强化学习&#xff08;RLHF&#xff09;”的综述&#xff0c;核心是讲如何让AI通过人类反…

单片机 + 图像处理芯片 + TFT彩屏 进度条控件

进度条控件使用说明 概述 本进度条控件基于单片机 RA8889/RA6809 TFT开发&#xff0c;提供了简单易用的进度显示功能。控件支持多个进度条同时显示、自定义颜色、边框和标签等特性&#xff0c;适用于需要直观显示进度信息的各类应用场景。 特性 支持多个进度条同时显示可…

数据处理: OPTICS聚类及Python实现

1. 基本原理 OPTICS&#xff08;Ordering Points To Identify the Clustering Structure&#xff09;是一种基于密度的聚类算法&#xff0c;可视为DBSCAN的改进版本。它能够识别不同密度的簇&#xff0c;并自动发现数据中的层次化聚类结构&#xff0c;适用于复杂分布的数据集…

PyCharm 在 Linux 上的完整安装与使用指南

PyCharm 在 Linux 上的完整安装与使用指南—目录 一、PyCharm 简介二、下载与安装1. 下载 PyCharm2. 安装前的依赖准备3. 安装步骤方法 1&#xff1a;通过 Snap 安装&#xff08;推荐&#xff09;方法 2&#xff1a;手动安装&#xff08;从官网下载 .tar.gz 文件&#xff09;方…

【React】路由器 React-Router

安装路由模式路由组件和属性 (Link、NavLink、Outlet、Routes、Navigate、element)路由传参 ( Hook&#xff1a;useParams 、useSearchParams )路由跳转&#xff08;Hook&#xff1a;useNavigate&#xff09;路由的构建 前端路由指的是一种将浏览器URL与特定页面或视图关联起来…

Flowable7.x学习笔记(十)分页查询已部署 BPMN XML 流程

前言 上一篇文章我们已经完成了流程的部署功能&#xff0c;那么下一步就是要激活流程了&#xff0c;但是我们要需要明确的指定具体要激活部署后的哪一条流程&#xff0c;所以我们先把已部署的基础信息以及具体定义信息分页查询出来&#xff0c;本文先把基础代码生成以及完成分页…

【论文阅读23】-地下水预测-TCN-LSTM-Attention(2024-11)

这篇论文主要围绕利用深度学习模型检测地下水位异常以识别地震前兆展开。 [1] Chen X, Yang L, Liao X, et al. Groundwater level prediction and earthquake precursor anomaly analysis based on TCN-LSTM-attention network[J]. IEEE Access, 2024, 12: 176696-176718. 期刊…

electron从安装到启动再到打包全教程

目录 介绍 安装 修改npm包配置 执行安装命令 源代码 运行 打包 先安装git, 安装打包工具 导入打包工具 执行打包命令 总结 介绍 electron确实好用,但安装是真的要耗费半条命。每次安装都会遇到各种问题,然后解决了之后。后面就不需要安装了,但有时候比如电脑重装…

【Rust 精进之路之第4篇-数据基石·上】标量类型:整数、浮点数、布尔与字符的精妙之处

系列&#xff1a; Rust 精进之路&#xff1a;构建可靠、高效软件的底层逻辑 作者&#xff1a; 码觉客 发布日期&#xff1a; 2025-04-20 引言&#xff1a;构成万物的“原子”——标量类型 在上一篇文章【变量观】中&#xff0c;我们深入探讨了 Rust 如何通过 let、mut、const…

消息中间件RabbitMQ:简要介绍及其Windows安装流程

一、简要介绍 定义&#xff1a;RabbitMQ 是一个开源消息中间件&#xff0c;用于实现消息队列和异步通信。 场景&#xff1a;适用于分布式系统、异步任务处理、消息解耦、负载均衡等场景。 比喻&#xff1a;RabbitMQ 就像是快递公司&#xff0c;负责在不同系统间安全快速地传递…

Docker概念详解

文章目录 一、Docker&#xff1a;容器化应用的基石1.1 环境1.2 Docker 是什么1.3 Docker镜像1.3.1 基础镜像(Base Image)1.3.2 Dockerfile1.3.3 容器镜像&#xff08;Container Image&#xff09; 1.4 Registry1.5 容器1.6 Docker VS 虚拟机 二、Docker 的架构原理2.1 C/S软件架…

linux查看及修改用户过期时间

修改用户有效期 密码到期时间 sudo chage -E 2025-12-31 username sudo chage -M 180 username sudo chage -d $(date %F) username 查询用户密码到期时间 for user in $(cat /etc/passwd |cut -d: -f1); do echo $user; chage -l $user | grep "Password expires"; …

CGAL 计算直线之间的距离(3D)

文章目录 一、简介二、实现代码三、实现效果一、简介 这里的计算思路很简单: 1、首先将两个三维直线均平移至过原点处,这里两条直线可以构成一个平面normal。 2、如果两个直线平行,那么两条直线之间的距离就转换为直线上一点到另一直线的距离。 3、如果两个直线不平行,则可…

<项目代码>YOLO小船识别<目标检测>

项目代码下载链接 YOLOv8是一种单阶段&#xff08;one-stage&#xff09;检测算法&#xff0c;它将目标检测问题转化为一个回归问题&#xff0c;能够在一次前向传播过程中同时完成目标的分类和定位任务。相较于两阶段检测算法&#xff08;如Faster R-CNN&#xff09;&#xff0…

基于RK3588+FPGA+AI YOLO全国产化的无人船目标检测系统(二)平台设计

基于项目需求确定国产 AI 平台的总体架构设计&#xff0c;完成硬件单元的选择和搭建以及开发工具链的配置工作。 4.1 国产 AI 平台总体架构 本文设计了一套灵活高效的国产 AI 平台总体架构&#xff0c;设计方法是在嵌入式平 台上使用串行总线&#xff08; Peripheral Co…

Typescript中的泛型约束extends keyof

概要 本文主要分享Typescript中泛型约束的使用方法。在开发过程中&#xff0c;通过使用该方法&#xff0c;可以在编译阶段&#xff0c;帮助我们查找到一些潜在的空值引用错误。 代码和实现 我们预先定义了IUser接口&#xff0c;接口包括了id&#xff0c;姓名&#xff0c;性别…

C++ 2025 展望:现代编程需求与新兴技术驱动下的变革

C 作为一门成熟的语言&#xff0c;在多个领域&#xff08;嵌入式系统、高性能计算、图形渲染、游戏开发等&#xff09;依旧占据重要地位。在 2024 年&#xff0c;C 开发继续在许多传统领域保持强劲的势头&#xff0c;同时也面临着新的挑战与发展方向。展望 2025 年&#xff0c;…

包管理工具有哪些?主流软件分享

常见的包管理工具主要有&#xff1a;npm、Yarn、pnpm、Composer、Maven、pip、Conda 等&#xff0c;其中 npm 是目前全球使用最广泛的JavaScript包管理工具&#xff0c;以丰富的生态、便捷的使用体验以及强大的社区支持闻名。npm具备依赖管理、版本控制、脚本执行等强大功能&am…