Android 中看不见的外部存储路径

戈壁老王
这个问题起源一个bug的分析过程,APP的cache路径无法通过adb进行访问。

基于Android 5.1代码进行分析

在 Android 应用中,获取存储路径的方法通常使用以下几个,

  • Environment.getDownloadCacheDirectory():/cache ,cache目录路径。
  • Environment.getRootDirectory():/system,system目录路径。
  • Environment.getDataDirectory():/data,内部存储的根路径。
  • Context.getFilesDir():/data/user/0/<packname>/files,内部存储中的files路径。
  • Context.getCacheDir():/data/user/0/<packname>/cache ,内部存储中的cache路径。
  • Context.getPackageCodePath():程序的安装包路径。
  • Context.getPackageResourcePath():当前应用程序对应的 apk 文件的路径。
  • Context.getDatabasePath(“xxx”):通过Context.openOrCreateDatabase 创建的数据库文件。
  • Context.getDir(“xxx”, Context.MODE_PRIVATE):获取某个应用在内部存储中的自定义路径。
  • Context.Environment.getExternalStorageDirectory():/storage/emulated/0,获得外部存储路径。
  • Context.Environment.getExternalStoragePublicDirectory(“xxx”):获得外部存储上的公共路径。
  • Context.getExternalFilesDir():/storage/emulated/0/Android/data/<packname>/files,外部存储中的files路径。
  • Context.getExternalCacheDir():/storage/emulated/0/Android/data/<packname>/cache,外部存储中的cache路径。

上述的接口多数与我们这次要讨论的无关,我们主要关注的是外部存储路径。外部存储路径在不同的 Android 版本上可能不同,上述是 Android 5.1 上显示的结果。外部存储代表的意义在不同版本上也不同。在 Android 4.4 之前的版本,外部存储指的就是扩展的SD卡,也就是通过插槽连接的SD卡。从 Android 4.4 开始,系统将机身存储划分为内部存储和外部存储,这样在没有扩展SD卡时,外部存储就是机身存储的一部分,指向/storage/emulated/0。当有扩展SD卡插入时,系统将获得两个外部存储路径。

getExternalStorageDirectory() 在不同 Android 版本下获取到的路径如下:

系统版本 外部存储路径 扩展SD卡路径
4.0 /mnt/sdcard /mnt/sdcard
4.1 /storage/sdcard0 /storage/sdcard0
4.2 /storage/sdcard0 /storage/sdcard1
4.4 /storage/emulated/0 /storage/sdcard1
6.0 /storage/emulated/0 /storage/<product-id>

上表的路径我并没有真实验证过,并且设备厂商会修改自己的SD卡路径,可能与实际的路径不同。Android 4 上存储路径本身就是一个混乱状态,这也不是我们关心的重点。我们关心的是 /storage/emulated/0,这个路径是使用机身内部存储模拟的外部存储。在应用中,我们通过 getExternalStorageDirectory() 返回的就是这个路径,所以应用本身的数据会写到这个路径下。但是当我们使用 adb 连接记性调试时,会发现这个路径并不存在,只能看到下面的结果:

# ls -l /storage/emulated/                                   
lrwxrwxrwx root     root              1970-01-01 08:00 legacy -> /mnt/shell/emulated/0
# ls -l /mnt/shell/emulated
drwxrwx--x root     sdcard_r          2020-04-15 11:07 0
drwxrwx--x root     sdcard_r          1970-01-01 08:03 legacy
drwxrwx--x root     sdcard_r          1970-01-01 08:00 obb

我们都知道应用的数据会写到 /mnt/shell/emulated/0 中,但这个路径是如何与 /storage/emulated/0 关联的呢?这就要提到 Linux 的 Mount 命名空间。

Mount 命名空间提供了一个用户或者容器独立的文件系统树。它隔离了每个进程可以看到的挂载点列表,换句话说,每个 Mount 命名空间都有它们自己的挂载点列表,意味着在不同命名空间中的进程都可以看到且控制不同的目录层次结构(目录树)。简单的说就是每个进程可以有自己独立的 Mount 节点,其他进程是无法看到的。下面看看 Android 中是如何使用的。在应用的启动时会通过下面的流程挂载模拟存储。

Zygote.forkAndSpecialize() ->
    Dalvik_dalvik_system_Zygote_forkAndSpecialize() ->
        forkAndSpecializeCommon() ->
            mountEmulatedStorage()

MountEmulatedStorage()完成具体的模拟存储挂载。

