Ryan Life is Simple

Android中的内部存储和外部存储

2019-08-10

内部存储和外部存储的概念随着Android版本的更新也在发生不断的变化。最早的内部存储指的是系统自带的ROM存储,外部存储指的是外置的Sdcard或者通过OTG挂在的USB存储。随着硬件价格越来愈低,系统内置的ROM也越来越大,现在即使是一台千元机,一般存储容量也是在16GB以上。所以在Android 4.4以后,系统将内部存储分为了Internal Storage和External Storage。而原来的外置Sdcard或者通过OTG挂载的USB,也都是属于External Storage。

内部存储(Internal File Storage)

内部存储指的是应用内部独有的存储,这部分存储的文件、数据,只能被应用自身访问到,其他应用都没有权限访问。

一般情况下,/data开头的路径都是Internal Storage。而一般应用所能够访问到的就是下面几个路径,称为应用内部私有存储。

应用内部私有存储:
/data/user/0/<包名>

/data/user/0/<包名>/files #存放文件数据

/data/user/0/<包名>/databases #存放Sqlite的数据库文件

/data/user/0/<包名>/shared_prefs #存放SharedPreference的数据

/data/user/0/<包名>/cache #存放缓存文件

需要注意的是,在支持多用户之前,应用内部私有存储的路径是/data/data/<包名>,在Android L系统引入多用户功能之后,每个用户对于每个应用都会有一个独立的存储空间,而这个路径等价于机主用户的应用存储空间也就是/data/user/0/<包名>。其他用户比如访客用户,就会以/data/user/10/<包名>路径进行存储。

由于应用内部私有存储的路径都是与应用包名强相关的,所以在应用内部访问这些路径都是通过Context相关API去访问的。

应用内部私有存储路径 获取路径的API 说明
/data/user/0/<包名> Context#getDataDir 应用内部私有数据存储根目录
/data/user/0/<包名>/files Context#getFilesDir 存放文件
/data/user/0/<包名>/cache Context#getCacheDir 存放缓存文件,在设备内存不足时,系统可能会删除以回收空间
/data/user/0/<包名>/foldername Context#getDir 存放自定义文件
/data/user/0/<包名>/databases 数据库相关API 存放数据库文件
/data/user/0/<包名>/shared_prefs SharedPreference相关API 存放SharedPreference的xml文件

应用内部私有存储的特点

  1. 存储永远都是处于可用状态的
  2. 只有App自己才能够访问保存的文件
  3. 一旦App被卸载,系统将会移除内部存储中相关应用的数据

文件读写实例
/data/user/0/<包名>/files 下面文件的读写,可以直接通过Context#openFileOutput写入 如下面的代码,在/data/user/0/<包名>/files 目录下,创建myfile文件,可以通过context.getFilesDir获取

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

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

存储一个内部cache文件

private File getTempFile(Context context, String url) {
    File file;
    try {
        String fileName = Uri.parse(url).getLastPathSegment();
        file = File.createTempFile(fileName, null, context.getCacheDir());
    } catch (IOException e) {
        // Error while creating file
    }
    return file;
}

外部存储(External File Storage)

外部存储指的是是公共的存储,这部分存储理论上是全局可见的,所有的应用都可以访问这部分数据,一般情况下,路径都是以/storage开头的,比如说/storage/emulated/0就是属于外部存储,这个路径的实际的挂载点是/data/media。又比如外置sdcard的路径为/storage/13FC-0F0B。 相比较内部存储一定会存在,外部存储可能是sdcard或者通过otg挂载的U盘形式,所以可能出现没有挂载的情况,所以所有的外部存储要在使用前通过下面的方式判断是否有被挂载。

检查外部存储是否可读写

