Android存储

2,361 阅读8分钟

Android的存储区域空间

Android的存储区域包括:内部存储(Internal Storage)和外部存储(External Storage)。

1 内部存储

内部存储分区:物理位置主要包括了Android系统根目录下的/data、/System、/cache等目录。

  • Root的手机可以通过文件管理软件查看内部存储下文件;
  • 正常的手机我们可以借助AndroidStudio的Device File Explorer工具来查看内部存储下文件。

内部存储分区的特点:

  1. 内部分区总是可用。
  2. 它存放App私有文件,并且不可被其他App访问。
  3. App卸载后,存储在内部分区上的该App数据将会被清除。
  4. 不需要额外申请权限。

1.1 /system目录:

/system

系统存放目录,它和/sdcard以及/data是同级的,是存储根目录的一级子目录。

image.png

访问方式:可以通过Environment类的getRootDirectory方法访问

    private static final String ENV_ANDROID_ROOT = "ANDROID_ROOT"; //环境变量
    private static final File DIR_ANDROID_ROOT = getDirectory(ENV_ANDROID_ROOT, "/system");//如果环境变量指定了,则使用指定值,否则使用"/system"
    public static @NonNull File getRootDirectory() {
        return DIR_ANDROID_ROOT;
    }

它下面对应一些子目录:

  • /system/app:存放rom本身附带的软件即系统软件。
  • /system/data:存放/system/app中,核心系统软件的数据文件信息。
  • /system/priv-app:存放手机厂商定制的系统级别的应用的apk文件。
  • /system/bin:存放系统的本地程序,里面主要是Linux系统自带的组件。
  • /system/media:存放一些音效、铃声、开关机动画等。

1.2 /data目录:

/data

data目录,我们App私有数据存储的顶级目录。

image.png

访问方式:可以通过Environment.getDataDirectory()方法访问

    private static final File DIR_ANDROID_DATA = getDirectory(ENV_ANDROID_DATA, "/data");
    /**
     * Return the user data directory.
     */
    public static File getDataDirectory() {
        return DIR_ANDROID_DATA;
    }

1.2.1 应用程序私有目录

/data/data/<app包名>

image.png

访问方式:可通过Context对象的getDataDir()方法来获取,在开发时,通常我们不应该直接使用该目录,而应该使用file、cache等系统已经定义好的目录

ContextImpl中源码:

    @Override
    public File getDataDir() {
        if (mPackageInfo != null) {
            File res = null;
            if (isCredentialProtectedStorage()) {
                res = mPackageInfo.getCredentialProtectedDataDirFile();
            } else if (isDeviceProtectedStorage()) {
                res = mPackageInfo.getDeviceProtectedDataDirFile();
            } else {
                res = mPackageInfo.getDataDirFile(); //mPackageInfo是LoadedApk的对象。
            }
            ……
        } else {
            throw new RuntimeException(
                    "No package details found for package " + getPackageName());
        }
    }

它下面对应一些子目录:

  • /data/data/<app包名>/files:App内部存储的文件目录。
  • /data/data/<app包名>/cache:App内部存储的缓存目录。
  • /data/data/<app包名>/databases:App内部存储的数据库目录。
  • /data/data/<app包名>/shared_prefs:App内部存储的SP目录。
  • /data/data/<app包名>/lib:App内部存储的lib目录。

1.2.2 应用程序files目录

/data/data/<app包名>/files

files目录是我们App内部存储的文件目录。

访问方式:可通过Context对象的getFilesDir()方法可以获得应用私有目录的file目录。

我们对文件操作常用的方法,Context对象的openFileOutput()方法的文件根目录地址就是files目录。

ContextImpl中的源码:

    @Override
    public File getFilesDir() {
        synchronized (mSync) {
            if (mFilesDir == null) {
                mFilesDir = new File(getDataDir(), "files");
            }
            return ensurePrivateDirExists(mFilesDir);
        }
    }

1.2.3 应用程序cache目录

/data/data/<app包名>/cache

cache目录是我们App内部存储的缓存目录。

访问方式:可通过Context对象的getCacheDir()方法可以获取cache目录。

ContextImpl中的源码:

    @Override
    public File getCacheDir() {
        synchronized (mSync) {
            if (mCacheDir == null) {
                mCacheDir = new File(getDataDir(), "cache");
            }
            return ensurePrivateCacheDirExists(mCacheDir, XATTR_INODE_CACHE);
        }
    }

如果您想暂时保留而非永久存储某些数据,则应使用特殊的缓存目录来保存这些数据。不应依赖系统为您清理这些文件,而应始终自行维护缓存文件,使其占用的空间保持在合理的限制范围内(例如 1MB)。当用户卸载您的应用时,这些文件也会随之移除

