【用法总结】无障碍AccessibilityService

一、背景

    本文仅用于做学习总结,转换成自己的理解,方便需要时快速查阅,深入研究可以去官网了解更多:官网链接点这里
    之前对接AI语音功能时,发现有些按钮(或文本)在我没有主动注册唤醒词场景下,还是响应了点击,使用profiler跟踪调用堆栈才发现是使用了无障碍服务实现的。因为开发的是系统应用,也没必要主动去打开无障碍服务开关,于是觉得无障碍服务有很大的可发挥空间,于是借助无障碍服务,实现了一个显示当前展示的Window/Activity/Dialog的悬浮窗,用于演示无障碍服务的用法及其强大之处。

二、用法

2.1 新建一个继承AccessibilityService的无障碍服务类,并按需重写回调方法
class AccessibilityTest : AccessibilityService() {
    override fun onAccessibilityEvent(event: AccessibilityEvent?) {
        Log.d(TAG, "onAccessibilityEvent: event = $event")
    }

    override fun onServiceConnected() {
        super.onServiceConnected()
        Log.d(TAG, "onServiceConnected: ")
    }
    
    override fun onUnbind(intent: Intent?): Boolean {
        Log.d(TAG, "onUnbind: intent = $intent")
        return super.onUnbind(intent)
    }

    override fun onInterrupt() {
        Log.d(TAG, "onInterrupt: ")
    }
}
  • onServiceConnected():
        当无障碍服务打开后,有注册的交互事件发生时,如果还没有连接服务,这会先执行连接,并回调这个方法。
        问题:AccessibilityServie继承Service也就是普通的服务,没有绑定前台的notification等可见的界面,会不会在后台过一会儿就断开了,是否会导致某些时候无法捕获到交互的事件??
  • onAccessibilityEvent(event: AccessibilityEvent?):
        当有注册的交互事件,比如:点击、长按、焦点变化等触发时,会回调这个函数
    • event.getEventType(): 获取事件类型,点击:TYPE_VIEW_CLICKED,长按:TYPE_VIEW_LONG_CLICKED,窗口状态变化:TYPE_WINDOW_STATE_CHANGED,详细的可以查阅AccessibilityService类的EventType注解中有枚举出所有的事件类型。
    • event.getPackName(): 获取交互来自的包名
    • event.getClassName(): 如果是Activity/Dialog则是其类的全路径名,如果是View的话则展示当前的View全路径名。
  • onUnbind(intent: Intent?):
        断开服务时回调,用于处理一些资源释放的逻辑。
  • onInterrupt():
2.2 AndroidManifest.xml文件中注册无障碍服务

    这个步骤和普通的Service注册有些不同,需要配置permission、intent-filter和无障碍服务的xml配置文件,基本都是固定的格式,只是按需改一些配置项。

<service
    android:name=".accessibility.AccessibilityTest"
    android:exported="true"
    android:label="@string/accessibility_tip"
    android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"
    android:process=":BackgroundService">
    <intent-filter>
        <action android:name="android.accessibilityservice.AccessibilityService" />
    </intent-filter>
    <meta-data
        android:name="android.accessibilityservice"
        android:resource="@xml/accessibility_config" />
</service>
  • service节点配置的label标签会在无障碍服务中展示,比如上面的label内容是“accessibility_tip”,那么在无障碍服务中展示就会如下所示“:
    在这里插入图片描述
    在这里插入图片描述
        在/res/xml目录下,新建一个accessibility_config.xml文件,内容如下:
<?xml version="1.0" encoding="utf-8"?>
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
    android:accessibilityEventTypes="typeAllMask"
    android:accessibilityFeedbackType="feedbackGeneric"
    android:canRetrieveWindowContent="true"
    android:description="@string/accessibilty_desc" />

⚠️:如果这里指定了包名(android:packageNames的值,多个包名用英文逗号分隔。)则只会收到对应包名应用的事件。

  • android:description属性设置的就是上面的无障碍中accessibility_tip服务最下面的文案介绍。
  • android:accessibilityEventTypes:指定接收的事件类型
  • android:accessibilityFeedbackType:指定接收的反馈类型