/* 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;
}

外部存储可以分为两个部分,一个是外部应用私有存储,另外一个是外部公共存储。
应用访问外部存储需要通过下面的方式在AndroidManifest.xml中申请动态权限,其中WRITE_EXTERNAL_STORAGE包含了读跟写的权限。不过在Android 4.4之后,访问外部应用私有存储不需要进行权限的申请。

<manifest ...>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    ...
</manifest>

应用外部私有存储

这部分存储的所有权属于应用,一般不会主动向外部提供数据,虽然应用可以通过类似绝对路径这样的方式访问到。 这部分数据会在应用卸载时会被清除,主要涉及的路径见下面的表格:

应用外部私有存储路径 获取路径的API 说明
/storage/emulated/0/Android/data/包名/files/Music Context#getExternalFilesDir() 包含如Music,Pictures等文件夹
/storage/emulated/0/Android/data/包名/cache Context#getExternalCacheDir 外部缓存文件
/storage/emulated/0/Android/media/<包名>
/storage/13FC-0F0B/Android/media/<包名>
Context#getExternalMediaDirs 如果有外部Sdcard,会有对应路径

存储到私有路径实例 可以通过在文件夹目录下添加.nomedia防止该目录下的文件被MediaScanner扫描到

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;
}

外部共有存储

这部分存储一般指的是/storage/emulated/0下面也就是/data/media挂载出来的)除去/storage/emulated/0/Android之外的所有路径,比如storage/emulated/0/Pictures这些文件夹。也包括外置sdcard,通过OTG挂载USB出来的存储,一般路径为/storage/xxxx-xxxx.

外部共有存储的路径跟应用本身没有关系,所以一般都是通过Environment类进行获取。而这些存储的数据也不会随着应用的卸载而丢失。

外部共有存储路径 获取路径的API 说明
/storage/emulated/0 Environment.getExternalStorageDirectory()  
/storage/emulated/0/Pictures Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)  

获取图片公有目录,并在内部创建 albumName 文件夹

public File getPublicAlbumStorageDir(String albumName) {
    // Get the directory for the user's public pictures directory.
    File file = new File(Environment.getExternalStoragePublicDirectory(
            Environment.DIRECTORY_PICTURES), albumName);
    if (!file.mkdirs()) {
        Log.e(LOG_TAG, "Directory not created");
    }
    return file;
}

应用清除数据和清除缓存

在设置->应用->应用详情->存储页面下面有”清除数据“与”清除缓存”两个菜单,点击之后分别删除了哪些文件呢?

  1. 清除数据
    这个操作将会清除
    /data/user/0/<包名>/ /storage/emulated/0/Android/data/<包名>/ 权限授予数据

  2. 清除缓存 点击清除缓存之后,将会对下面路径下的缓存文件进行清除
    /data/user/0/<包名>/cache /storage/emulated/0/Android/data/<包名>/cache

关于外置Sdcard

外置Sdcard由于传输速率等原因,虽然到目前还支持,但也逐渐开始被Google放弃,很多厂商也开始不在支持外置Sdcard。

外置SDcard在Android M之后支持两种挂载方式

Portable Storage

这种挂载方式是Android最早支持的外置sdcard的方式,sdcard存粹作为可移动存储的方式进行存储,一般用来存储照片、音乐这样的多媒体文件。外置存储将会以/mnt/media_rw/的形式挂载到系统中
Filesystem                     Size  Used Avail Use% Mounted on
… …
/data/media                     24G  3.9G   20G  17% /mnt/runtime/default/emulated
/dev/block/vold/public:179,129 941M   64K  941M   1% /mnt/media_rw/35B6-0CFA
/mnt/media_rw/35B6-0CFA        941M   64K  941M   1% /mnt/runtime/default/35B6-0CFA

如果通过File接口直接在外置sdcard路径下去创建文件,会报下面的错误。
08-10 20:51:04.033 24575 24575 W System.err: java.io.IOException: Permission denied
08-10 20:51:04.033 24575 24575 W System.err: at java.io.UnixFileSystem.createFileExclusively0(Native Method)
08-10 20:51:04.034 24575 24575 W System.err: at java.io.UnixFileSystem.createFileExclusively(UnixFileSystem.java:281)
08-10 20:51:04.034 24575 24575 W System.err: at java.io.File.createNewFile(File.java:1008)
08-10 20:51:04.034 24575 24575 W System.err: at com.ryan.demostore.storagedemo.StorageDemoFragment$17.onClick(StorageDemoFragment.java:337)
08-10 20:51:04.034 24575 24575 W System.err: at android.view.View.performClick(View.java:6597)
08-10 20:51:04.034 24575 24575 W System.err: at android.view.View.performClickInternal(View.java:6574)
08-10 20:51:04.034 24575 24575 W System.err: at android.view.View.access$3100(View.java:778) 08-10 20:51:04.034 24575 24575 W System.err: at android.view.View$PerformClick.run(View.java:25885)
08-10 20:51:04.034 24575 24575 W System.err: at android.os.Handler.handleCallback(Handler.java:873)
08-10 20:51:04.035 24575 24575 W System.err: at android.os.Handler.dispatchMessage(Handler.java:99)
08-10 20:51:04.035 24575 24575 W System.err: at android.os.Looper.loop(Looper.java:193) 08-10 20:51:04.035 24575 24575 W System.err: at android.app.ActivityThread.main(ActivityThread.java:6669)
08-10 20:51:04.035 24575 24575 W System.err: at java.lang.reflect.Method.invoke(Native Method) 08-10 20:51:04.035 24575 24575 W System.err: at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:493)
08-10 20:51:04.036 24575 24575 W System.err: at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:858)
在Android P之前,带有平台签名的应用在申请android.permission.WRITE_MEDIA_STORAGE权限之后,可以读写外置sdcard文件,而在Android P之后,由于权限策略的修改,即使是平台签名应用也无法读写外置sdcard。所有应用都需要通过DocumentFile的方式去读写。

Adoptable storage

这种挂载方式会讲外置sdcard格式化为加密的存储,替代原先ROM中挂载出来的/data/media,这种sdcard的数据无法被其他机器识别。如下面所示/mnt/expand/735f55e1-4a68-44aa-885c-e04c18cb8534/media将会代替原先/data/media成为系统中的Primary外置存储。

/dev/block/dm-2                                         24G  3.9G   20G  17% /data
/dev/block/dm-3                                        895M  1.6M  893M   1% /mnt/expand/735f55e1-4a68-44aa-885c-e04c18cb8534
/mnt/expand/735f55e1-4a68-44aa-885c-e04c18cb8534/media 895M  1.6M  893M   1% /mnt/runtime/default/emulated

所有的存储在data/media应用数据将会迁徙到sdcard上,同时原先ROM中的外部存储空间将会被废弃掉,只能被用户以应用的数据进行填充,无法被用户作为存储如MTP这样的形式使用。这种方式的体验并不好,因为在一般情况下,外置sdcard的读写速度是比不上ROM的存储。即使是EMMC的Flash,读写速度也要比一般的外置sdcard快的多,如果使用外置sdcard作为Primary存储,很容易造成系统的卡顿。

尾记

随着Android版本的更新,存储权限的控制越来越精细,就像在Android P上,应用无法通过file:///这的uri直接共享文件,否则会爆出FileUriExposedException的错误。

据说在Android Q平台,Google对于应用存储的权限做了比较大的变更。传统的 READ_EXTERNAL_STORAGE/WRITE_EXTERNAL_STORAGE 读写权限已经被更加细化的权限替代了,READ_MEDIA_IMAGES 和 READ_MEDIA_VIDEO 分别对应读取图片和视频,授权是会作为一组权限同时授予,而 READ_MEDIA_AUDIO 读取音频权限为单独一组授予,它们会控制应用通过 MediaStore 读取媒体的能力。(此段摘自 https://feng.moe/archives/47/)

参考链接

https://developer.android.com/guide/topics/data/data-storage
https://feng.moe/archives/47/


Comments

Content