在 Android 开发里,文件存储一直是个让人头疼的问题。不过别担心,今天就来聊聊怎么解决这个难题,主要讲讲 Scoped Storage 适配和最佳实践。

一、Scoped Storage 是什么

Scoped Storage 是 Android 在 10(API 级别 29)开始引入的一种新的文件存储机制。以前,应用在存储文件时权限比较大,能随意访问外部存储设备上的文件,这就带来了安全隐患。Scoped Storage 对应用访问外部存储的权限做了限制,让应用只能访问自己专属目录下的文件,以及用户明确授权访问的其他文件。

举个例子,假如你开发了一个图片编辑应用。在没有 Scoped Storage 之前,这个应用可以直接访问手机相册里的所有图片。但有了 Scoped Storage 后,应用默认只能访问自己创建的图片,要是想访问相册里其他图片,就需要用户手动授权。

二、应用场景

1. 图片、视频类应用

对于图片和视频编辑、浏览类应用,经常需要处理大量的媒体文件。Scoped Storage 可以让应用更安全地管理这些文件,避免和其他应用的文件冲突。比如一个图片编辑应用,它可以把用户编辑后的图片保存到自己的专属目录下,这样既方便管理,又能保证数据安全。

2. 文档管理类应用

文档管理应用需要对各种文档进行存储和访问。使用 Scoped Storage 可以让应用更好地处理用户的文档,并且在用户授权的情况下访问其他应用生成的文档。例如,一个办公文档管理应用可以在得到授权后访问用户在其他办公软件中创建的文档。

三、技术优缺点

1. 优点

提高数据安全性

限制了应用对外部存储的访问权限,减少了数据泄露的风险。比如,一个恶意应用就没办法随意访问其他应用的文件了。

简化文件管理

应用只需要关注自己的专属目录,不用再担心和其他应用的文件混淆。例如,一个音乐播放应用可以把下载的音乐文件都存放在自己的目录下,方便管理和查找。

提升用户体验

用户对自己的文件有更多的控制权,可以明确知道哪些应用可以访问自己的哪些文件。比如,当一个应用请求访问用户的相册时,用户可以根据需要选择是否授权。

2. 缺点

适配难度大

对于已经存在的应用,需要进行大量的代码修改来适配 Scoped Storage。比如,原来直接访问外部存储路径的代码都需要进行修改。

兼容性问题

在不同版本的 Android 系统上,Scoped Storage 的实现可能会有差异,需要进行额外的兼容性处理。例如,在 Android 10 到 Android 13 之间,Scoped Storage 的一些权限和访问方式都有变化。

四、注意事项

1. 权限请求

在使用 Scoped Storage 时,需要正确请求相应的权限。在 Android 10 及以上版本,需要使用新的权限请求方式。例如,请求访问媒体文件的权限代码示例(Java 技术栈):

// Java 技术栈示例
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
    // 检查是否有权限
    if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)
            != PackageManager.PERMISSION_GRANTED) {
        // 请求权限
        ActivityCompat.requestPermissions(this,
                new String[]{Manifest.permission.READ_EXTERNAL_STORAGE},
                MY_PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE);
    }
}

2. 路径处理

在 Scoped Storage 中,不能再像以前一样直接使用文件路径来访问文件。需要使用 ContentResolver 来进行文件的读写操作。例如,读取图片文件的代码示例(Java 技术栈):

// Java 技术栈示例
// 获取 ContentResolver 实例
ContentResolver contentResolver = getContentResolver();
// 假设 uri 是图片的 Content URI
Uri uri = Uri.parse("content://media/external/images/media/123");
try {
    // 打开输入流
    InputStream inputStream = contentResolver.openInputStream(uri);
    if (inputStream != null) {
        // 处理输入流,这里可以读取图片数据
        // ...
        inputStream.close();
    }
} catch (FileNotFoundException e) {
    e.printStackTrace();
} catch (IOException e) {
    e.printStackTrace();
}

3. 版本兼容性

不同版本的 Android 系统对 Scoped Storage 的支持有所不同。在开发时需要进行版本判断,采取不同的处理方式。例如,在 Android 9 及以下版本,不需要使用 Scoped Storage,可以继续使用传统的文件访问方式。代码示例(Java 技术栈):

