Android 权限管理 — 只防君子不防小人_第1张图片

存储优化系列专题,来聊一聊开发过程中常见存储方法的优缺点,希望可以帮你在日常工作中如何做出更好的选择。


Android 存储优化系列专题
  • SharedPreferences 系列

《Android 之不要滥用 SharedPreferences(上)》
《Android 之不要滥用 SharedPreferences(下)》

  • ContentProvider 系列(待更)

《Android 存储选项之 ContentProvider 的启动性能》
《Android 存储选项之 ContentProvider 深入分析》

  • 对象序列化系列

《Android 对象序列化之你不知道的 Serializable》
《Android 对象序列化之 Parcelable 取代 Serializable ?》
《Android 对象序列化之追求性能完美的 Serial》

  • 数据序列化系列(待更)

《Android 数据序列化之 JSON》
《Android 数据序列化之 Protocol Buffer》
《基于 MMAP 的高性能 K-V 组件之 MMKV》

  • SQLite 存储系列

《Android 存储选项之 SQLiteDatabase 创建过程源码分析》
《Android 存储选项之 SQLiteDatabase 源码分析》
《数据库连接池 SQLiteConnectionPool 源码分析》
《SQLiteDatabase 启用事务源码分析》
《SQLite 数据库 WAL 模式工作原理简介》
《SQLite 数据库锁机制与事务简介》
《SQLite 数据库优化那些事儿》


大多数开发者都曾经遇到过在存储设计上的问题,该问题也是被广泛讨论的老话题了,时至现在的 Android 10 仍然有很多问题在系统层面没有被很好的解决。

Android 存储优化系列专题先后为大家介绍了系统为我们提供的多种持久化存储方案,存储就是把特定的数据结构转换成可以被记录和还原的格式。如果文件涉及到敏感或不想被其他应用访问时该如何选择呢?今天我们就来聊一聊 Android 存储的权限管理。

所有 Android 设备都有两个文件存储区域:内部存储区和外部存储区。这主要是由于 Android 早期,当时大多数设备都提供了内置的非意易失性内存(内部存储),以及可移动的外置存储介质,例如 micro SD 卡(外部存储)。

1. 有限的内部存储

由于早期 Android 手机自带存储空间只有内部存储,而且空间很有限。也是因为这样的原因,应用一般要将语音、图片、视频等都放在外置 SD 卡上,否则放入内部存储会很快被用尽存储空间,导致用户看到手机还有空间,而 App 却不能正常使用。

现在,许多设备将永久存储空间划分为单独的内部和外部分区。因此,即使没有可移动的存储介质,这两个存储空间也始终存在,并且无论外部存储是否可移动,API 行为都是相同的。

2. 目前还不安全的外部(私有)储存

虽然 Android 也提供了不获取权限直接可用的外部私有存储目录如 Context.getExternalFilesDir()。但目前这样的设计对应用数据的安全来说没有帮助,因为外置(私有)存储仍然可以被有权限的应用读取。

由于外部存储的不确定性,因此两种选项之间会存在一些差异,如下图所示:

Android 权限管理 — 只防君子不防小人_第2张图片

乍一看,可能还是不太好理解,到底该怎么理解内部存储和外部存储呢?当要确保其他应用或用户都无法访问的存储时,此时内部存储是最佳选择;而外部存储是存放那些不需要访问限制,允许其他应用或用户通过手机可以直接访问的文件

  • 应用程序默认被安装到内部存储区域,但是也可以通过清单文件配置来指定应用允许被安装在外部存储。这种情况适用于 APK 文件较大且外部空间大于内部存储空间时。具体可以参阅 App 安装位置。

内部存储

默认情况下,保存至内部存储的文件属于应用私有文件,其他应用(或用户)不能访问这些文件(除非拥有 Root 访问权限)。如此一来,内部存储非常适合保存不需要让用户直接访问的应用内部数据。在文件系统中,系统会为每个应用提供私有存储目录,在该目录下我们可以进一步创建应用所需的任何文件。

当应用被卸载时,保存在内部存储中的文件也随之被移除。所以,不能依赖内部存储来保存用户希望独立于应用而保留的任何数据

按照访问存储目录 API,内部存储可以简单划分为:内部文件存储和内部缓存存储。它们都属于应用私有存储目录,访问应用内部存储目录 API 如下:

// 内部文件存储File filesDir = getFilesDir();// 内部缓存存储File cacheDir = getCacheDir();// 在应用唯一目录内创建/打开一个新的目录getDir(String name, int mode)// 应用内部存储的核心 API,以上目录全都依赖于它getDataDir()
内部缓存文件

如果想要暂时保存某些数据,系统提供了特殊的私有缓存目录来保存这些数据。当设备存储空间不足时,Android 可能会删除这些缓存文件以回收空间。但是不要过度依赖于系统提供的清理工作,而始终应该自行维护这些缓存文件的大小。以便使占用空间保持在合理的限制范围内。当应用被卸载时,这些文件也会随之被删除。

内部缓存存储在设备存储空间不足时,可能会被系统清理掉,这是区别于内部文件存储的最重要特征。


将数据保存在内部存储中

内部存储是根据程序的包名在 Android 文件系统的特殊位置被创建。注意与外部存储目录不同,应用不需要任何系统权限即可读写内部存储

1. 查询可用空间

如果我们事先知道要保存数据的大小,可以通过如下两个 API 查找是否有足够的可用空间。这样可以有效避免将存储量填满到某个阈值以上。

  • getFreeSpace():获取当前存储空间的剩余空间大小。
  • getTotalSpace():获取当前存储空间总大小。
2. 写入文件

将数据保存到内部存储时,可以通过以下两种方式获取适当的存储目录:

  • getFileDir():获取内部文件存储目录
  • getCacheDir():获取内部缓存存储目录

注意:如果系统存储空间不足,则可能在没有警告的情况下删除缓存目录下文件。

如果需要创建新的目录,可以使用 File 重新指定,将上述指定的内部存储目录作为参数:

File newFile = new File(getFilesDir(), fileName)

Google 推荐使用 Jetpack 安全性库 方式写入文件:

// Although you can define your own key generation parameter specification, it's// recommended that you use the value specified here.KeyGenParameterSpec keyGenParameterSpec = MasterKeys.AES256_GCM_SPEC;String masterKeyAlias = MasterKeys.getOrCreate(keyGenParameterSpec);// Creates a file with this name, or replaces an existing file// that has the same name. Note that the file name cannot contain// path separators.String fileToWrite = "my_sensitive_data.txt";try {    EncryptedFile encryptedFile = new EncryptedFile.Builder(            new File(directory, fileToWrite),            context,            masterKeyAlias,            EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB    ).build();    // Write to a file.    BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(            encryptedFile.openFileOutput()));    writer.write("MY SUPER-SECRET INFORMATION");} catch (GeneralSecurityException gse) {    // Error occurred getting or creating keyset.} catch (IOException ex) {    // Error occurred opening file for writing.}

或者,可以直接使用系统提供的 openFileOutput() 获取一个 FileOutputStream 对象如下:

final String fileName = "testInternalFile";final String fileContent = "Hello World!";FileOutputStream fos;try{    fos = context.openFileOutput(fileName, Context.MODE_PRIVATE);    fos.write(fileContent.toByteArray());    fos.close();}catch(Exception e){    e.printStackTrace();}

注意:openFileOutput() 方法需要指定文件模式,通过 MODE_PRIVATE 使它对您的应用变为私有。从 API Level 17 开始,其他模式选项 MODE_WROLD_READABLE 和 MODE_WORLD_WRITEABLE 已被弃用。从 Android 7.0(API Level 24)开始,如果再使用它们将会抛出 SecurityException。如果需要与其他应用程序共享私有文件,则应该使用带有 FLAG_GRANT_READ_URI_PERMISSION 属性的 FileProvider。

3. 写入缓存文件

如果需要暂时缓存某些数据,可以使用 createTempFile()。例如从 URL 对象中提取文件名,并为其创建缓存目录:

File file;try{    final String fileName = Uri.parse(url).getLastPathSegmetn();    file = File.createTempFile(fileName, null, context.getCacheDir());}catch(Exception e){    // Error while creating file}return file;

正如前面所述,我们应该定期清除不再需要的缓存文件,而不是依赖系统的清理工作。

4. 打开已有文件

Google 推荐使用 Jetpack 库 以更安全的方式读取文件,如下:

// Although you can define your own key generation parameter specification, it's// recommended that you use the value specified here.KeyGenParameterSpec keyGenParameterSpec = MasterKeys.AES256_GCM_SPEC;String masterKeyAlias = MasterKeys.getOrCreate(keyGenParameterSpec);String fileToRead = "my_sensitive_data.txt";EncryptedFile encryptedFile = new EncryptedFile.Builder(        File(directory, fileToRead),        context,        masterKeyAlias,        EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB).build();StringBuffer stringBuffer = new StringBuffer();try (BufferedReader reader =             new BufferedReader(new FileReader(encryptedFile))) {    String line = reader.readLine();    while (line != null) {        stringBuffer.append(line).append('\n');        line = reader.readLine();    }} catch (IOException e) {    // Error occurred opening raw file for reading.} finally {    String contents = stringBuffer.toString();}

同样,也可以直接使用 openFileInput(name) 传递文件名方式获取到 FileInputStream。

注意:如果需要在安装时将文件打包为可在应用程序中访问的文件,可以将文件保存在项目 /res/raw 目录中。传入带有前缀的文件名 R.raw 作为资源 ID 通过 openRawResource() 打开对应文件。此方法返回 InputStream,但是无法写回数据。


外部存储

外部存储在概念上非常容易被误解,它属于 Android 发展的历史产物,早期的手机设备支持插入新的存储介质,例如 micro SD 卡(外部存储)。现在已经很难看到了。外部存储是相对于内部存储而言的,如果数据允许被其他应用访问或者允许用户在手机中进行访问,此时外部存储便非常适合。

外部存储可以划分为公共目录应用私有目录

  • 公共目录:可以被其他应用或用户自由使用的文件。该目录下文件不会受到应用的卸载而被移除。

  • 应用私有目录:存储在特定于应用程序目录的文件,可以使用 Context.getExternalFilesDir() 或 Context.getExternalCacheDir() 获取。当应用被卸载时该目录下文件也会被移除。注意,虽然它们也属于应用私有目录,但它们位于外部存储上,从技术上来讲用户和其他应用程序都可以访问这些文件,一般用于存放一些不是特别敏感的应用数据,但由不想与其他应用程序共享的文件

注意:如果用户卸下或断开外部存储设备(例如 SD 卡)的连接,则存储在外部存储中的文件可能变得不可用。如果应用程序的功能取决于这些文件,则应修改为将数据写入内部存储。

1. 请求外部存储权限

使用外部存储之前必须申请以下权限:

  • READ_EXTERNAL_STORAGE

允许应用访问外部存储设备中的文件。

  • WIRTE_EXTERNAL_STORAGE

允许应用写入和修改外部设备中的文件。拥有此权限的应用程序也会自动获取到 READ_EXTARNAL_STORAGE 权限。

从 Android 4.4 (API 级别 19)开始,在特定于应用的目录中读写文件不需要任何与存储相关的权限。因此,如果您的应用程序支持 Android 4.3(API Level 18)及更低版本,并且您只想访问特定于应用程序的目录,则通过添加以下 maxSdkVersion 属性来声明仅在较低版本的 Android 上请求权限。

        

注意:如果您的应用程序仅使用内部存储,则不需要声明任何与存储相关的权限,除非需要访问其他应用程序文件。

2. 验证外部存储是否可用

由于外部存储可能被移除,所以外部存储并不总是可用,因此在访问之前,始终应该验证是否可用。可以通过查询外部存储状态 getExternalStorageState()。如果返回状态为 MEDIA_MOUNTED,则可以读取和写入文件。如果是 MEDIA_MOUNTED_READ_ONLY,则只能读取文件。

/* Checks if external storage is available for read and write */public boolean isExternalStorageWritable() {    String state = Environment.getExternalStorageState();    if (Environment.MEDIA_MOUNTED.equals(state)) {        return true;    }    return false;}/* Checks if external storage is available to at least read */public boolean isExternalStorageReadable() {    String state = Environment.getExternalStorageState();    if (Environment.MEDIA_MOUNTED.equals(state) ||        Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) {        return true;    }    return false;}
保存到公共目录

如果要将文件保存在其他应用程序可以访问的外部存储上,可以使用以下 API:

  • 保存照片、音频或视频剪辑,请使用 MediaStore API。

  • 要保存其他文件(如 PDF),请使用 ACTION_CREATE_DOCUMENT Intent,它是 Storage Access Framework 的一部分。

注意,如果想在“媒体扫描器”中隐藏文件,可以在特定应用程序的目录中包含一个名为 .nomedia 的空文件。这样便可以防止媒体扫描器读取您的媒体文件并通过 MediaStore API 将它们提供给其他应用程序。

保存到私有目录

注意外部存储虽然也有应用私有目录,但是它并不向内部存储那样安全,并且也会跟随应用的卸载而被移除。

如果需要保存一些不是那么敏感的应用私有数据到外部存储上,可以使用 getExternalFilesDir() 或 getExternalCacheDir() 获取应用程序在外部存储的私有目录,它们在功能上类似于内部存储的 getFileDir() 和 getCacheDir()。

public File getPrivateAlbumStorageDir(Context context, String albumName) {    // Get the directory for the app's private pictures directory.    File file = new File(context.getExternalFilesDir(            Environment.DIRECTORY_PICTURES), albumName);    if (!file.mkdirs()) {        Log.e(LOG_TAG, "Directory not created");    }    return file;}

注意,如果预定义的子目录(Environment.DIRECTORY_PICTURES)都不合适作为要存储的目录,那么可以使用 getExternalFilesDir(null) 返回应用程序在外部存储上私有目录的根目录。

在多个外部存储之间进行选择

当设备也插入外置 SD 卡时,此时设备便有两个不同的外部存储目录,因此将“私有”文件写入外部存储时,需要选择使用哪个目录。

从 Android 4.4(API Level 19) 开始,可以通过 getExternalFilesDirs() 来访问这些目录,它返回一个 File 数组,数组的第一条被默认为是主要的外部存储,应用应该优先使用该位置作为第一外部存储(除非空间已满)。

或者使用 ContextCompat.getExternalFilesDir() 来兼容 Android 4.3 及更低版本。需要注意,在 4.3 及更低版本该方法仅返回一个主外部存储目录,即在 Android 4.3 及更低的版本无法访问第二个存储位置。


最后

随着 Android 手机硬件配置的提升,内置存储空间越来越大;出于兼容低端机的考虑,我们还是不能简单粗暴的将数据迁入内部存储。综合来看,迁移还需慢慢做,外部存储的敏感数据加密混淆也要配合。

Android 的权限管理只防君子不防小人,SD 卡存储读写权限只要应用申请了基本都可以获取到。所以建议对于敏感的外置存储的文件进行加密或混淆处理。剩下的等待明年的 Android 11 相对更完善的外部私有存储空间进行权限隔离控制。

Android 10 计划将外部(私有)存储的共享权限彻底收回,不过对 native 支持的不好,及一次性改变太大开发者对其反映强烈,计划被推迟一年。具体可以参考 Android 10 功能和 API。

便于大家理解,最后我将数据存储目录的相关路径整理出,大家需要对照查看发现它们的异同之处:

Android 权限管理 — 只防君子不防小人_第3张图片

我们也曾在项目中遇到存储敏感数据设计上的问题,本文正好借此来跟大家聊一聊,文中如有不妥或有更好的分析结果欢迎您的指正。

文章如果对你有帮助,请留个赞吧!


扩展阅读

Android 存储优化系列专题
Android 之不要滥用 SharedPreferences(上)
Android 对象序列化之 Parcelable 取代 Serializable ?
Android 存储选项之 SQLite 优化那些事儿

其他系列专题

深入 Activity 三部曲(1)View 绘制流程之 setContentView() 到底做了什么 ?
深入 Activity 三部曲(2)View 绘制流程之 DecorView 添加至窗口的过程
深入 Activity 三部曲(3)之 View 绘制流程
关于 UI 渲染,你需要了解什么?
Android 之如何优化 UI 渲染(上)
Android 之你真的了解 View.post() 原理吗?
Android 之 ViewTreeObserver 全面解析

更多相关文章

  1. freetype 在android编译时上的一个makefile文件
  2. Android应用程序开发的五大概念
  3. Android应用程序内存分析-Memory Analysis for Android Applicat
  4. 面试例题1:如何在android应用程序的窗口上绘制图形
  5. Android 数据存储(二) 文件的使用
  6. android保存文件到手机内存
  7. 安卓布局文件中控件的各种属性的含义

随机推荐

  1. 鼠标悬停,向前和向后翻页,实例演示 xhr对
  2. 轮播图:1: 实现鼠标悬停时自动停止播放,
  3. 购物车实例演示
  4. Androidの通过查询电话号码或ID获取联系
  5. Android(安卓)监听开机广播打开APP
  6. Android之路——第一个Android小程序(Andr
  7. android List拖动时背景变化
  8. android电话服务
  9. 文章收集
  10. Android多点触控揭秘