2026/8/26 13:05:47

Android开发实战:如何安全高效地获取并访问U盘存储路径

Android开发实战:如何安全高效地获取并访问U盘存储路径 1. 项目概述为什么在Android上获取U盘路径是个“技术活”如果你是一名Android开发者接到一个需求是“让App能读取用户插在手机OTG接口上的U盘文件”听起来是不是挺简单的不就是找个路径然后读写文件嘛。但当你真正动手去查Environment.getExternalStorageDirectory()或者遍历/storage目录时大概率会碰一鼻子灰。你会发现U盘的路径根本不是固定的不同品牌、不同Android版本、甚至不同文件系统格式的U盘挂载点都可能千差万别。这背后涉及的是Android系统复杂的存储架构和日益严格的外设访问权限管理。这个项目标题“Android获取usb上的U盘存储路径”看似是一个具体的API调用问题实则是一个需要深入理解Android存储访问框架SAF、USB主机模式USB Host Mode和运行时权限体系的综合性实战课题。它解决的不仅仅是“路径在哪”更是“如何安全、兼容、高效地让App与外置存储设备对话”。对于需要开发文件管理器、车载媒体播放器、数据备份工具或任何涉及外部大容量存储交互的App开发者来说掌握这套方法至关重要。新手可能会觉得迷茫有经验的开发者也可能在版本适配和厂商定制化问题上踩坑。接下来我将结合多年的移动端开发经验为你彻底拆解从原理到实现的完整流程并分享那些在官方文档里找不到的“避坑指南”。2. 核心原理与方案选型不止是File类那么简单在Android早期版本大致是4.4 KitKat之前处理外部存储相对“粗放”。应用申请了WRITE_EXTERNAL_STORAGE权限后就可以相对自由地访问sdcard目录。U盘等USB大容量存储设备USB Mass Storage UMS通常会挂载到/storage或/mnt下的某个动态生成的目录比如/storage/usb0或/mnt/media_rw/XXXX-XXXX。那时一些开发者的做法是监听系统挂载广播ACTION_MEDIA_MOUNTED然后遍历这些可能的前缀路径通过尝试列出文件列表来判断哪个是有效的U盘路径。然而这个方案在今天看来已经漏洞百出主要原因有三点权限模型收紧从Android 6.0API 23引入运行时权限开始到Android 10API 29引入分区存储Scoped Storage对外部存储的访问变得越来越严格。即使拥有旧版存储权限App的默认工作视图也仅限于应用专属目录和媒体集合。直接通过文件路径File API访问U盘根目录在Android 11及以上版本默认是禁止的。路径不统一与不可靠不同设备制造商OEM对USB存储设备的挂载点命名规则差异巨大。有的用/storage/usb有的用/mnt/usb_storage还有的是一串随机UUID。依赖硬编码或模式匹配的路径查找方式兼容性极差。协议与模式演变现代Android设备更多使用MTP媒体传输协议或PTP图片传输协议与电脑连接而U盘则使用USB大容量存储类UMS协议。设备需要支持USB主机模式USB Host Mode才能识别U盘。这涉及到硬件和驱动层面的支持。因此当前在Android上可靠获取并访问U盘存储的正确技术方案核心是USB主机模式API与存储访问框架的结合使用。方案选型决策逻辑目标API Level 21 (Android 5.0以下)这类设备已非常罕见。如果必须支持可尝试结合监听ACTION_MEDIA_MOUNTED广播和使用StorageManager的getStorageVolumes()方法部分旧版本可能有效进行遍历。但此方案属于“考古”范畴稳定性和兼容性无保障。目标API Level 21 (Android 5.0及以上)这是当前的主流和推荐方案。核心是使用UsbManager来枚举和请求访问USB设备识别出大容量存储设备后再通过StorageVolume或DocumentFile来获取可安全访问的存储表示。这个方案能很好地处理权限和路径抽象。注意网络上一些较旧的教程会教你如何通过反射调用隐藏API如StorageManager的getVolumeList()来获取路径。在Android高版本上这不仅是非官方做法而且会因为分区存储和权限限制而失效甚至导致应用崩溃。强烈建议摒弃这种危险的做法。3. 环境准备与权限配置在开始编码之前正确的配置是成功的一半。这里涉及到清单声明、权限请求和硬件特性确认。3.1 AndroidManifest.xml 配置首先在AndroidManifest.xml文件中声明必要的权限和特性。manifest ... !-- 1. USB主机模式权限必需 -- uses-feature android:nameandroid.hardware.usb.host android:requiredtrue / !-- 如果你的应用也希望支持USB附件模式Accessory Mode可以添加但非U盘必需 -- !-- uses-feature android:nameandroid.hardware.usb.accessory / -- !-- 2. 声明需要使用的权限 -- !-- 用于监听USB设备插拔的广播 -- uses-permission android:nameandroid.permission.USB_PERMISSION / !-- 在Android 11API 30及以上如果要使用MediaStore访问公共目录可能需要 -- !-- uses-permission android:nameandroid.permission.READ_EXTERNAL_STORAGE android:maxSdkVersion32 / -- !-- 注意对于U盘分区存储下的MANAGE_EXTERNAL_STORAGE权限是另一条路但上架Google Play有严格限制不推荐常规使用。 -- application ... !-- 3. 注册USB设备附件广播接收器可选但推荐 -- !-- 这个intent-filter允许你的应用在用户插入USB设备时被系统提示是否打开你的应用 -- activity ... intent-filter action android:nameandroid.hardware.usb.action.USB_DEVICE_ATTACHED / /intent-filter !-- 必须指定一个资源文件其中声明了你的应用感兴趣的USB设备信息 -- meta-data android:nameandroid.hardware.usb.action.USB_DEVICE_ATTACHED android:resourcexml/device_filter / /activity /application /manifest3.2 创建USB设备过滤器 (device_filter.xml)在res/xml/目录下创建device_filter.xml文件。这个文件用于声明你的应用希望处理哪些USB设备。对于U盘大容量存储设备我们通常关心其接口类Interface Class。?xml version1.0 encodingutf-8? resources !-- 捕获大容量存储设备USB Mass Storage Class -- usb-device class8 !-- 大容量存储设备的类代码是8 -- subclass6 !-- 子类代码6通常代表SCSI透明命令集SCSI Transparent Command Set是U盘的常见子类 -- protocol80 / !-- 协议代码80代表Bulk-Only Transport (BOT)最常见的U盘传输协议 -- !-- 注意class/subclass/protocol的组合可能因设备而异。 一个更宽泛的过滤器是只指定 class8这样可以捕获所有大容量存储类设备但可能也包含一些非U盘设备如读卡器。 在实际项目中可能需要根据具体设备调试确定。 -- /resources实操心得class8是核心。subclass和protocol的精确值有时需要连接具体U盘后通过UsbDevice对象打印其信息来获取。对于希望兼容性更广的应用可以先只设置class8在代码中再做进一步判断。3.3 运行时权限请求从Android 6.0开始需要动态请求危险权限。虽然USB权限USB_PERMISSION不是典型运行时权限但请求流程是类似的并且需要在合适的时机如Activity创建时检查并请求。对于存储权限根据你的访问策略如果你计划通过ACTION_OPEN_DOCUMENT_TREE让用户选择U盘根目录推荐则不需要声明READ_EXTERNAL_STORAGE权限。如果你需要以更传统的方式访问并且目标API在29以下可能需要请求READ_EXTERNAL_STORAGE。4. 核心实现枚举、请求与获取存储访问权这是整个流程的代码核心部分。我们将分步骤实现。4.1 初始化UsbManager并枚举设备在你的Activity或Fragment中首先获取UsbManager服务并枚举当前已连接的USB设备。// 以Kotlin为例Java逻辑类似 import android.hardware.usb.UsbManager import android.hardware.usb.UsbDevice import android.content.Context class USBActivity : AppCompatActivity() { private lateinit var usbManager: UsbManager override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_usb) usbManager getSystemService(Context.USB_SERVICE) as UsbManager // 枚举当前所有连接的USB设备 val deviceList: HashMapString, UsbDevice usbManager.deviceList for (device in deviceList.values) { Log.d(USB, 发现设备: ${device.deviceName}, VID: ${device.vendorId}, PID: ${device.productId}) // 判断是否为大容量存储设备 if (isMassStorageDevice(device)) { Log.i(USB, 疑似U盘设备: ${device.deviceName}) // 接下来可以请求权限 requestUSBPermission(device) } } } private fun isMassStorageDevice(device: UsbDevice): Boolean { // 方法1检查设备类代码 if (device.deviceClass 8) { // USB_CLASS_MASS_STORAGE return true } // 方法2更严谨的做法是遍历设备的所有接口Interface for (i in 0 until device.interfaceCount) { val usbInterface device.getInterface(i) if (usbInterface.interfaceClass 8) { return true } } return false } }4.2 请求USB设备访问权限发现目标设备后需要向用户请求访问该特定USB设备的权限。// 定义权限请求的广播接收器 private val usbPermissionReceiver object : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { val action intent.action if (ACTION_USB_PERMISSION action) { synchronized(this) { val device: UsbDevice? intent.getParcelableExtra(UsbManager.EXTRA_DEVICE) if (intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)) { device?.apply { Log.i(USB, 已获得USB设备访问权限: $deviceName) // 权限已授予现在可以尝试获取存储路径 processGrantedUsbDevice(this) } } else { Log.e(USB, 用户拒绝了USB设备访问权限: ${device?.deviceName}) Toast.makeText(context, 未获得USB访问权限无法读取U盘。, Toast.LENGTH_LONG).show() } } } } } private fun requestUSBPermission(device: UsbDevice) { // 创建权限请求意图 val permissionIntent PendingIntent.getBroadcast( this, 0, Intent(ACTION_USB_PERMISSION), PendingIntent.FLAG_IMMUTABLE ) // 检查是否已有权限 if (usbManager.hasPermission(device)) { Log.d(USB, 已拥有该设备权限直接处理) processGrantedUsbDevice(device) } else { // 注册广播接收器 val filter IntentFilter(ACTION_USB_PERMISSION) registerReceiver(usbPermissionReceiver, filter) // 请求权限会弹出系统对话框 usbManager.requestPermission(device, permissionIntent) Log.d(USB, 已弹出USB权限请求对话框) } } companion object { private const val ACTION_USB_PERMISSION ${BuildConfig.APPLICATION_ID}.USB_PERMISSION }注意事项requestPermission调用会触发一个系统对话框。用户的操作结果允许或拒绝会通过你注册的广播接收器usbPermissionReceiver返回。务必在onDestroy中注销广播接收器避免内存泄漏。4.3 获取U盘的存储路径URI或DocumentFile获得USB设备权限后传统的思路是去找一个像/storage/xxxx这样的绝对路径。但在现代Android开发中更推荐使用存储卷StorageVolume或存储访问框架SAF提供的URI来代表存储位置这更安全、更兼容。方法A通过StorageManager获取StorageVolume (API 24)在Android 7.0 (API 24) 及以上StorageManager提供了getStorageVolumes()方法可以返回包括U盘在内的所有存储卷列表。import android.os.storage.StorageManager import android.os.storage.StorageVolume private fun processGrantedUsbDevice(device: UsbDevice) { val storageManager getSystemService(Context.STORAGE_SERVICE) as StorageManager // 获取所有存储卷 val storageVolumes: ListStorageVolume storageManager.storageVolumes for (volume in storageVolumes) { // 判断是否为可移除Removable存储U盘通常属于此类 if (volume.isRemovable) { // 注意这里无法直接通过UsbDevice对象与StorageVolume关联。 // 一种常见做法是当只有一个可移除卷时假定它就是U盘。 // 更可靠的方法是通过卷的标签getDescription或UUID来判断但这需要更多上下文信息。 Log.i(USB, 找到可移除存储卷: ${volume.getDescription(this)}) // 获取该存储卷的挂载状态 val state Environment.getExternalStorageState(volume.directory) if (Environment.MEDIA_MOUNTED state || Environment.MEDIA_MOUNTED_READ_ONLY state) { // 已挂载可以访问 // 获取代表该卷根目录的DocumentFile需要用户通过SAF授权 // 或者直接使用该卷的目录在分区存储下直接访问可能受限 val volumeDir volume.directory Log.i(USB, 存储卷目录: $volumeDir) // 重要在Android 10直接使用File(volumeDir.path)访问可能失败。 // 推荐引导用户通过SAF选择此卷。 launchSAFTreePicker(volume) break } } } }方法B引导用户通过SAF选择U盘根目录最通用、最推荐这是兼容性最好、符合Android最新存储规范的做法。我们使用ACTION_OPEN_DOCUMENT_TREE意图让用户自己选择U盘的根目录。一旦用户选择并授权我们将获得一个持久化的URI权限以后都可以用这个URI来访问U盘。import android.content.Intent import android.net.Uri import androidx.documentfile.provider.DocumentFile private fun launchSAFTreePicker(storageVolume: StorageVolume? null) { val intent Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).apply { // 可选指定初始URI但并非所有设备都支持 storageVolume?.let { // 从StorageVolume获取其特定的URIAPI 30 if (Build.VERSION.SDK_INT Build.VERSION_CODES.R) { val volumeUri it.directory // 注意这里需要将File转换为合适的URI但更直接的做法是让用户选择 // putExtra(DocumentsContract.EXTRA_INITIAL_URI, volumeUri) } } // 添加标志请求持久化权限 addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION) addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION) // 如果需要写入 } // 使用一个明确的请求码 startActivityForResult(intent, REQUEST_CODE_OPEN_DOCUMENT_TREE_USB) } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) if (requestCode REQUEST_CODE_OPEN_DOCUMENT_TREE_USB resultCode Activity.RESULT_OK) { data?.data?.let { treeUri - Log.i(USB, 用户选择的树URI: $treeUri) // 保存这个URI例如使用SharedPreferences saveUsbTreeUri(treeUri) // 现在可以使用DocumentFile API来遍历和操作U盘文件 val rootDocumentFile DocumentFile.fromTreeUri(this, treeUri) rootDocumentFile?.listFiles()?.forEach { file - Log.d(USB, U盘根目录下的文件: ${file.name}) } // 提示用户授权成功 Toast.makeText(this, 已获得U盘访问权限, Toast.LENGTH_SHORT).show() } } } private fun saveUsbTreeUri(uri: Uri) { // 使用SharedPreferences保存URI字符串 val prefs getSharedPreferences(usb_prefs, Context.MODE_PRIVATE) prefs.edit().putString(usb_tree_uri, uri.toString()).apply() } // 在应用下次启动时可以尝试恢复权限 private fun restoreUsbAccess() { val prefs getSharedPreferences(usb_prefs, Context.MODE_PRIVATE) val uriString prefs.getString(usb_tree_uri, null) uriString?.let { val treeUri Uri.parse(it) // 重新获取持久化权限重要 contentResolver.takePersistableUriPermission( treeUri, Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION ) val rootDoc DocumentFile.fromTreeUri(this, treeUri) if (rootDoc ! null rootDoc.exists()) { Log.i(USB, 成功恢复U盘访问权限) // 可以开始使用rootDoc进行文件操作 } else { Log.w(USB, 保存的URI权限已失效需要用户重新选择) // 清除无效的URI prefs.edit().remove(usb_tree_uri).apply() } } }实操心得ACTION_OPEN_DOCUMENT_TREE是获取外部存储包括U盘访问权的“黄金标准”。它把选择权和知情权交给了用户完全符合Google的隐私和安全策略。获取到的treeUri通常以content://com.android.externalstorage.documents/tree/XXXX-XXXX%3A形式出现就是你的“存储路径”在现代Android中的等价物。之后所有文件操作都应通过DocumentFileAPI或使用ContentResolver打开输入/输出流来完成而不是传统的java.io.File。5. 文件操作实战用DocumentFile替代File一旦你拥有了代表U盘根目录的DocumentFile对象就可以进行类似File的操作但API有所不同。// 假设 rootDoc 是U盘根目录的DocumentFile对象 // 1. 列出文件和目录 val filesAndDirs rootDoc.listFiles() filesAndDirs.forEach { docFile - val name docFile.name ?: 未知 val type if (docFile.isDirectory) 目录 else 文件 val size if (docFile.isFile) formatFileSize(docFile.length()) else Log.d(USB, $type: $name $size) } // 2. 创建新目录 val newDir rootDoc.createDirectory(MyAppData) if (newDir ! null newDir.exists()) { Log.i(USB, 目录创建成功) } // 3. 创建新文件并写入内容 val newFile rootDoc.createFile(text/plain, my_note.txt) newFile?.let { fileDoc - // 写入内容需要使用ContentResolver contentResolver.openOutputStream(fileDoc.uri)?.use { outputStream - outputStream.write(Hello from Android App!.toByteArray()) outputStream.flush() } Log.i(USB, 文件创建并写入成功) } // 4. 读取文件内容 val targetFile rootDoc.findFile(my_note.txt) // 查找文件 targetFile?.let { fileDoc - contentResolver.openInputStream(fileDoc.uri)?.use { inputStream - val content inputStream.bufferedReader().readText() Log.i(USB, 文件内容: $content) } } // 5. 删除文件或目录 val itemToDelete rootDoc.findFile(old_file.txt) itemToDelete?.delete() // 辅助函数格式化文件大小 private fun formatFileSize(size: Long): String { if (size 0) return 0 B val units arrayOf(B, KB, MB, GB, TB) val digitGroups (Math.log10(size.toDouble()) / Math.log10(1024.0)).toInt() return DecimalFormat(#,##0.#).format(size / Math.pow(1024.0, digitGroups.toDouble())) units[digitGroups] }关键点解析DocumentFile是一个抽象层它屏蔽了底层真实的文件系统路径。createFile方法的第一个参数是MIME类型这很重要。对于U盘上的文件系统可能会根据MIME类型来建议默认的应用程序。读取和写入都必须通过ContentResolver和Uri进行这确保了访问在系统控制的沙箱内进行。6. 兼容性处理与疑难排查在实际开发中你会遇到各种设备和系统版本带来的挑战。以下是常见问题及解决方案。6.1 不同Android版本的适配策略Android 版本 (API)关键变化适配策略API 21 (5.0)无官方USB Host API支持存储模型旧。考虑放弃支持或使用已废弃的getExternalStorageDirectory()和广播监听兼容性极差。API 21-22 (5.x)引入UsbManager和StorageVolume部分API。可以使用USB Host API检测设备。获取路径可能仍需依赖StorageManager.getStorageVolumes()部分厂商实现。SAF (ACTION_OPEN_DOCUMENT_TREE) 已引入是首选。API 23-28 (6.0-9.0)运行时权限、分区存储前夜。必须动态请求权限。FileAPI在获得权限后仍可访问外部存储但为未来兼容强烈建议开始迁移到SAF。API 29 (10.0)分区存储 (Scoped Storage)强制实施。FileAPI访问外部公共目录受限。访问U盘等外置存储必须使用SAF (ACTION_OPEN_DOCUMENT_TREE)或通过MediaStore访问媒体文件。MANAGE_EXTERNAL_STORAGE权限审核严格不推荐。API 30 (11.0)存储权限细化MANAGE_EXTERNAL_STORAGE引入。SAF依然是访问U盘的最佳实践。如果应用需要管理所有文件如文件管理器可申请MANAGE_EXTERNAL_STORAGE但上架Google Play需声明并可能面临审核。6.2 常见问题速查与解决问题1usbManager.deviceList始终为空检测不到U盘。检查1硬件与线缆。确认手机/平板支持USB OTG功能。使用质量好的OTG转接线。有些设备需要手动在设置中开启“OTG连接”选项。检查2电源。大容量U盘可能供电不足尝试使用带外接供电的USB Hub。检查3文件系统。Android通常支持FAT32、exFAT和NTFS部分设备需额外支持。如果U盘是EXT4、HFS等Linux/Mac格式可能无法识别。尝试在电脑上将U盘格式化为FAT32或exFAT。检查4清单配置。确认uses-feature android:nameandroid.hardware.usb.host android:requiredtrue /已声明。问题2用户点击授权后BroadcastReceiver没有收到ACTION_USB_PERMISSION广播。检查1PendingIntent的Flag。在Android 8.0以上创建PendingIntent时必须指定FLAG_IMMUTABLE或FLAG_MUTABLE。通常使用FLAG_IMMUTABLE。检查2广播接收器注册时机。确保在调用requestPermission之前已经registerReceiver。检查3Intent Filter Action。确保接收器监听的Action字符串与请求时Intent的Action完全一致。使用应用包名作为前缀是防止冲突的好习惯。问题3通过SAF选择U盘后应用重启后DocumentFile.fromTreeUri返回null或操作失败。原因持久化URI权限在系统重启或设备长时间休眠后可能会丢失或者用户手动清除了数据。解决每次使用保存的URI前都应调用contentResolver.takePersistableUriPermission重新获取权限并检查DocumentFile.exists()。如果失败需要引导用户重新选择。这是SAF机制的一部分应用需要优雅地处理权限失效的情况。问题4在部分国产定制系统如MIUI、EMUI上SAF选择器里看不到U盘。原因一些厂商深度定制了文件选择器可能隐藏了存储卷或者需要用户手动在“安全”或“权限管理”中授予应用“读取外部存储”的权限即使你未声明该权限。解决引导用户去系统设置的应用权限管理中为你的应用开启“读取手机存储”或类似权限。在应用内提供清晰的指引告诉用户如何在系统自带的“文件管理”App中找到U盘然后通过“文件管理”的分享功能选择你的应用来打开这有时会触发系统授予你对该目录的临时访问权。作为备选方案可以考虑申请MANAGE_EXTERNAL_STORAGE权限但这会带来上架和用户信任问题。问题5使用DocumentFileAPI进行文件操作尤其是大量小文件读写时速度较慢。原因ContentResolver的抽象层会带来一定的性能开销与直接的文件系统IO相比有损耗。优化对于大文件传输使用ContentResolver.openFileDescriptor获取ParcelFileDescriptor然后使用NIO通道如FileChannel进行读写性能更好。批量操作时尽量减少listFiles()的调用可以缓存目录结构。在非UI线程执行所有文件IO操作。7. 性能优化与高级技巧当基本功能实现后可以考虑以下优化点来提升用户体验和应用的健壮性。7.1 监听USB设备的动态插拔除了在应用启动时枚举设备实时监听插拔事件能让体验更流畅。// 在Activity或Service中注册广播接收器 private val usbDeviceReceiver object : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { val action intent.action val device: UsbDevice? intent.getParcelableExtra(UsbManager.EXTRA_DEVICE) when (action) { UsbManager.ACTION_USB_DEVICE_ATTACHED - { Log.i(USB, USB设备已连接: ${device?.deviceName}) device?.let { if(isMassStorageDevice(it)) { requestUSBPermission(it) } } } UsbManager.ACTION_USB_DEVICE_DETACHED - { Log.i(USB, USB设备已移除: ${device?.deviceName}) // 清理与该设备相关的资源和UI状态 onUsbDeviceRemoved(device) } } } } override fun onResume() { super.onResume() val filter IntentFilter().apply { addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED) addAction(UsbManager.ACTION_USB_DEVICE_DETACHED) } registerReceiver(usbDeviceReceiver, filter) } override fun onPause() { super.onPause() unregisterReceiver(usbDeviceReceiver) }7.2 处理多U盘同时连接的情况高端设备或通过Hub可能连接多个U盘。你的应用需要能区分和管理它们。标识设备使用UsbDevice的vendorId(VID)、productId(PID)、序列号serialNumber或设备名deviceName的组合来唯一标识一个物理设备。关联存储卷这是一个难点。系统没有直接API将UsbDevice对象与StorageVolume或SAF URI关联。一种实践策略是在用户通过SAF选择U盘目录时记录下此时的UsbDevice列表。假设用户选择的卷对应于当前已连接的、符合大容量存储类规则的某个设备。你可以将选择的treeUri与一个设备标识符如VID_PID_SERIAL绑定存储起来。当设备重新连接时根据标识符尝试恢复对应的treeUri访问权限。UI设计在UI上清晰列出所有已授权访问的U盘并允许用户分别对它们进行操作。7.3 后台服务与文件传输如果需要进行长时间的大文件备份或同步需要在后台服务中处理。使用WorkManager对于可延迟的、保证最终会执行的传输任务WorkManager是最佳选择。它能处理应用进程死亡、设备重启等情况。使用前台服务对于用户主动发起、需要即时反馈且可能耗时的传输应启动一个前台服务并显示持续的通知告知用户传输进度。注意事项在后台通过ContentResolver和DocumentFile进行文件操作是允许的。但务必在服务中重新获取并持有对treeUri的持久化权限。同时要妥善处理USB设备在传输过程中被拔出的情况及时通知用户并清理任务。这个项目从表面看是获取一个路径但深入下去它串联了Android硬件访问、权限体系、存储演进和最佳实践。我个人的体会是尽早拥抱Storage Access Framework虽然初期学习曲线比直接操作File要陡峭但它为你扫清了未来系统升级和厂商定制化带来的大量兼容性地雷。在实际开发中一定要在尽可能多的真机特别是不同品牌、不同Android版本上进行测试因为USB和存储相关的“魔改”实在是太多了。最后给用户清晰的操作引导和友好的错误提示比任何精巧的代码都更重要。