Android深入学习之数据存储

jkouu 238 0

在Android开发的过程中,数据的本地存储总是绕不开的一个场景。从android 10版本开始,谷歌为了规范Android开发过程中的数据存储,推出了"分区存储"的概念。这个变化出现后,很多新人(也包括我)会突然发现文件存储的权限变得特别乱,自己的脑海中没有一个清晰的体系架构在里面。所以这篇文章我们就来系统地学习一下Android的数据存储。

1.存储空间的划分

1.1.内部存储和外部存储

首先我们要明白Android是怎么对存储空间进行划分的。Android将设备的存储空间简单分成了两大部分:内部存储和外部存储。这个概念提出的时候,大多数手机的存储空间是分为两个部分的,一个是手机内部自带的存储空间,一个是我们可以自己扩展的MIcro SD卡。这个我相信大家都有过使用按键手机和板砖机的经历,应该都还有印象。对于手机内部自带的存储空间,Android就将其称为内部存储;对于Micro SD卡,Android就将其称为外部存储。

当然,对于现在的手机来说,Micro SD卡基本上不常见了,因为厂商将存储介质内置到了手机当中,不给我们扩展的选择了。但是,这并不意味着就没有外部存储了,内置存储卡虽然叫内置,但实际上还是一个外部存储介质,只不过它不对外暴露了,所以它就是现在手机中的外部存储空间。

1.2.两个存储空间的路径

那么内部存储空间和外部存储空间的路径都是什么呢?那就要从Android的文件系统来说了。

Android深入学习之数据存储

上面是Android的文件系统。我们知道,Android是基于Linux的,所以它的文件系统和Linux的文件系统一样,都是树形结构。从根节点往下,有三个子节点——data、system和storage。我们一个个说。

data节点往下的盘区就是Android的内部存储空间了。所以一个app的内部存储目录的路径应该是:/data/user/0/app的包名。

system节点是系统所使用的存储空间。严格来说,它也是内部存储空间的一部分。手机厂商预装的应用的apk就放在这里。Android会自动将这里存储的apk安装在手机中(所以系统应用基本上删不掉,即使你删了它也会自动安装上去,除非它在安装完成后自动把这里的apk删掉)。

storage节点,顾名思义,就是外部存储空间的根节点。这个节点下主要有两个节点——sdcard1和emulated。对于使用外置Micro SD卡的手机来说,SD卡会挂载到sdcard1节点下;对于使用内置存储卡的手机来说,存储介质会挂载到emulated节点下。一般来说,设备的根节点就是0。所以我们可以把0为根节点的子树整个看作是外部存储空间所存储的数据,即外部存储空间的根目录为/storage/emulated/0。

顺便说一句,在不root的情况下,我们只能遍历0为根节点的子树,也就是外部存储空间。data和system我们是无法在不root的情况下通过文件管理器看到的。

1.3.两个存储空间的特点

1.3.1.内部空间

内部空间最大的特点就是安全。这个安全是随着Andoird的发展越来越严格的。

显然,对于一个app来说,它可以随意在内部空间进行数据读写。通过下面两个api,app可以获得自己在内部存储空间的两个目录:

String fileFolderPath = context.getFilesDir() /data/user/0/app包名/files
String cacheFolderPath = context.getCacheDir() /data/user/0/app包名/cache

当然,我们也可以只获取到app在内部存储空间根节点路径:

String dataDir = getApplicationInfo().dataDir

在Android 7.0以前,app的内部存储空间也是可以对外开放的。应用在内部存储空间创建一个文件时,可以通过openFileOutPut()方法来对该文件的权限进行设置:

  String filename = "myfile";
    String fileContents = "Hello world!";
    FileOutputStream outputStream;

    try {
        outputStream = openFileOutput(filename, Context.MODE_WORLD_WRITEABLE);
        outputStream.write(fileContents.getBytes());
        outputStream.close();
    } catch (Exception e) {
        e.printStackTrace();
    }