cache文件有以下几个特点需要注意:

  1. 系统将在磁盘空间不足时自动删除此目录中的文件。
  2. 系统将始终首先删除旧文件。
  3. 我们可以使用StorageManager类的相关方法更好的管理我们的删除规则。
  4. App所占缓存空间的大小可以通过StorageManager.getCacheQuotaBytes(java.util.UUID)来获得。
  5. 超过App所分配限额的缓存空间将被优先删除,我们应该尽可能的使我们的cache空间内的文件低于限额值,这会使得我们的cache文件最大可能的减少被删除的概率。

1.2.4 应用程序databases目录

/data/data/<app包名>/databases

databases目录存放了应用程序的数据库文件。

访问方式:可通过Context对象的getDatabasesDir()方法可以获取databases目录。

ContextImpl中的源码:

    private File getDatabasesDir() {
        synchronized (mSync) {
            if (mDatabasesDir == null) {
                if ("android".equals(getPackageName())) {
                    mDatabasesDir = new File("/data/system");
                } else {
                    mDatabasesDir = new File(getDataDir(), "databases");
                }
            }
            return ensurePrivateDirExists(mDatabasesDir);
        }
    }

该方法是一个私有方法,不能直接访问,我们通常使用DB的相关封装方法来进行访问。这里我们可以看到,如果应用程序的包名是"android",则DB的目录是"/data/system",否则,DB的目录是/data/data/<app包名>/databases。

1.2.5 应用程序shared_prefs目录

/data/data/<app包名>/shared_prefs

如果应用想存储一些数据量较小的键值对信息,可以使用SharedPreferences来保存数据,例如,一些应用相关的配置信息等。

通过Context对象的getSharedPreferences方法来进行访问操作。

访问方式:可通过getPreferencesDir()方法来进行获取,我们不能直接使用该方法。

ContextImpl中的源码:

    private File getPreferencesDir() {
        synchronized (mSync) {
            if (mPreferencesDir == null) {
                mPreferencesDir = new File(getDataDir(), "shared_prefs");
            }
            return ensurePrivateDirExists(mPreferencesDir);
        }
    }

1.3 /cache目录:

/cache

下载缓存内容目录,它和/system以及/data是同级的

访问方式:可以通过EnvironmentgetDownloadCacheDirectory方法访问

    private static final File DIR_DOWNLOAD_CACHE = getDirectory(ENV_DOWNLOAD_CACHE, "/cache");
    public static File getDownloadCacheDirectory() {
        return DIR_DOWNLOAD_CACHE;
    }

2 外部存储

很多人总是认为外部存储就是外置SD卡,这是误解。 外部存储可以是外置的SD卡,也可以是内置存储卡的部分分区。

外部存储可以分为公共目录私有目录。 如下图所示:根目录sdcard/算是公共目录;sdcard/Android/data/<包名>/下属于私有目录。

image.png

外部存储分区的特点:

  1. 外部分区并不总是可用。
  2. 保存在这里的文件可能被其他程序访问。
  3. 当用户卸载app时,系统仅仅会删除external中的缓存目录(Context.getExternalCacheDir())和file目录(Context.getExternalFilesDir())下的相关文件。
  4. 有些情况需要申请WRITE_EXTERNAL_STORAGE或READ_EXTERNAL_STORAGE权限。

看到上述特点,发现语气词都不太肯定,那是因为针对外部存储的使用,Google一直在新版本的是适配上在对其进行限制。下面我们会针对私有目录和公共目录详细说明。

2.1 外部公共存储目录

外部公共存储目录有以下特点:

  • 可以被其它应用程序访问
  • 用户卸载app后,资源可以保留
  • 权限?

受最新版AndroidR(11)限制,禁止在sd卡公共目录进行读写操作,媒体资源除外。

早期为了方便寻找APP的一些文件,很多开发者都是在根目录下创建一个到多个目录来存储程序的资源文件。一个APP这样做没问题,可一旦所有app都这样开发时,Android用户打开资源管理器就会发现sd卡上面密密麻麻的文件文件夹。

AndroidQ(10)开始执行分区存储。简单的说,就是应用对于文件的读写只能在沙盒环境,也就是属于自己应用的目录里面读写。其他媒体文件可以通过MediaStore进行访问。(Google还是为开发者考虑,留了一手。在targetSdkVersion = 29应用中,设置android:requestLegacyExternalStorage=“true”,就可以不启动分区存储,让以前的文件读取正常使用。)

AndroidR(11)开始强制执行分区存储。而且在所有的Android11的设备上,即便targetSdkVersion<30,也不允许sd卡根目录进行读写操作。 因此,需要把自己的文件放在外部私有存储目录,把需要保留的照片,录像等放在系统媒体文件公共存储目录下。

2.1.1 外部sd卡根目录

/storage/emulated/0

外部存储的sd卡根目录,也就是我们平时从文件管理器中能看到的最顶级目录

访问方式:可以通过Environment类的getExternalStorageDirectory方法访问:

    @Deprecated
    public static File getExternalStorageDirectory() {
        throwIfUserRequired();
        return sCurrentUser.getExternalDirs()[0];
    }

AndroidQ(10)以前在根目录的基础上可以自定义一些目录,目前已经禁止使用,不在讨论。

2.1.2 外部公共特殊目录