2.3 在AccessibilityTest的onServiceConnected方法中动态设置serviceInfo
  • AccessibilityTest->onServiceConnected(): 通过在Service连接到无障碍服务的回调,调用setServiceInfo方法,可以在在运行时调整无障碍服务的配置:
override fun onServiceConnected() {
    super.onServiceConnected()
    Log.d(TAG, "onServiceConnected: ")
    val accessibilityServiceInfo = AccessibilityServiceInfo()
    accessibilityServiceInfo.eventTypes = (AccessibilityEvent.TYPE_WINDOWS_CHANGED
            or AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED
            or AccessibilityEvent.TYPE_VIEW_CLICKED
            or AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED
            or AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED)
    accessibilityServiceInfo.feedbackType = AccessibilityServiceInfo.FEEDBACK_ALL_MASK
    accessibilityServiceInfo.notificationTimeout = 0
    accessibilityServiceInfo.flags = AccessibilityServiceInfo.DEFAULT
    // 如果这里指定了包名则只会收到对应包名应用的事件
      accessibilityServiceInfo.packageNames = arrayOf("com.yanggui.animatortest")
    serviceInfo = accessibilityServiceInfo
}

    完成以上的步骤,然后编译运行安装到手机上,然后从无障碍服务中开启,就能够在AccessibilityTest的onAccessibilityEvent中收到各种交互事件了。

无障碍服务跑起来后,会打印如下log:

// 从无障碍服务中打开accessibility_tip服务回调onServiceConnected方法:
14:25:34.170  D  onServiceConnected:
// 打开当前应用会执行onAccessibilityEvent方法,回调一系列的事件信息,封装在// AccessibilityEvent中
14:26:44.792  D  onAccessibilityEvent: event = EventType: TYPE_VIEW_CLICKED; EventTime: 7897173; PackageName: com.google.android.apps.nexuslauncher; MovementGranularity: 0; Action: 0; ContentChangeTypes: []; WindowChangeTypes: [] [ ClassName: android.widget.TextView; Text: [AnimatorTest]; ContentDescription: AnimatorTest; ItemCount: -1; CurrentItemIndex: -1; Enabled: true; Password: false; Checked: false; FullScreen: false; Scrollable: false; BeforeText: null; FromIndex: -1; ToIndex: -1; ScrollX: 0; ScrollY: 0; MaxScrollX: 0; MaxScrollY: 0; ScrollDeltaX: -1; ScrollDeltaY: -1; AddedCount: -1; RemovedCount: -1; ParcelableData: null ]; recordCount: 0
14:26:44.847  D  onAccessibilityEvent: event = EventType: TYPE_WINDOW_CONTENT_CHANGED; EventTime: 7897231; PackageName: com.google.android.apps.nexuslauncher; MovementGranularity: 0; Action: 0; ContentChangeTypes: [CONTENT_CHANGE_TYPE_SUBTREE]; WindowChangeTypes: [] [ ClassName: android.widget.FrameLayout; Text: []; ContentDescription: null; ItemCount: -1; CurrentItemIndex: -1; Enabled: true; Password: false; Checked: false; FullScreen: false; Scrollable: false; BeforeText: null; FromIndex: -1; ToIndex: -1; ScrollX: 0; ScrollY: 0; MaxScrollX: 0; MaxScrollY: 0; ScrollDeltaX: -1; ScrollDeltaY: -1; AddedCount: -1; RemovedCount: -1; ParcelableData: null ]; recordCount: 0
14:26:44.891  D  onAccessibilityEvent: event = EventType: TYPE_WINDOW_STATE_CHANGED; EventTime: 7897277; PackageName: com.yanggui.animatortest; MovementGranularity: 0; Action: 0; ContentChangeTypes: []; WindowChangeTypes: [] [ ClassName: com.yanggui.animatortest.leanback.RowsSupportFragmentActivity; Text: [AnimatorTest]; ContentDescription: null; ItemCount: -1; CurrentItemIndex: -1; Enabled: true; Password: false; Checked: false; FullScreen: true; Scrollable: false; BeforeText: null; FromIndex: -1; ToIndex: -1; ScrollX: 0; ScrollY: 0; MaxScrollX: 0; MaxScrollY: 0; ScrollDeltaX: -1; ScrollDeltaY: -1; AddedCount: -1; RemovedCount: -1; ParcelableData: null ]; recordCount: 0
14:26:44.952  D  onAccessibilityEvent: event = EventType: TYPE_WINDOW_STATE_CHANGED; EventTime: 7897337; PackageName: com.yanggui.animatortest; MovementGranularity: 0; Action: 0; ContentChangeTypes: []; WindowChangeTypes: [] [ ClassName: com.yanggui.animatortest.leanback.RowsSupportFragmentActivity; Text: [AnimatorTest]; ContentDescription: null; ItemCount: -1; CurrentItemIndex: -1; Enabled: true; Password: false; Checked: false; FullScreen: true; Scrollable: false; BeforeText: null; FromIndex: -1; ToIndex: -1; ScrollX: 0; ScrollY: 0; MaxScrollX: 0; MaxScrollY: 0; ScrollDeltaX: -1; ScrollDeltaY: -1; AddedCount: -1; RemovedCount: -1; ParcelableData: null ]; recordCount: 0
// 从无障碍服务中关闭accessibility_tip服务回调onUnbind方法:
14:24:38.264  D  onUnbind: intent = Intent { cmp=com.yanggui.animatortest/.accessibility.AccessibilityTest }
2.4 附:反馈类型(feedbackType)和事件类型(eventType)的枚举值
  • 反馈类型(feedbackType):定义在AccessibilityServiceInfo中