Context的参数预设值包括MODE_WORLD_WRITEABLE、MODE_WORLD_READABLE和MODE_PRIVATE三个值,分别允许文件可被其它app读写、被其它app读和禁止其它app访问。

从Android 7.0开始,MODE_WORLD_WRITEABLE、MODE_WORLD_READABLE两个参数被弃用,如果再使用它们会直接抛出SecurityException。作为替代,如果一个app想将自己的内部存储文件共享给其它app,需要通过FileProvider来实现。FileProvider是ContentProvider的一个实现,具体使用方法这里就不多着墨了。

1.3.2.外部存储空间

外部存储空间的特点是大。因为是外部存储介质,空间是远比手机自带的存储空间大的。对于体积比较大的应用,资源紧张的内部存储空间可能不是一个特别好的选择,那么我们就可以通过修改AndroidManifest.xml的manifest标签,添加属性:android: installlocation = "auto"/"preferExternal",将其安装在外部存储空间。

当然,因为是外部存储,安全性自然就默认不如内部存储高。外部空间的权限管理是一个比较大的内容,我们在下面单独展开说。

2.外部存储的管理

2.1.公共目录和私有目录

Andoird系统对外部存储进一步做了划分,将其分成了公共目录和私有目录两部分。

私有目录,顾名思义,是app保存在外部存储空间中私有的部分。我们再回头看看上面那张文件系统图,/storage/emulated/0/Android目录下有一个data目录,这个目录下的空间就是外部存储的私有目录区。在这里,每一个app都会有一个以包名命名的目录(即/storage/emulated/0/Android/data/app包名)。虽然叫私有目录,它并不是私有的,它允许别的应用进行访问和修改(前提是要知道这个应用的包名)。

公共目录是直接位于/storage/emulated/0目录下的目录,这些目录包括Android系统默认创建的DownLoad、Photo、Music、DCIM、Documents等目录,也包括app在这里创建的目录。

公共目录和私有目录的区别在于,当app被卸载时,app在私有目录下的数据都会被删除,而公共目录下的数据则仍然存在(所以即使我们卸载了app,我们还是会在手机中找到这些app的残留文件夹)。同时,在app的系统设置界面,我们可以通过点击“清除应用数据”键来人为清除app在私有目录下的files和cache两个文件夹的内容(事实上,手机清理的原理就是挨个去清理每个app的私有目录。因为针对的是私有目录,所以无论你清了多少次,公共目录下的应用残留还是会存在)。最后,在私有目录下的文件不会被系统扫描到,即如果把图片保存到了私有目录下,除非手动通知系统将其同步,否则我们不会在相册里看到它。

2.2.两个目录的访问

在Android 4.4以前,应用无论是访问自己的私有目录还是设备的公共目录,都需要申请权限,就是我们都熟知的READ_EXTERNAL_STORAGE 或 WRITE_EXTERNAL_STORAGE。从Android 4.4开始,只有访问公共目录才需要申请这两个权限了。

2.2.1.检查外部存储状态

无论是访问公共目录还是私有目录,在访问前最好要做一次外部存储的检查,因为外部存储是可变的(比如一些手机的外置Micro SD卡卡槽也可以插SIM卡),虽然现在绝大多数手机都是内置存储卡,但是从逻辑上来说,检查外部存储状态是有必要的。

String state = Environment.getExternalStorageState();

getExternalStorageState()这个函数的返回值有10种,分别表示了外部存储的不同状态,具体见下表:

返回值 意义
MEDIA_BAD_REMOVAL 存储设备在没有完成解挂的情况下被拔出,类似于我们直接把USB从电脑上拔出来
MEDIA_CHECKING 顾名思义,存储设备存在且正在被扫描
MEDIA_EJECTING 设备正在被弹出,类似于电脑上的安全弹出设备
MEDIA_MOUNTED 设备已挂载,可以正常读写
MEDIA_MOUNTED_READ_ONLY 设备已挂载,但是只能读
MEDIA_NOFS 设备存在但是无法挂载,通常是文件系统不兼容
MEDIA_REMOVED 设备不存在
MEDIA_SHARED 设备并非挂载在手机上,而是通过USB连接
MEDIA_UNKNOWN 设备类型未知
MEDIA_UNMOUNTABLE 设备存在但是无法挂载,通常是文件系统崩了
MEDIA_UNMOUNTED 设备存在但是还没有完成挂载