frameworks/base/core/jni/com_android_internal_os_Zygote.cpp

static bool MountEmulatedStorage(uid_t uid, jint mount_mode, bool force_mount_namespace) {
  if (mount_mode == MOUNT_EXTERNAL_NONE && !force_mount_namespace) {
    return true;
  }

  // 在当前进程创建一个新的命名空间
  if (unshare(CLONE_NEWNS) == -1) {
    ......
  }
  ......

  // 将uid转换为用户id,一个用户最多有10000个uid
  userid_t user_id = multiuser_get_user_id(uid);

  // Bind模拟存储
  if (mount_mode == MOUNT_EXTERNAL_MULTIUSER || mount_mode == MOUNT_EXTERNAL_MULTIUSER_ALL) {
    // These paths must already be created by init.rc
    const char* source = getenv("EMULATED_STORAGE_SOURCE");
    const char* target = getenv("EMULATED_STORAGE_TARGET");
    const char* legacy = getenv("EXTERNAL_STORAGE");
    ......
    // /mnt/shell/emulated/0
    const String8 source_user(String8::format("%s/%d", source, user_id));
    // /storage/emulated/0
    const String8 target_user(String8::format("%s/%d", target, user_id));

    if (fs_prepare_dir(source_user.string(), 0000, 0, 0) == -1
        || fs_prepare_dir(target_user.string(), 0000, 0, 0) == -1) {
      return false;
    }

    if (mount_mode == MOUNT_EXTERNAL_MULTIUSER_ALL) {
      ......
    } else {
      // 将 /mnt/shell/emulated/0 绑定到 /storage/emulated/0 上
      if (TEMP_FAILURE_RETRY(
              mount(source_user.string(), target_user.string(), NULL, MS_BIND, NULL)) == -1) {
        ......
      }
    }

    if (fs_prepare_dir(legacy, 0000, 0, 0) == -1) {
        return false;
    }

    // 将 /storage/emulated/0 绑定到 /storage/emulated/legacy 上
    if (TEMP_FAILURE_RETRY(
            mount(target_user.string(), legacy, NULL, MS_BIND | MS_REC, NULL)) == -1) {
      ......
    }
  } else {
    ......
  }

  return true;       
}

实现过程也是很简单,就是在应用启动过程中创建了独立的 Mount 命名空间,在该命名空间中使用 MS_BIND 将 /mnt/shell/emulated/0/storage/emulated/0 绑定,/storage/emulated/0/storage/emulated/legacy 绑定,这样访问 /storage/emulated/0 就等同与访问 /mnt/shell/emulated/0 。因为 adb 并不是通过 zygote 启动的,所以就看不到 /storage/emulated/0 目录。

模拟存储的挂载需要使用一些环境变量,这些变量在 init.rc 中设定。下面是 init.rc 中与存储路径相关的代码。

    export EXTERNAL_STORAGE /storage/emulated/legacy # 模拟存储目录
    export EMULATED_STORAGE_SOURCE /mnt/shell/emulated # 模拟存储的源目录
    export EMULATED_STORAGE_TARGET /storage/emulated # 模拟存储的目标目录
    export SECONDARY_STORAGE /storage/sdcard1 # 扩展SD卡目录

    # Support legacy paths
    # 将模拟存储链接到/sdcard,/mnt/sdcard,/storage/sdcard0
    symlink /storage/emulated/legacy /sdcard 
    symlink /storage/emulated/legacy /mnt/sdcard
    symlink /storage/emulated/legacy /storage/sdcard0
    # 启动时将/mnt/shell/emulated/0链接为模拟存储,应用启动时会重新绑定
    symlink /mnt/shell/emulated/0 /storage/emulated/legacy
    # 外接存储的路径
    mkdir /storage/external_storage 0555 root root
    symlink /storage/sdcard1 /storage/external_storage/sdcard1
    symlink /storage/udisk0 /storage/external_storage/udisk0
    symlink /storage/sr0 /storage/external_storage/sr0

参考文档:

彻底搞懂Android文件存储---内部存储,外部存储以及各种存储路径解惑

阅读 1.1k

老王系统屋
做为一个不称职的老年码农,一直疏忽整理笔记,开博记录一下,用来丰富老年生活,

做为一个不称职的老年码农,一直疏忽整理笔记,开博记录一下,用来丰富老年生活,

58 声望
13 粉丝
0 条评论

做为一个不称职的老年码农,一直疏忽整理笔记,开博记录一下,用来丰富老年生活,

58 声望
13 粉丝
宣传栏