@IntDef(flag = true, prefix = { "FEEDBACK_" }, value = {
        FEEDBACK_AUDIBLE,
        FEEDBACK_GENERIC,
        FEEDBACK_HAPTIC,
        FEEDBACK_SPOKEN,
        FEEDBACK_VISUAL,
        FEEDBACK_BRAILLE
})
@Retention(RetentionPolicy.SOURCE)
public @interface FeedbackType {}
  • 事件类型(eventType):定义在AccessibilityEvent中
@IntDef(
      flag = true,
      prefix = {"TYPE_"},
      value = {
           TYPE_VIEW_CLICKED, // 点击事件
           TYPE_VIEW_LONG_CLICKED, // 长按事件
           TYPE_VIEW_SELECTED, // view选中
           TYPE_VIEW_FOCUSED, // view上焦,使用遥控操作的需要关注该事件
           TYPE_VIEW_TEXT_CHANGED, // 表示更改 android.widget.EditText的文本的事件。
           TYPE_WINDOW_STATE_CHANGED,
           TYPE_NOTIFICATION_STATE_CHANGED,
           TYPE_VIEW_HOVER_ENTER,
           TYPE_VIEW_HOVER_EXIT,
           TYPE_TOUCH_EXPLORATION_GESTURE_START,
           TYPE_TOUCH_EXPLORATION_GESTURE_END,
           TYPE_WINDOW_CONTENT_CHANGED,
           TYPE_VIEW_SCROLLED,
           TYPE_VIEW_TEXT_SELECTION_CHANGED,
           TYPE_ANNOUNCEMENT,
           TYPE_VIEW_ACCESSIBILITY_FOCUSED,
           TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED,
           TYPE_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY,
           TYPE_GESTURE_DETECTION_START,
           TYPE_GESTURE_DETECTION_END,
           TYPE_TOUCH_INTERACTION_START,
           TYPE_TOUCH_INTERACTION_END,
           TYPE_WINDOWS_CHANGED,
           TYPE_VIEW_CONTEXT_CLICKED,
           TYPE_ASSIST_READING_CONTEXT,
           TYPE_SPEECH_STATE_CHANGE
    })
    @Retention(RetentionPolicy.SOURCE)
    public @interface EventType {}

三、实现展示当前Activity/Dialog/Window信息的悬浮窗

    如上介绍的,无障碍服务是能够获取到各种交互事件,从onAccessibilityEvent回调中可轻松拿到交互控件的packageName和className,所以基于无障碍服务能力的支持,也就很容易实现悬浮展示当前Activity的功能了。