// Java 技术栈示例
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
    // Android 9 及以下版本,使用传统文件访问方式
    File file = new File(Environment.getExternalStorageDirectory(), "myfile.txt");
    if (file.exists()) {
        // 读取文件内容
        try {
            FileInputStream fis = new FileInputStream(file);
            BufferedReader br = new BufferedReader(new InputStreamReader(fis));
            String line;
            while ((line = br.readLine()) != null) {
                // 处理每行内容
            }
            fis.close();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
} else {
    // Android 10 及以上版本,使用 Scoped Storage 方式
    // ...
}

五、最佳实践

1. 使用 MediaStore API

对于媒体文件(图片、视频、音频等),可以使用 MediaStore API 来进行管理。它可以方便地查询、插入和删除媒体文件。例如,插入一张图片到相册的代码示例(Java 技术栈):

// Java 技术栈示例
ContentResolver contentResolver = getContentResolver();
ContentValues values = new ContentValues();
values.put(MediaStore.Images.Media.DISPLAY_NAME, "new_image.jpg");
values.put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
    values.put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES);
}
Uri uri = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
if (uri != null) {
    try {
        OutputStream outputStream = contentResolver.openOutputStream(uri);
        if (outputStream != null) {
            // 将图片数据写入输出流
            // ...
            outputStream.close();
        }
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    }
}

2. 使用 Storage Access Framework (SAF)

当需要访问其他应用的文件时,可以使用 Storage Access Framework (SAF)。它提供了一个统一的界面让用户选择文件,并且应用可以在得到授权后访问这些文件。例如,使用 SAF 选择一个文件的代码示例(Java 技术栈):

// Java 技术栈示例
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("*/*");
startActivityForResult(intent, PICK_FILE_REQUEST_CODE);

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
    if (requestCode == PICK_FILE_REQUEST_CODE && resultCode == RESULT_OK) {
        if (data != null) {
            Uri uri = data.getData();
            if (uri != null) {
                // 处理选择的文件
                // ...
            }
        }
    }
}

3. 数据迁移

如果你的应用从旧的存储方式迁移到 Scoped Storage,需要进行数据迁移。可以在应用启动时检查是否需要迁移数据,并将旧的数据复制到新的存储位置。例如,将旧的图片文件迁移到新的专属目录的代码示例(Java 技术栈):

// Java 技术栈示例
private void migrateData() {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
        File oldDir = new File(Environment.getExternalStorageDirectory(), "old_images");
        if (oldDir.exists() && oldDir.isDirectory()) {
            File[] files = oldDir.listFiles();
            if (files != null) {
                for (File file : files) {
                    if (file.isFile()) {
                        try {
                            // 创建新的文件 Uri
                            ContentValues values = new ContentValues();
                            values.put(MediaStore.Images.Media.DISPLAY_NAME, file.getName());
                            values.put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg");
                            values.put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES);
                            ContentResolver contentResolver = getContentResolver();
                            Uri uri = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
                            if (uri != null) {
                                OutputStream outputStream = contentResolver.openOutputStream(uri);
                                FileInputStream fis = new FileInputStream(file);
                                byte[] buffer = new byte[1024];
                                int length;
                                while ((length = fis.read(buffer)) > 0) {
                                    outputStream.write(buffer, 0, length);
                                }
                                fis.close();
                                outputStream.close();
                            }
                        } catch (FileNotFoundException e) {
                            e.printStackTrace();
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }
    }
}

六、文章总结

Scoped Storage 是 Android 为提高数据安全性和用户体验而引入的一种新的文件存储机制。虽然它带来了很多好处,如提高数据安全性、简化文件管理等,但也存在适配难度大、兼容性问题等缺点。在开发过程中,需要注意权限请求、路径处理和版本兼容性等问题。通过使用 MediaStore API、Storage Access Framework (SAF) 等最佳实践,可以更好地适配 Scoped Storage。对于从旧的存储方式迁移到 Scoped Storage 的应用,需要进行数据迁移。总之,掌握 Scoped Storage 的适配和最佳实践对于 Android 开发者来说是非常重要的。