Android 7.0如何读取SD卡文件?旧版应用适配问题求助
我完全理解你的困扰——Android从4.0到7.0的存储机制变化确实踩了不少坑,尤其是外部存储的定义和API行为的改变。咱们一步步拆解问题,找到解决方案:
问题根源
在Android 4.0时代,大多数设备的“外部存储”就是可移除SD卡,所以Environment.getExternalStorageDirectory()能直接指向SD卡路径。但从Android 4.4开始,系统把设备内置存储的一部分模拟成外部存储(也就是你看到的storage/emulated/0/),而真正的可移除SD卡变成了独立的“二级外部存储”。到Android 7.0时,getExternalStorageDirectory()和getExternalStoragePublicDirectory()已经完全指向内置的模拟外部存储,不再关联可移除SD卡,这就是你的应用失效的核心原因。
解决方案步骤
1. 获取可移除SD卡的真实路径
Android没有直接的API返回可移除SD卡的路径,但可以通过StorageManager遍历存储卷,筛选出可移除的SD卡:
- 针对Android 7.0(API 24)及以上,直接用公开的
StorageVolumeAPI即可:
public List<String> getRemovableSdCardPaths(Context context) { List<String> sdCardPaths = new ArrayList<>(); StorageManager storageManager = (StorageManager) context.getSystemService(Context.STORAGE_SERVICE); if (storageManager == null) return sdCardPaths; for (StorageVolume volume : storageManager.getStorageVolumes()) { // 判断是否为可移除SD卡 if (volume.isRemovable()) { // 适配不同API版本的路径获取方式 String path = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O ? volume.getDirectory().getPath() : volume.getPath(); sdCardPaths.add(path); } } return sdCardPaths; }
- 如果需要兼容更低版本(比如回到Android 4.4),可以用反射调用隐藏API,避免版本兼容问题:
public List<String> getRemovableSdCardPaths(Context context) { List<String> sdCardPaths = new ArrayList<>(); StorageManager storageManager = (StorageManager) context.getSystemService(Context.STORAGE_SERVICE); if (storageManager == null) return sdCardPaths; try { // 反射调用getVolumeList()获取存储卷 Method getVolumeList = StorageManager.class.getMethod("getVolumeList"); Object[] volumes = (Object[]) getVolumeList.invoke(storageManager); for (Object volume : volumes) { // 判断是否可移除 Method isRemovable = volume.getClass().getMethod("isRemovable"); boolean removable = (boolean) isRemovable.invoke(volume); if (removable) { // 获取路径 Method getPath = volume.getClass().getMethod("getPath"); String path = (String) getPath.invoke(volume); sdCardPaths.add(path); } } } catch (Exception e) { e.printStackTrace(); } return sdCardPaths; }
调用这个方法后,你会得到类似/storage/XXXX-XXXX/的路径(XXXX是SD卡的UUID,不同设备可能不同),这就是可移除SD卡的真实根目录。
2. 申请必要的存储权限
Android 6.0(API 23)及以上需要动态申请存储权限,否则无法访问SD卡文件:
private static final int STORAGE_PERMISSION_REQUEST_CODE = 1001; // 发起权限申请 private void requestStoragePermission() { if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions( this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE}, STORAGE_PERMISSION_REQUEST_CODE ); } else { // 权限已获取,开始访问SD卡文件 accessSdCardFiles(); } } // 处理权限申请结果 @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); if (requestCode == STORAGE_PERMISSION_REQUEST_CODE) { if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { accessSdCardFiles(); } else { Toast.makeText(this, "需要存储权限才能访问SD卡数据", Toast.LENGTH_SHORT).show(); } } } // 访问SD卡文件的逻辑 private void accessSdCardFiles() { List<String> sdPaths = getRemovableSdCardPaths(this); if (!sdPaths.isEmpty()) { // 取第一个可移除SD卡路径(大多数设备只有一个) String sdRoot = sdPaths.get(0); // 拼接你要访问的文件路径,比如之前复制的data文件 File targetFile = new File(sdRoot + "/your_data_folder/your_file.txt"); // 接下来就可以正常读写这个文件了 } else { Toast.makeText(this, "未检测到可移除SD卡", Toast.LENGTH_SHORT).show(); } }
3. 绝对不要硬编码路径
不同品牌设备的SD卡路径差异极大(比如有的是/storage/sdcard1/,有的是带UUID的路径),硬编码路径会导致应用在部分设备上直接失效,必须通过API动态获取。
额外注意事项
- 如果你的应用需要访问SD卡上的媒体文件(图片、音频、视频),也可以通过
MediaStoreAPI查询,但对于用户通过USB复制的任意文件,直接访问SD卡路径更直接。 - Android 10及以上引入了Scoped Storage,对外部存储的访问限制更严格,但你的问题针对Android 7.0,暂时不需要考虑这部分。
内容的提问来源于stack exchange,提问作者Stephen Poley