3.1 全局悬浮窗的实现
  • 这个业务点的关键知识点是能全局悬浮,且不依赖Activity类型context,也就是不需要windowToken参数的window类型。
private const val TAG = "TopActivityEvent"
class TopActivityEventWindow {
    companion object {
        @SuppressLint("StaticFieldLeak")
        private var rootView: View? = null

        @SuppressLint("StaticFieldLeak")
        private var tvContent: TextView? = null

        @SuppressLint("StaticFieldLeak")
        private var window: TopActivityEventWindow? = null

        @SuppressLint("StaticFieldLeak")
        fun showEvent(ctx: Context, pkgName: String?, activityClassName: String?) {
            if (window == null) {
                initEventWindow(ctx)
            }
            if (!pkgName.isNullOrBlank() && !activityClassName.isNullOrBlank()) {
                tvContent?.text = "$pkgName\n$activityClassName"
            } else {
                Log.e(TAG, "showEvent: pkgName = $pkgName, activityClassName = $activityClassName")
            }
        }

        private fun initEventWindow(ctx: Context) {
            rootView =
                LayoutInflater.from(ctx).inflate(R.layout.layout_top_activity_window, null, false)
            val windowManager = ctx.getSystemService(Context.WINDOW_SERVICE) as? WindowManager
            windowManager?.apply {
                val lp = WindowManager.LayoutParams(
                    WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.WRAP_CONTENT
                )
                lp.type = if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.N) {
                    WindowManager.LayoutParams.TYPE_TOAST
                } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
                    // android31及以上 需要使用TYPE_APPLICATION_OVERLAY才能展示,使用ALERT_WINDOW会报没有权限
                    WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
                } else {
                    WindowManager.LayoutParams.TYPE_SYSTEM_ALERT
                }
                lp.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
                lp.gravity = Gravity.TOP or Gravity.LEFT
                lp.format = PixelFormat.TRANSLUCENT
                addView(rootView, lp)
            }
            tvContent = rootView?.findViewById(R.id.top_activity_window_text)
            window = TopActivityEventWindow()
        }

        fun dismiss(ctx: Context) {
            if (window != null) {
                val windowManager = ctx.getSystemService(Context.WINDOW_SERVICE) as? WindowManager
                windowManager?.removeView(rootView)
                tvContent = null
                rootView = null
                window = null
            }
        }
    }
}
3.1 在无障碍服务的onAccessibilityEvent中调用悬浮窗的展示逻辑
override fun onAccessibilityEvent(event: AccessibilityEvent?) {
        Log.d(TAG, "onAccessibilityEvent: event = $event")
        if (event?.eventType == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {
            TopActivityEventWindow.showEvent(
                this.applicationContext, "${event.packageName}", "${event.className}"
            )
        }
    }
3.2 实现的实际效果
  • 捕获pixel3a-api33模拟器的Launcher展示效果:
    在这里插入图片描述
  • 自己的demo app的主页获取效果:
    在这里插入图片描述
  • Dialog的捕获效果:
    在这里插入图片描述
  • PopupWindow获取不到待解决!!

四、总结

    以上是自定义Android的无障碍服务的基本用法。在清单文件中注册CustomAccessibilityService(在meta-datade android.accessibilityservice为key,填写配置文件xml的路径,关键是intent-filter节点中要写,service节点要写permission),然后重写onAccessiblityEvent()方法拿到交互事件、packageName、className,最后只要在系统设置-无障碍打开,就能很轻松实现一个自己的无障碍服务。这块定义注册Service的套路是固定的,核心是理解不同属性的作用,比如配置接收哪些事件类型和反馈类型,指定包名的方式等,其他的步骤不用纠结,直接在需要时照猫画虎就好了,不必花太多时间研究基础用法了。
    但是无障碍服务支持的能力还远不止于此,还能实现很多丰富的功能,比如:触发指定控件的点击,从而配合语音识别实现点击、跳转等业务逻辑,需要我们进一步阅读官方文档进行学习实践总结。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【JAVA】在 Queue 中 poll()和 remove()有什么区别

&#x1f34e;个人博客&#xff1a;个人主页 &#x1f3c6;个人专栏&#xff1a;JAVA ⛳️ 功不唐捐&#xff0c;玉汝于成 目录 前言 正文 poll() 方法&#xff1a; remove() 方法&#xff1a; 区别总结&#xff1a; 结语 我的其他博客 前言 在Java的Queue接口中&…

计算机找不到msvcp120.dll如何解决?总结五个可靠的教程

在计算机使用过程中&#xff0c;遇到“找不到msvcp120.dll”这一问题常常令人困扰。msvcp120.dll作为Windows系统中至关重要的动态链接库文件&#xff0c;对于许多应用程序的正常运行起着不可或缺的作用。那么&#xff0c;究竟是什么原因导致找不到msvcp120.dll呢&#xff1f;又…

Multi-View-Information-Bottleneck

encoder p θ ( z 1 ∣ v 1 ) _θ(z_1|v_1) θ​(z1​∣v1​)&#xff0c;D S K L _{SKL} SKL​ represents the symmetrized KL divergence. I ˆ ξ ( z 1 ; z 2 ) \^I_ξ(z_1; z_2) Iˆξ​(z1​;z2​) refers to the sample-based parametric mutual information estimatio…

vim升级和配置

vim升级和配置 1、背景2、环境说明3、操作3.1 升级VIM3.2 配置VIM3.2.1、编辑vimrc文件3.2.2、安装插件 1、背景 日常工作跟linux系统打交道比较多&#xff0c;目前主要用到的是Cenots7和Ubuntu18这两个版本的linux系统&#xff0c;其中Centos7主要是服务器端&#xff0c;Ubun…

如何开启文件共享及其他设备如何获取

1.场景分析 日常生活中&#xff0c;常常会遇到多台电脑共同办公文件却不能共享的问题&#xff0c;频繁的用移动硬盘、U盘等拷贝很是繁琐&#xff0c;鉴于此&#xff0c;可以在同一内网环境下设置共享文件夹&#xff0c;减少不必要的文件拷贝工作&#xff0c;提升工作效率。废话…

Maven《一》-- 一文带你快速了解Maven

目录 &#x1f436;1.1 为什么使用Maven 1. Mavan是一个依赖管理工具 ①jar包的规模 ②jar包的来源问题 ③jar包的导入问题 ④jar包之间的依赖 2. Mavan是一个构建工具 ①你没有注意过的构建 ②脱离IDE环境仍需构建 3. 结论 &#x1f436;1.2 什么是Maven &#x…

C语言数据结构(1)复杂度(大o阶)

欢迎来到博主的专栏——C语言与数据结构 博主ID——代码小豪 文章目录 如何判断代码的好坏时间复杂度什么是时间复杂度如何计算时间复杂度 空间复杂度 如何判断代码的好坏 实现相同作用的不同代码&#xff0c;如何分辨这些代码的优劣之处呢&#xff1f; 有人说了&#xff0c…

stm32学习笔记:USART串口通信

1、串口通信协议&#xff08;简介软硬件规则&#xff09; 全双工&#xff1a;打电话。半双工&#xff1a;对讲机。单工&#xff1a;广播 时钟&#xff1a;I2C和SPI有单独的时钟线&#xff0c;所以它们是同步的&#xff0c;接收方可以在时钟信号的指引下进行采样。串口、CAN和…

行业内参~移动广告行业大盘趋势-2023年12月

前言 2024年&#xff0c;移动广告的钱越来越难赚了。市场竞争激烈到前所未有的程度&#xff0c;小型企业和独立开发者在巨头的阴影下苦苦挣扎。随着广告成本的上升和点击率的下降&#xff0c;许多原本依赖广告收入的创业者和自由职业者开始感受到前所未有的压力。 &#x1f3…

IPv6组播--SSM Mapping

概念 SSM(Source-Specific Multicast)称为指定源组播,要求路由器能了解成员主机加入组播组时所指定的组播源。 如果成员主机上运行MLDv2,可以在MLDv2报告报文中直接指定组播源地址。但是某些情况下,成员主机只能运行MLDv1,为了使其也能够使用SSM服务,组播路由器上需要提…

swing快速入门(四十)JList、JComboBox实现列表框

注释很详细&#xff0c;直接上代码 上一篇 新增内容 &#x1f9e7;1.列表的属性设置与选项监听器 &#x1f9e7;2.下拉框的属性设置与选项监听器 &#x1f9e7;3.Box中组件填充情况不符合预期的处理方法 &#x1f9e7;4.LIst向Vector的转化方法 源码&#xff1a; package swing…

操作系统期末考复盘

简答题4题*5 20分计算题2题*5 10分综合应用2题*10 20分程序填空1题10 10分 1、简答题&#xff08;8抽4&#xff09; 1、在计算机系统上配置OS的目标是什么&#xff1f;作用主要表现在哪个方面&#xff1f; 在计算机系统上配置OS&#xff0c;主要目标是实现:方便性、…

操作系统复习 七、八章

操作系统复习 七、八章 文章目录 操作系统复习 七、八章第七章 内存管理内存管理的基本要求和原理覆盖与交换连续分配管理方式非连续分配管理方式基本分段存储管理方式段页式管理方式补充 第八章 虚拟内存虚拟内存的基本概念请求分页管理方式易混知识点页面置换算法页面分配策略…

memory泄露分析方法(java篇)

#memory泄露主要分为java和native 2种&#xff0c;本文主要介绍java# 测试每天从monkey中筛选出内存超标的app&#xff0c;提单流转到我 首先&#xff0c;辨别内存泄露类型&#xff08;java&#xff0c;还是native&#xff09; 从采到的dumpsys_meminfo_pid看java heap&…

案例:新闻数据加载

文章目录 介绍相关概念相关权限约束与限制完整示例 代码结构解读构建主界面数据请求下拉刷新总结 介绍 本篇Codelab是基于ArkTS的声明式开发范式实现的样例&#xff0c;主要介绍了数据请求和touch事件的使用。包含以下功能&#xff1a; 数据请求。列表下拉刷新。列表上拉加载…

Redis高并发高可用(主从复制、哨兵)

复制 在分布式系统中为了解决单点问题,通常会把数据复制多个副本部署到其他机器,满足故障恢复和负载均衡等需求。Redis也是如此,它为我们提供了复制功能,实现了相同数据的多个Redis 副本。复制功能是高可用Redis的基础,哨兵和集群都是在复制的基础上实现高可用的。 默认…

C++11 异常

C异常概念 在C中&#xff0c;异常是程序在执行过程中遇到意外情况或错误时抛出的一种机制。当程序运行过程中发生异常&#xff0c;可以通过抛出异常、捕获异常和处理异常来改变程序的正常执行流程。 抛出异常&#xff1a;在程序中&#xff0c;可以使用关键字 throw 来抛出异常…

Android基于Matrix绘制PaintDrawable设置BitmapShader,以手指触点为中心显示原图像圆图,Kotlin(3)

Android基于Matrix绘制PaintDrawable设置BitmapShader&#xff0c;以手指触点为中心显示原图像圆图&#xff0c;Kotlin&#xff08;3&#xff09; 在 Android基于Matrix绘制PaintDrawable设置BitmapShader&#xff0c;以手指触点为中心显示原图像圆图&#xff0c;Kotlin&#…

【小白专用】C# 连接 MySQL 数据库

C# – Mysql 数据库连接 1. 配置环境 #前提&#xff1a;电脑已安装Mysql服务&#xff1b; Visual Studio 安装Mysql依赖库&#xff1a; 工具 -> NuGet 包管理器 -> 管理解决方案的 NuGet程序包 —> 搜索&#xff0c; 安装Mysql.Data (Oracle); (安装成功后&…

libcurl开源库的编译与使用全攻略

libcurl简介 libcurl 是一个广泛使用的、支持多种协议的、开源的客户端URL传输库&#xff0c;提供了许多用于数据传输的API&#xff0c;例如文件传输、FTP、HTTP、HTTPS、SMTP等。libcurl 的主要特点包括 支持多种协议&#xff1a;libcurl 支持多种协议&#xff0c;如 HTTP、F…
最新文章