在检查了存储设备后,就可以进行外部存储的读写了。

2.2.2.访问外部存储的私有目录

对于外部存储的私有目录,可以说安全级别是不算低的。一个应用想要访问另外一个应用的私有目录,要么要获取到root权限,要么要另外一个应用通过ContentProvider来共享。但是应用自己在自己的私有目录下创建和管理文件还是非常方便的。

String fileFolderPath = context.getExternalFilesDir()
String cacheFolderPath = context.getExternalCacheDir()

上面是获取外部存储的私有目录路径的两个方法。需要注意的是,这两个方法返回的是手机内置的外部存储。也就是说,如果你的手机既有内置存储卡,又扩展了一张Micro SD卡,那么这两个方法是不会返回Micro SD卡的路径的。如果你需要,那么可以使用下面两个方法:

String fileFolderPath = context.getExternalFilesDirs()
String cacheFolderPath = context.getExternalCacheDirs()

这两个方法返回的是一个数组,数组包括了所有外部存储的路径。数组的第一个仍然是内置存储卡的路径,后面的就是Micro SD卡的路径了。

2.2.3.访问外部存储的公共目录

对于外部存储的公共目录,在Android 10以前,安全级别是不高的,所有的应用都可以通过file://+文件路径的形式在公共目录下随意访问文件夹和文件(所以公共目录下的东西十分混乱而且不安全)。

从Android 10开始,公共目录的读和写都做了严格的规范。Android 10开始,规范的访问文件的途径只有MediaStore和SAF两种。因为Android 10以后,系统不再支持file://访问,只支持uri访问。uri的来源就是这两种途径(其实也可以自己写程序手动实现绝对路径和uri的转换,但是既然有现成的框架为啥不用呢)。这么做的意义是,谷歌实际上是希望所有应用把公共目录下的数据都存到Image、Download等系统预创建的文件夹下,保持公共目录的整洁(虽然基本上没有人按照谷歌想的走)。

这里要说明的是,Android10和11目前都还支持兼容模式,即可以用以前的file://+文件路径的方法随意创建文件,只需要在AndroidManifest.xml文件的application标签中添加"requestLegacyExternalStorage=true"(Android 11是“preserveLegacyExternalStorage

=true”,但是这和10还不一样。如果你的应用卸载重装了,就不能再用旧的方式了。)即可。但是这是一时的办法,后面的版本中随时会取消,所以还是建议大家按照分区存储的规则来做。

首先是写入。写入的权限很简单,所有应用只能修改自己创建的文件,想要修改别的应用创建的文件,要么申请MANAGE_EXTERNAL_STORAGE权限(当然这是有限制的,所有申请了此权限的应用在Google Play上架前都需要经过谷歌的审核。不知道国内的平台有没有这个操作),要么需要其它应用通过ContentProvider来分享文件。

然后是读取,Android 10同样在权限上做了规范。现在,除非应用是系统的默认应用,否则它只能写入或删除自己创建的文件,对于别的应用创建的文件只有读权限。

3.SAF和MediaStore

3.1.SAF

SAF的全称为Storage Access Framework,是Android提供一个访问文件的服务。通过SAF,我们可以访问公共目录下所有类型文件。下面是一个简单的例子:

private void openFileWithSAF(){
    int resultCode = 1; //设置返回码

    Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); //指定操作类型为打开文件,对于创建文件使用Intent.ACTION_CREATE_DOCUMENT
    intent.addCategory(Intent.CATEGORY_OPENABLE); //设置过滤规则,这里的意思是只显示能打开的文件
    intent.setType("*/*"); //设置筛选后的文件类型,这里是全选
    startActivityForResult(intent, resultCode);
}