/storage/emulated/0/DCIM

/storage/emulated/0/Music

/storage/emulated/0/Movies

/storage/emulated/0/Pictures

/storage/emulated/0/Downloads

AndroidQ(10)以前,通过Environment的getExternalStoragePublicDirectory方法可以访问公共目录。 该方法有几种特定参数类型(参数不能为null):

     *            {@link #DIRECTORY_MUSIC}, 
     *            {@link #DIRECTORY_PODCASTS},
     *            {@link #DIRECTORY_RINGTONES}, 
     *            {@link #DIRECTORY_ALARMS},
     *            {@link #DIRECTORY_NOTIFICATIONS}, 
     *            {@link #DIRECTORY_PICTURES},
     *            {@link #DIRECTORY_MOVIES}, 
     *            {@link #DIRECTORY_DOWNLOADS},
     *            {@link #DIRECTORY_DCIM}, or 
     *            {@link #DIRECTORY_DOCUMENTS}

在AndroidQ(10)中,该接口已经废弃。替代方案建议使用Context.getExternalFilesDir、MediaStore、Intent.ACTION_OPEN_DOCUMENT。

AndroidQ(10)中引入了一个新定义媒体文件的共享集合,如果要访问沙盒外的媒体共享文件,比如照片,音乐,视频等,需要申请新的媒体权限:READ_MEDIA_IMAGES,READ_MEDIA_VIDEO,READ_MEDIA_AUDIO,申请方法同原来的存储权限。

2.2 外部私有存储目录

因内部存储空间有限,Android在外部存储空间中提供了特殊目录供app存放私有文件。

外部私有存储目录有以下特点:

  • 不能被其它应用程序访问
  • 用户卸载app后,资源不能保留
  • 权限?

2.2.1 外部私有存储根目录

/storage/emulated/0/Android/data/<app包名>

2.2.2 外部私有存储Files目录

/storage/emulated/0/Android/data/<app包名>/files

一般放一些长时间保存的数据。

访问方式:可以通过getExternalFilesDir()方法

ContextImpl中的源码:

    @Override
    public File getExternalFilesDir(String type) {
        // Operates on primary external storage
        final File[] dirs = getExternalFilesDirs(type);
        return (dirs != null && dirs.length > 0) ? dirs[0] : null;
    }

    @Override
    public File[] getExternalFilesDirs(String type) {
        synchronized (mSync) {
            File[] dirs = Environment.buildExternalStorageAppFilesDirs(getPackageName());
            if (type != null) {
                dirs = Environment.buildPaths(dirs, type);
            }
            return ensureExternalDirsExistOrFilter(dirs);
        }
    }

getExternalFilesDir方法的参数,表示将要创建的/files目录下的子目录。通过源码我们看到,它调用了Environment.buildExternalStorageAppFilesDirs(getPackageName())来获取/files目录。

buildExternalStorageAppFilesDirs根据外部存储的数量,返回的是一个File的数组;

getExternalFilesDir只取第一个,也就是主外部存储的目录。

Environment.buildExternalStorageAppFilesDirs方法:

    public static final String DIR_ANDROID = "Android";
    private static final String DIR_DATA = "data";
    private static final String DIR_MEDIA = "media";
    private static final String DIR_OBB = "obb";
    private static final String DIR_FILES = "files";
    private static final String DIR_CACHE = "cache";
    
    @UnsupportedAppUsage
    public static File[] buildExternalStorageAppFilesDirs(String packageName) {
        throwIfUserRequired();
        return sCurrentUser.buildExternalStorageAppFilesDirs(packageName);
    }

调用到Environment内部类UserEnvironmentbuildExternalStorageAppFilesDirs方法:

   public File[] buildExternalStorageAppFilesDirs(String packageName) {
       return buildPaths(getExternalDirs(), DIR_ANDROID, DIR_DATA, packageName, DIR_FILES);
   }

这里按照目录的父子关系,依次创建了相应的目录:Android、data、packageName目录、files。

2.2.3 外部私有存储Cache目录

/storage/emulated/0/Android/data/<app包名>/cache

一般存放临时缓存数据时使用。

访问方式:可以通过getExternalCacheDir()方法

ContextImpl中的源码:

    @Override
    public File getExternalCacheDir() {
        // Operates on primary external storage
        final File[] dirs = getExternalCacheDirs();
        return (dirs != null && dirs.length > 0) ? dirs[0] : null;
    }

    @Override
    public File[] getExternalCacheDirs() {
        synchronized (mSync) {
            File[] dirs = Environment.buildExternalStorageAppCacheDirs(getPackageName());
            return ensureExternalDirsExistOrFilter(dirs);
        }
    }

逻辑与files目录类似,最终调用到Environment内部类UserEnvironmentbuildExternalStorageAppCacheDirs方法。

    public File[] buildExternalStorageAppCacheDirs(String packageName) {
        return buildPaths(getExternalDirs(), DIR_ANDROID, DIR_DATA, packageName, DIR_CACHE);
    }