【Android】问deepseek存储访问
这些天deepseek爆火,我们来问问android问题看看,如果问android中的应用怎么访问外部存储,回答的很清楚,但是如果问的深入一些,比如Android中是怎么控制让应用不能读取其他应用的外部存储文件的,回答的比较抽象,只是给出了大概的分析方向。但还是可以从中受到启发。
android中的应用怎么访问外部存储
好的,用户问的是“android中的应用怎么访问外部存储”。看起来他们可能已经对Android存储访问有一些了解,但需要更详细的步骤或最新版本的适配方法。首先,我需要回忆一下Android的存储权限变化,特别是从Android 10引入的作用域存储(Scoped Storage)带来的影响。用户可能已经知道早期版本中使用Environment.getExternalStorageDirectory()的方法,但现在需要适配新版本。
接下来,用户可能想知道如何正确地在不同版本中访问外部存储,包括如何请求权限、使用MediaStore访问媒体文件,以及如何处理非媒体文件。他们可能遇到了在Android 10及以上版本中无法直接通过文件路径访问的问题,所以需要指导如何使用Storage Access Framework(SAF)或者申请MANAGE_EXTERNAL_STORAGE权限。
另外,用户可能对如何兼容旧版本和新版本感到困惑,比如使用requestLegacyExternalStorage标记。需要解释这个标记的作用和适用条件,以及何时应该放弃使用它,转而遵循作用域存储的规范。
还需要考虑用户是否有特殊需求,比如访问所有文件,这时候需要提到MANAGE_EXTERNAL_STORAGE权限,但也要提醒他们Google Play对使用此权限的限制,避免滥用。此外,用户可能关心如何安全地处理文件描述符,避免资源泄漏,尤其是在跨进程传递文件描述符时。
最后,用户可能需要示例代码来具体实现各个步骤,包括动态权限请求、使用MediaStore查询文件、通过SAF选择文件等。要确保示例代码简洁明了,覆盖不同场景,同时指出注意事项和常见问题,比如权限拒绝处理、文件路径访问限制等。
在 Android 中访问外部存储(如 SD 卡或共享存储空间)需要遵循系统的权限规则和存储访问策略,尤其是从 Android 10(API 29) 开始引入的 作用域存储(Scoped Storage)。以下是详细的访问方法及适配不同 Android 版本的步骤:
1. 动态请求存储权限
根据 Android 版本请求对应的存储权限:
权限声明(AndroidManifest.xml)
xml
<!-- Android 13+ 媒体文件分类型权限 --> <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" /> <uses-permission android:name="android.permission.READ_MEDIA_VIDEO" /> <uses-permission android:name="android.permission.READ_MEDIA_AUDIO" /><!-- Android 10-12 通用媒体权限 --> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /><!-- 全文件访问权限(需用户手动授权) --> <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
动态请求权限代码
kotlin
private fun checkStoragePermission() {val requiredPermissions = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {// Android 13+ 分类型媒体权限arrayOf(Manifest.permission.READ_MEDIA_IMAGES,Manifest.permission.READ_MEDIA_VIDEO)} else {arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE)}if (requiredPermissions.all { permission ->ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED}) {loadExternalFiles()} else {ActivityCompat.requestPermissions(this, requiredPermissions, REQUEST_CODE_STORAGE)} }override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {super.onRequestPermissionsResult(requestCode, permissions, grantResults)if (requestCode == REQUEST_CODE_STORAGE && grantResults.all { it == PackageManager.PERMISSION_GRANTED }) {loadExternalFiles()} }
2. 访问应用的私有目录
每个应用在外部存储中拥有独立的私有目录,路径为 Android/data/<包名>/
或 Android/obb/<包名>/
。
代码示例
kotlin
复制
// 获取私有目录(如 Downloads、Pictures 等子目录) val privateDir = getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)// 创建或访问文件 val file = File(privateDir, "test.txt") file.writeText("Hello World!")// 卸载应用时,系统会自动删除此目录
3. 访问公共媒体文件(图片、视频、音频)
使用 MediaStore
API 访问公共媒体文件,这是 Android 10+ 的推荐方式。
查询图片文件示例
kotlin
复制
val projection = arrayOf(MediaStore.Images.Media._ID,MediaStore.Images.Media.DISPLAY_NAME,MediaStore.Images.Media.DATE_ADDED )val sortOrder = "${MediaStore.Images.Media.DATE_ADDED} DESC"contentResolver.query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,projection,null,null,sortOrder )?.use { cursor ->while (cursor.moveToNext()) {val id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID))val uri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id)// 通过 Uri 读取文件val inputStream = contentResolver.openInputStream(uri)// 处理文件流...} }
4. 访问非媒体文件(文档、下载目录等)
使用 Storage Access Framework (SAF) 让用户主动选择文件或目录。
代码示例
kotlin
复制
// 打开文档选择器 val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {addCategory(Intent.CATEGORY_OPENABLE)type = "*/*" // 允许选择任意类型文件 } startActivityForResult(intent, REQUEST_CODE_OPEN_DOCUMENT)// 处理返回的 Uri override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {if (requestCode == REQUEST_CODE_OPEN_DOCUMENT && resultCode == RESULT_OK) {data?.data?.let { uri ->// 通过 Uri 读取文件contentResolver.openInputStream(uri)?.use { stream ->// 读取文件内容...}}} }
5. 访问所有文件(需特殊权限)
从 Android 11(API 30) 开始,需申请 MANAGE_EXTERNAL_STORAGE
权限并跳转系统设置。
代码示例
kotlin
复制
// 检查是否已授权 if (Environment.isExternalStorageManager()) {// 已授权,可访问所有文件val files = File("/sdcard").listFiles() } else {// 跳转到系统设置页val intent = Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION)startActivity(intent) }
6. 兼容旧版本(Android 9 及以下)
若需临时禁用作用域存储,可在 AndroidManifest.xml
中添加 requestLegacyExternalStorage
。
xml
复制
<applicationandroid:requestLegacyExternalStorage="true"...> </application>
运行 HTML
注意:
-
仅当
targetSdkVersion < 30
时有效。 -
Google Play 可能对使用此标记的应用进行审核。
关键限制
-
禁止直接路径访问:
Android 10+ 禁止通过File
类直接访问非媒体文件(如/sdcard/Download/file.txt
)。 -
其他应用私有目录不可见:
/sdcard/Android/data/<其他应用包名>
对其他应用完全隔离。
总结
场景 | 方法 |
---|---|
应用私有文件 | getExternalFilesDir() |
公共媒体文件 | MediaStore API + 动态权限 |
非媒体文件 | Storage Access Framework (SAF) |
全文件访问 | MANAGE_EXTERNAL_STORAGE 权限(需用户手动授权) |
旧版本兼容 | requestLegacyExternalStorage (仅限 targetSdkVersion < 30 ) |
开发者应优先使用 MediaStore
和 SAF
,避免依赖绝对路径,以确保应用兼容未来 Android 版本。
问:
Android中是怎么控制让应用不能读取其他应用的外部存储文件的
嗯,用户之前问过几次关于Android应用如何访问外部存储的问题,现在他们又问Android是如何控制应用不能读取其他应用的外部存储文件的。看起来用户对Android的存储权限机制和隔离措施很感兴趣,可能是开发者或者安全研究人员,想深入了解系统底层的安全机制。
首先,我需要回顾之前的对话历史,用户已经了解了作用域存储、权限模型、文件路径隔离等内容。用户现在可能想知道更具体的技术实现,比如系统是如何在底层限制应用访问其他应用的数据的。
接下来,我需要考虑如何详细解释这些控制机制。可能需要从Linux文件权限、进程隔离、SELinux策略、虚拟文件系统等方面展开。同时,用户可能对具体的技术细节感兴趣,比如UID/GID如何分配,文件描述符的传递机制,以及Binder在跨进程通信中的作用。
用户之前的问题中提到了HIDL和文件描述符的传递,这可能意味着他们对底层IPC机制有一定了解。所以在回答时,可以适当深入这些方面,但也要保持清晰易懂,避免过于晦涩。
另外,用户可能想知道这些机制在不同Android版本中的差异,比如Android 10之后的作用域存储如何强化了这些限制。需要分版本说明,特别是Android 10和11的变化。
还要注意用户可能的误区,比如认为拥有存储权限就可以访问所有文件,但实际上作用域存储限制了这一点。需要澄清权限和路径访问的关系,以及系统如何通过组合权限检查和文件系统隔离来实现控制。
最后,确保回答结构清晰,分点说明,用技术术语但不过于复杂,同时举例子帮助理解。比如,通过示例说明应用A尝试访问应用B的私有目录时,系统如何拦截并阻止。
在 Android 中,系统通过 多层级的安全机制 实现应用之间外部存储文件的隔离,确保一个应用无法随意读取其他应用的外部存储文件。这些机制结合了 Linux 文件系统权限、沙盒隔离、权限模型和虚拟化技术,以下是具体实现原理:
1. Linux 文件系统权限
Android 基于 Linux 内核,继承其文件系统的权限控制:
-
UID/GID 隔离:
-
每个应用在安装时会被分配一个唯一的 用户 ID(UID) 和 组 ID(GID)。
-
应用私有目录(如
/sdcard/Android/data/<包名>
)的权限默认设置为0700
(仅允许所属 UID 读写)。 -
其他应用因 UID 不同,无法直接通过文件路径访问这些目录。
-
-
权限位控制:
-
公共目录(如
/sdcard/DCIM
)的权限通常为0777
(所有用户可读写),但通过 作用域存储 限制应用的实际访问能力。
-
2. 作用域存储(Scoped Storage,Android 10+)
从 Android 10 开始引入,进一步限制应用对外部存储的访问:
-
虚拟文件系统(FUSE):
-
Android 使用 FUSE(用户态文件系统) 或 Samsung RFS 对
/sdcard
路径进行虚拟化。 -
应用看到的
/sdcard
是一个虚拟视图,仅包含:-
自身私有目录(
Android/data/<包名>
)。 -
通过
MediaStore
授权的公共媒体文件(图片、视频、音频)。 -
用户通过 Storage Access Framework (SAF) 主动授权的文件。
-
-
-
路径过滤:
-
当应用尝试访问
/sdcard/Android/data/<其他应用包名>
时,虚拟文件系统直接返回 文件不存在(ENOENT)。 -
即使应用拥有
READ_EXTERNAL_STORAGE
权限,也无法绕过此限制。
-
3. 权限模型(Runtime Permissions)
-
存储权限分级:
-
READ_EXTERNAL_STORAGE
:允许读取公共媒体文件(Android 13 后细化为READ_MEDIA_IMAGES
、READ_MEDIA_VIDEO
等)。 -
WRITE_EXTERNAL_STORAGE
:允许写入公共媒体文件(Android 10 后逐渐废弃)。 -
MANAGE_EXTERNAL_STORAGE
:允许访问所有文件(需用户手动授权,Google Play 审核严格)。
-
-
动态权限检查:
-
应用在访问公共目录时,系统会在 VFS(虚拟文件系统)层 隐式检查权限状态。
-
若未授权,即使通过
File
类尝试访问文件,也会返回 权限错误(SecurityException)。
-
4. 进程隔离与 SELinux
-
沙盒机制:
-
每个应用运行在独立的进程空间,拥有自己的 UID 和 SELinux 上下文。
-
SELinux 策略严格限制应用进程只能访问其所属的文件和目录。
-
-
Binder IPC 限制:
-
跨进程文件访问需通过 Binder 传递文件描述符(
ParcelFileDescriptor
),系统会在 IPC 过程中检查权限和所有权。
-
5. 合法访问途径
若应用需共享文件,必须通过以下 显式授权机制:
-
ContentProvider:
-
应用 A 实现
ContentProvider
,通过 URI(如content://com.appA.provider/file1
)共享文件。 -
应用 B 需通过
ContentResolver
查询 URI,并拥有对应的权限(如android:readPermission
)。
-
-
FileProvider:
-
生成临时 URI(如
content://com.appA.fileprovider/files/file1
),通过 Intent 传递给其他应用。 -
临时 URI 可设置有效期和权限,接收方应用需通过
ContentResolver
访问。
-
-
Storage Access Framework (SAF):
-
用户通过系统文件选择器(如
Intent.ACTION_OPEN_DOCUMENT
)主动授权访问特定文件或目录。
-
6. 示例:阻止访问的完整流程
假设 应用 A 尝试读取 应用 B 的私有文件 /sdcard/Android/data/com.appB/files/secret.txt
:
-
路径解析:
-
应用 A 调用
new File("/sdcard/Android/data/com.appB/files/secret.txt")
。
-
-
虚拟文件系统拦截:
-
FUSE 发现路径属于其他应用的私有目录,直接返回 文件不存在(即使文件实际存在)。
-
-
权限检查:
-
若应用 A 尝试绕过虚拟路径(如直接操作
/data/media/...
),SELinux 会阻止访问。
-
-
进程隔离:
-
即使应用 A 获取了
MANAGE_EXTERNAL_STORAGE
权限,系统仍会限制其访问其他应用的私有目录。
-
7. 绕过限制的例外情况
-
ROOT 设备:
-
拥有 Root 权限的应用可绕过所有限制(但普通用户设备极少开放 Root)。
-
-
系统应用:
-
系统签名应用(如预装文件管理器)可能拥有更高权限,但需通过 Google 认证。
-
总结
Android 通过以下组合机制实现应用间外部存储的隔离:
机制 | 作用 |
---|---|
Linux 文件权限 | 基于 UID/GID 的目录隔离 |
作用域存储(FUSE) | 虚拟化文件路径,隐藏其他应用的私有目录 |
运行时权限模型 | 动态检查存储权限,限制公共目录访问 |
SELinux 策略 | 强制进程隔离,禁止跨应用文件操作 |
安全的文件共享 API | 强制通过 ContentProvider 、FileProvider 或 SAF 显式授权 |
开发者应始终使用系统提供的 API(如 MediaStore
、SAF
)访问文件,避免直接操作路径,以确保兼容性和安全性。
相关文章:

【Android】问deepseek存储访问
这些天deepseek爆火,我们来问问android问题看看,如果问android中的应用怎么访问外部存储,回答的很清楚,但是如果问的深入一些,比如Android中是怎么控制让应用不能读取其他应用的外部存储文件的,回答的比较抽…...

Android记事本App设计开发项目实战教程2025最新版Android Studio
平时上课录了个视频,从新建工程到打包Apk,从头做到尾,没有遗漏任何实现细节,欢迎学过Android基础的同学参加,如果你做过其他终端软件开发,也可以学习,快速上手Android基础开发。 Android记事本课…...

python学习——函数的返回值
在 Python 中,函数的返回值决定了调用该函数后得到的结果。默认情况下,如果函数没有使用 return 语句或没有明确返回一个值,函数将返回 None。为了实现更复杂的逻辑,可以通过 return 语句返回多个值、错误信息或其他数据类型。 返…...

【竞技宝】裂变天地S1:BB0-2PARI淘汰出局
北京时间2月1日,DOTA2裂变天地S1继续进行,昨日共进行三场比赛,第三场比赛迎来败者组第二轮PARI对阵BB。以下是本场比赛的详细战报。 第一局: 首局比赛,BB在天辉方,PARI在夜魇方。阵容方面,BB点出了圣堂、卡尔、玛尔斯、奶绿、亚巴顿,PARI则是拿到小娜迦、凤凰、大圣、玛西、萨…...

