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

news2025/5/11 12:07:09

最近在做一个基于无障碍自动刷短视频的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) {
			//关联 IAccessibilityServiceConnection
			AccessibilityInteractionClient.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 {
	    //获取前面关联的缓存在线程中的IAccessibilityServiceConnection
              IAccessibilityServiceConnection 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 {
	...
	// 解析当前的windowId
	resolvedWindowId = 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 AccessibilityInteractionConnectionManager
	implements 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);
        }

	...
	@Override
        public 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();
	//首先找出包含检索字符串的view
	root.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.coloradmin.cn/o/2340269.html

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

相关文章

搭建用友U9Cloud ERP及UAP IDE环境

应用环境 Microsoft Windows 10.0.19045.5487 x64 专业工作站版 22H2Internet Information Services - 10.0.19041.4522Microsoft SQL Server 2019 - 15.0.2130.3 (X64)Microsoft SQL Server Reporing Services 2019 - 15.0.9218.715SQL Server Management Studio -18.6 laster…

多模态大语言模型arxiv论文略读(二十九)

Temporal Insight Enhancement: Mitigating Temporal Hallucination in Multimodal Large Language Models ➡️ 论文标题&#xff1a;Temporal Insight Enhancement: Mitigating Temporal Hallucination in Multimodal Large Language Models ➡️ 论文作者&#xff1a;Li Su…

卷积神经网络(CNN)详解

文章目录 引言1.卷积神经网络&#xff08;CNN&#xff09;的诞生背景2.卷积神经网络&#xff08;CNN&#xff09;介绍2.1 什么是卷积神经网络&#xff1f;2.2 卷积神经网络&#xff08;CNN&#xff09;的基本特征2.2.1 局部感知&#xff08;Local Connectivity&#xff09;2.2.…

【SF顺丰】顺丰开放平台API对接(注册、API测试篇)

1.注册开发者账号 注册地址&#xff1a;顺丰企业账户中心 2.登录开发平台 登录地址&#xff1a;顺丰开放平台 3.开发者对接 点击开发者对接 4.创建开发对接应用 开发者应用中“新建应用”创建应用&#xff0c;最多创建应用限制数量5个 注意&#xff1a;需要先复制保存生产校验…

VisualSVN过期后的解决方法

作为一款不错的源代码管理软件&#xff0c;svn还是有很多公司使用的。在vs中使用svn&#xff0c;大家一般用的都是VisualSVN插件。在30天试用期过后&#xff0c;它就不能被免费使用了。下面给大家讲如何免费延长过期时间&#xff08;自定义天数&#xff0c;可以设定一个很大的值…

DeepSeek智能时空数据分析(二):3秒对话式搞定“等时圈”绘制

序言&#xff1a;时空数据分析很有用&#xff0c;但是GIS/时空数据库技术门槛太高 时空数据分析在优化业务运营中至关重要&#xff0c;然而&#xff0c;三大挑战仍制约其发展&#xff1a;技术门槛高&#xff0c;需融合GIS理论、SQL开发与时空数据库等多领域知识&#xff1b;空…

STM32学习2

一、OLED 1.1 OLED介绍 OLED&#xff08;Organic Light Emitting Diode&#xff09;&#xff1a;有机发光二极管 OLED显示屏&#xff1a;性能优异的新型显示屏&#xff0c;具有功耗低、相应速度快、宽视角、轻薄柔韧等特点 0.96寸OLED模块&#xff1a;小巧玲珑、占用接口少…

LabVIEW液压系统远程监控与故障诊断

开发了一种基于LabVIEW的远程液压系统监控解决方案&#xff0c;通过先进的数据采集与分析技术&#xff0c;有效提升工程机械的运作效率和故障响应速度。该系统结合现场硬件设备和远程监控软件&#xff0c;实现了液压系统状态的实时检测和故障诊断&#xff0c;极大地提升了维护效…

Idea中实用设置和插件

目录 一、Idea使用插件 1.Fitten Code智能提示 2.MyBatisCodeHelperPro 3.HighlightBracketPair‌ 4.Rainbow Brackets Lite 5.GitToolBox(存在付费) 6.MavenHelperPro 7.Search In Repository 8.VisualGC(存在付费) 9.vo2dto 10.Key Promoter X 11.CodeGlance…

Java写数据结构:栈

1.概念&#xff1a; 一种特殊的线性表&#xff0c;其只允许在固定的一端进行插入和删除元素操作。进行数据插入和删除操作的一端称为栈顶&#xff0c;另一端称为栈底。栈中的数据元素遵守后进先出LIFO&#xff08;Last In First Out&#xff09;的原则。 压栈&#xff1a;栈的插…

机器学习-08-推荐算法-案例

总结 本系列是机器学习课程的系列课程&#xff0c;主要介绍机器学习中关联规则 参考 机器学习&#xff08;三&#xff09;&#xff1a;Apriori算法&#xff08;算法精讲&#xff09; Apriori 算法 理论 重点 MovieLens:一个常用的电影推荐系统领域的数据集 23张图&#x…

LLM中的N-Gram、TF-IDF和Word embedding

文章目录 1. N-Gram和TF-IDF&#xff1a;通俗易懂的解析1.1 N-Gram&#xff1a;让AI学会"猜词"的技术1.1.1 基本概念1.1.2 工作原理1.1.3 常见类型1.1.4 应用场景1.1.5 优缺点 1.2 TF-IDF&#xff1a;衡量词语重要性的尺子1.2.1 基本概念1.2.2 计算公式1.2.3 为什么需…

Office文件内容提取 | 获取Word文件内容 |Javascript提取PDF文字内容 |PPT文档文字内容提取

关于Office系列文件文字内容的提取 本文主要通过接口的方式获取Office文件和PDF、OFD文件的文字内容。适用于需要获取Word、OFD、PDF、PPT等文件内容的提取实现。例如在线文字统计以及论文文字内容的提取。 一、提取Word及WPS文档的文字内容。 支持以下文件格式&#xff1a; …

HXBC编译相关错误

0、Keil MDK报错:Browse information of one or more files is not available----解决方法: 1、使用cubemax生成的工程中,某些引脚自定义了的,是在main.h中,要记得移植。 注意:cubemax生成的spi.c后,在移植的时候,注意hal_driver下面要对应增加hal_stm32H7xxxspi.c …

运维概述(linux 系统)

1、运维的基本概念 2、企业的运行模式 3、计算机硬件 运维概述 运维岗位的定义 在技术人员&#xff08;写代码的&#xff09;之间&#xff0c;一致对运维有一个开玩笑的认知&#xff1a;运维就是修电脑的、装网线的、背锅的岗位。 IT运维管理是指为了保障企业IT系统及网络…

C语言 数据结构 【堆】动态模拟实现,堆排序,TOP-K问题

引言 堆的各个接口的实现&#xff08;以代码注释为主&#xff09;&#xff0c;实现堆排序&#xff0c;解决经典问题&#xff1a;TOP-K问题 一、堆的概念与结构 堆 具有以下性质 • 堆中某个结点的值总是不大于或不小于其父结点的值&#xff1b; • 堆总是一棵完全二叉树。 二…

模型加载常见问题

safetensors_rust.SafetensorError: Error while deserializing header: HeaderTooLarge 问题代码&#xff1a; model AutoModelForVision2Seq.from_pretrained( "/data-nvme/yang/Qwen2.5-VL-32B-Instruct", trust_remote_codeTrue, torch_dtypetorc…

PyTorch 深度学习实战(37):分布式训练(DP/DDP/Deepspeed)实战

在上一篇文章中&#xff0c;我们探讨了混合精度训练与梯度缩放技术。本文将深入介绍分布式训练的三种主流方法&#xff1a;Data Parallel (DP)、Distributed Data Parallel (DDP) 和 DeepSpeed&#xff0c;帮助您掌握大规模模型训练的关键技术。我们将使用PyTorch在CIFAR-10分类…

微信小程序通过mqtt控制esp32

目录 1.注册巴法云 2.设备连接mqtt 3.微信小程序 备注 本文esp32用的是MicroPython固件&#xff0c;MQTT服务用的是巴法云。 本文参考巴法云官方教程&#xff1a;https://bemfa.blog.csdn.net/article/details/115282152 1.注册巴法云 注册登陆并新建一个topic&#xff…

1.Vue3 - 创建Vue3工程

目录 一、 基于vue-cli 脚手架二、基于vite 推荐2.1 介绍2.2 创建项目2.3 文件介绍2.3.1 extensions.json2.3.2 脚手架的根目录2.3.3 主要文件 src2.3.3.1 main.js2.3.3.2 App.vue 组件2.3.3.3 conponents 2.3.4 env.d.ts2.3.5 index.html 入口文件2.3.6 package2.3.7 tsconfig…