@Override
public void onActivityResult(int requestCode, int resultCode,Intent resultData) {
    //判断响应码
    if(requestCode == 1){
        //返回结果判空
        if(resultData != null){
            try {
                Uri uri = resultData.getData();
                //获取输入输出流,之后就可以读写文件了
                InputStream inputStream = getContentResolver().openInputStream(uri);
                OutputStream outputStream = getContentResolver().openOutputStream(uri);
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            }
        }
    }

    super.onActivityResult(requestCode, resultCode, resultData);
}

Android 5.0版本开始,SAF还提供了选择某一个文件夹的服务,只需要将Intent.ACTION_OPEN_DOCUMENT改成Intent.ACTION_OPEN_DOCUMENT_TREE就可以了。

这里要说一下,SAF实际上是让系统针对特定的URI对应用授权。这个授权是暂时的,如果设备重启了,那么授权就无效了。想要获得长期的权限,还需要添加下面的代码:

final int takeFlags = intent.getFlags()
            & (Intent.FLAG_GRANT_READ_URI_PERMISSION
            | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
getContentResolver().takePersistableUriPermission(uri, takeFlags);

因为是针对URI的授权,所以当文件移动了位置,URI发生了变化,那么即使申请了长期权限也只能再重新获得授权。

3.2.MediaStore

MediaStore实际上是Android系统的一个数据库,它不仅存储了多媒体文件的URI,还存储了多媒体文件的一些属性值。我们可以通过ContentResolver来对数据库进行查找,获得我们需要的多媒体文件的URI。当然,MediaStore所提供的结果可能是不完全的,比如私有目录中的多媒体文件如果不认为同步的话,MediaStore是不会收录的。除此之外,还可以对在Photo、Audio、Vedio这些文件夹内创建.nomedia文件夹。这个文件夹内的多媒体文件不会被MediaStore收录。

下面谷歌给出的一个使用MediaStore的例子,场景为查找时长大于等于5分钟的视频:

// Need the READ_EXTERNAL_STORAGE permission if accessing video files that your
// app didn't create.

// Container for information about each video.
class Video {
    private final Uri uri;
    private final String name;
    private final int duration;
    private final int size;

    public Video(Uri uri, String name, int duration, int size) {
        this.uri = uri;
        this.name = name;
        this.duration = duration;
        this.size = size;
    }
}
List<Video> videoList = new ArrayList<Video>();

String[] projection = new String[] {
    MediaStore.Video.Media._ID,
    MediaStore.Video.Media.DISPLAY_NAME,
    MediaStore.Video.Media.DURATION,
    MediaStore.Video.Media.SIZE
};
String selection = MediaStore.Video.Media.DURATION +
        " >= ?";
String[] selectionArgs = new String[] {
    String.valueOf(TimeUnit.MILLISECONDS.convert(5, TimeUnit.MINUTES));
};
String sortOrder = MediaStore.Video.Media.DISPLAY_NAME + " ASC";

try (Cursor cursor = getApplicationContext().getContentResolver().query(
    MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
    projection,
    selection,
    selectionArgs,
    sortOrder
)) {
    // Cache column indices.
    int idColumn = cursor.getColumnIndexOrThrow(MediaStore.Video.Media._ID);
    int nameColumn =
            cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DISPLAY_NAME);
    int durationColumn =
            cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DURATION);
    int sizeColumn = cursor.getColumnIndexOrThrow(MediaStore.Video.Media.SIZE);

    while (cursor.moveToNext()) {
        // Get values of columns for a given video.
        long id = cursor.getLong(idColumn);
        String name = cursor.getString(nameColumn);
        int duration = cursor.getInt(durationColumn);
        int size = cursor.getInt(sizeColumn);

        Uri contentUri = ContentUris.withAppendedId(
                MediaStore.Video.Media.EXTERNAL_CONTENT_URI, id);

        // Stores column values and the contentUri in a local object
        // that represents the media file.
        videoList.add(new Video(contentUri, name, duration, size));
    }
}

 

 

 

发表评论 取消回复
您必须 [登录] 才能发表评论!
分享