数据分析系列--⑨RapidMiner训练集、测试集、验证集划分
一、数据集获取 二、划分数据集 1.导入和加载数据 2.数据集划分 2.1 划分说明 2.2 方法一 2.3 方法二 一、数据集获取 点击下载数据集 此数据集包含538312条数据. 二、划分数据集 1.导入和加载数据 2.数据集划分 2.1 划分说明 2.2 方法一 使用Filter Example Range算子. …...

实践Rust:编写一个猜数字游戏
如果你正在学习Rust,并且想通过一个有趣的小项目来巩固所学知识,那么“猜数字游戏”是一个绝佳的选择!这个游戏的逻辑非常简单:程序会随机生成一个数字,玩家需要猜测这个数字是多少,程序会告诉玩家猜大了还…...

JavaFX - 3D 形状
在前面的章节中,我们已经了解了如何在 JavaFX 应用程序中的 XY 平面上绘制 2D 形状。除了这些 2D 形状之外,我们还可以使用 JavaFX 绘制其他几个 3D 形状。 通常,3D 形状是可以在 XYZ 平面上绘制的几何图形。它们由两个或多个维度定义&#…...

阿里新发的大模型Qwen2.5-max如何?
阿里新发布的大模型Qwen2.5-Max是一款性能卓越、技术先进的大型语言模型,其在多个方面展现了突出的表现。以下是基于我搜索到的资料对Qwen2.5-Max的详细评价: 技术特点 超大规模预训练数据:Qwen2.5-Max采用了超过20万亿tokens的超大规模预训…...

