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.pswp.cn/news/902347.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/902347.shtml
英文地址,請注明出處:http://en.pswp.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…