文本复制兼容方案最佳实现落地。
文章目录 一、navigator.clipboard.writeText二、方案落地总结 一、navigator.clipboard.writeText navigator.clipboard.writeText 是一个Web API,它允许网页脚本将文本数据写入用户的系统剪贴板。这个API是异步的,并且设计用于提高安全性和用户体验&a…...

x86-64数据传输指令
关于汇编语言一些基础概念的更详细的介绍,可移步MIPS指令集(一)基本操作_mips指令 sw-CSDN博客 该指令集中一个字2字节。 该架构有16个64位寄存器,名字都以%r开头,每个寄存器的最低位字节,低1~2位字节&…...

LigerUI在MVC模式下的响应原则
LigerUI是基于jQuery的UI框架,故他也是遵守jQuery的开发模式,但是也具有其特色的侦听函数,那么当LigerUI作为View层的时候,他所发送后端的必然是表单的数据,在此我们以俩个div为例: {Layout "~/View…...

java CountDownLatch和CyclicBarrier
专栏系列文章地址:https://blog.csdn.net/qq_26437925/article/details/145290162 本文目标: 理解CountDownLatch和CyclicBarrier的使用,主要是复习Aqs 另外工作中用到CountDownLatch的地方还很多,一般是完成某些事情才能继续某…...

力扣动态规划-17【算法学习day.111】
前言 ###我做这类文章一个重要的目的还是给正在学习的大家提供方向(例如想要掌握基础用法,该刷哪些题?建议灵神的题单和代码随想录)和记录自己的学习过程,我的解析也不会做的非常详细,只会提供思路和一些关…...

读书笔记-《你的灯亮着吗?》
这是一本短篇且有趣的书,通过一些小故事讨论了下解决问题时会遇到的困难。 01 为什么难以定义问题 常言道,办法总比问题多,但往往我们在一开始就没有定义好问题: 在学校时,校园教育使得我们会紧紧抓住看起来像是“问题…...

MATLAB实现多种群遗传算法
多种群遗传算法(MPGA, Multi-Population Genetic Algorithm)是一种改进的遗传算法,它通过将种群分成多个子种群并在不同的子种群之间进行交叉和交换,旨在提高全局搜索能力并避免早期收敛。下面是多种群遗传算法的主要步骤和流程&a…...

tf.Keras (tf-1.15)使用记录3-model.compile方法
model.compile 是 TensorFlow Keras 中用于配置训练模型的方法。在开始训练之前,需要通过这个方法来指定模型的优化器、损失函数和评估指标等。 注意事项: 在开始训练(调用 model.fit)之前,必须先调用 model.compile()。 1 基本…...

Prometheus 中的 Exporter
在 Prometheus 生态系统中,Exporter 扮演着至关重要的角色,它们负责从不同的服务或系统中收集和暴露度量数据。本文将详细介绍 Exporter 的概念、类型以及如何有效使用它们将 Prometheus 集成到各种系统中进行监控。 什么是 Exporter? Exporter 是一段软件,它从应用程序或…...

网工_HDLC协议
2025.01.25:网工老姜学习笔记 第9节 HDLC协议 9.1 HDLC高级数据链路控制9.2 HDLC帧格式(*控制字段)9.2.1 信息帧(承载用户数据,0开头)9.2.2 监督帧(帮助信息可靠传输,10开头…...

leetcode 2563. 统计公平数对的数目
题目如下 数据范围 显然数组长度最大可以到10的5次方n方的复杂度必然超时,阅读题目实际上就是寻找两个位置不同的数满足不等式即可(实际上i j无所谓是哪个 我们只要把位置小的想成i就行)。 按照上面的思路我们只需要排序数组然后从前往后遍历数组然后利用二分查找…...

Debian 10 中 Linux 4.19 内核在 x86_64 架构上对中断嵌套的支持情况
一、中断嵌套的定义与原理 中断嵌套是指在一个中断处理程序(ISR)正在执行的过程中,另一个更高优先级的中断请求到来,系统暂停当前中断处理程序,转而处理新的高优先级中断。处理完高优先级中断后,系统返回到原来的中断处理程序继续执行。这种机制允许系统更高效地响应紧急…...

FLTK - FLTK1.4.1 - demo - bitmap
文章目录 FLTK - FLTK1.4.1 - demo - bitmap概述笔记END FLTK - FLTK1.4.1 - demo - bitmap 概述 // 功能 : 演示位图数据在按钮上的显示 // * 以按钮为范围或者以窗口为范围移动 // * 上下左右, 文字和图像的相对位置 // 失能按钮,使能按钮 // 知识点 // FLTK可…...

数据结构 树1
目录 前言 一,树的引论 二,二叉树 三,二叉树的详细理解 四,二叉搜索树 五,二分法与二叉搜索树的效率 六,二叉搜索树的实现 七,查找最大值和最小值 指针传递 vs 传引用 为什么指针按值传递不会修…...

android主题设置为..DarkActionBar.Bridge时自定义DatePicker选中日期颜色
安卓自定义DatePicker选中日期颜色 背景:解决方案:方案一:方案二:实践效果: 背景: 最近在尝试用原生安卓实现仿element-ui表单校验功能,其中的的选择日期涉及到安卓DatePicker组件的使用&#…...

MySQL 如何深度分页问题
在实际的数据库应用场景中,我们常常会遇到需要进行分页查询的需求。对于少量数据的分页查询,MySQL 可以轻松应对。然而,当我们需要进行深度分页(即从大量数据的中间位置开始获取少量数据)时,就会面临性能严…...

1.攻防世界easyphp
进入题目页面如下 是一段PHP代码进行代码审计 <?php // 高亮显示PHP文件源代码 highlight_file(__FILE__);// 初始化变量$key1和$key2为0 $key1 0; $key2 0;// 从GET请求中获取参数a的值,并赋值给变量$a $a $_GET[a]; // 从GET请求中获取参数b的值ÿ…...

深度学习 Pytorch 神经网络的学习
本节将从梯度下降法向外拓展,介绍更常用的优化算法,实现神经网络的学习和迭代。在本节课结束将完整实现一个神经网络训练的全流程。 对于像神经网络这样的复杂模型,可能会有数百个 w w w的存在,同时如果我们使用的是像交叉熵这样…...

如何利用天赋实现最大化的价值输出-补
原文: https://blog.csdn.net/ZhangRelay/article/details/145408621 如何利用天赋实现最大化的价值输出-CSDN博客 如何利用天赋实现最大化的价值输出-CSDN博客 引用视频差异 第一段视频目标明确,建议也非常明确。 录制视频的人是主动性…...

Vue简介
目录 Vue是什么?为什么要使用Vue?Vue的三种加载方式拓展:什么是渐进式框架? Vue是什么? Vue是一套用于构建用户界面的渐进式 JavaScript (主张最少)框架 ,开发者只需关注视图层。另一方面,当与…...

three.js+WebGL踩坑经验合集(6.2):负缩放,负定矩阵和行列式的关系(3D版本)
本篇将紧接上篇的2D版本对3D版的负缩放矩阵进行解读。 (6.1):负缩放,负定矩阵和行列式的关系(2D版本) 既然three.js对3D版的负缩放也使用行列式进行判断,那么,2D版的结论用到3D上其实是没毛病的,THREE.Li…...

使用 OpenResty 构建高效的动态图片水印代理服务20250127
使用 OpenResty 构建高效的动态图片水印代理服务 在当今数字化的时代,图片在各种业务场景中广泛应用。为了保护版权、统一品牌形象,动态图片水印功能显得尤为重要。然而,直接在后端服务中集成水印功能,往往会带来代码复杂度增加、…...