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

CodeZenithPro
• 阅读 5336
这个问题起源一个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文件存储---内部存储,外部存储以及各种存储路径解惑

点赞
收藏
评论区
推荐文章
android系统稳定分析
分析Android问题时,经常会遇到一些稳定性问题。什么是稳定性问题呢,我归结有以下特点,非必现问题,或没有找到复现路径的问题。其实没有非必现问题,只有找不到复现方法。系统越复杂这类问题越多,因为软件路径太多了。应用的死机重启。这类问题不能简单的归结为应用问题,毕竟应用是跑在系统上的。当应用开发人员无法分析出问题时,可能就会认为是稳定性问题。系统死机重启。A
Wesley13 Wesley13
3年前
android保存文件到手机内存
首先要指定文件保存的位置,在Java中,我们可以直接使用FilefilenewFile(“info.txt”),但是在Android中,使用这个路径文件会被保存到data/app文件夹(应用程序根目录)下,Android是不允许在这里保存文件的。Android保存文件都是保存在“data/data/包名”文件夹下的。故应该:Filefilene
刘望舒 刘望舒
4年前
Android深入四大组件(一)应用程序启动过程(前篇)
Android框架层Android深入四大组件categories:Android框架层本文首发于微信公众号「后厂技术官」前言在此前的文章中,我讲过了Android系统启动流程和Android应用进程启动过程,这一篇顺理成章来学习Android7.0的应用程序的启动过程。分析应用程序的启动过程其实就是分析根Activity的启动过程。<!more1
阿里官方推荐:有了这些中高端面试专题-大厂还会远吗
大佬带你走进Android开发的世界,掌握了这些知识点,学习Android也可以很轻松。核心分析内容对于怎么学习Android,主要解决的是3个问题:学什么、怎么学&怎么用。具体如下:下面,我将带着上述几个问题,详细讲解自身学习Android的方法和Android学习路径;最后,还会结合前面内容,给出综合的具体执行学习Android的建议。面经分享
易娃 易娃
4年前
Android开发 - 获取Android设备的唯一标识码(Android 6.0或更高)
在我们的APP开发中,通常需要获取到设备的唯一标识。在Android6.0之前,有很多方法我们可以方便获取到硬件的唯一标识,但是在Android6.0之后,Android系统大幅限制了我们获取设备的硬件信息。Android6.0之前的方法(已过时)1.DEVICE\_ID通getSystem
Stella981 Stella981
3年前
Android Kotlin遇到的问题
AndroidKotlin遇到的问题1.使用外部文件做一个APP的时候遇到这么一个问题,我需要打开其他APP存放在sdcard中的sqlite数据库,我设置的路径是sdcard/dir/db/db001.sqlite路径看起来是没问题的,但是读取的时候总是报下面这个错误filessd
Stella981 Stella981
3年前
CoreGraphics 之CGAffineTransform仿射变换(3)
 CoreGraphics的仿射变换可以用于平移、旋转、缩放变换路径或者图形上下文。  (1)平移变换将路径或图形上下文中的形状的当前位置平移到另一个相对位置。举例来说,如果你在(10,20)的位置处画一个点,对它应用(30,40)的平移变换,然后绘制它,这个点将被绘制在(40,60)的位置处。为了创建一个平移变换,使用CGAffineTra
Wesley13 Wesley13
3年前
03.Android崩溃Crash库之ExceptionHandler分析
目录总结00.异常处理几个常用api01.UncaughtExceptionHandler02.Java线程处理异常分析03.Android中线程处理异常分析04.为何使用setDefaultUncaughtExceptionHandler前沿上一篇整体介绍了crash崩溃
Wesley13 Wesley13
3年前
Unity横屏
Android下发现Unity里面的Player设置,并不能完全有效,比如打开了自动旋转,启动的时候还是会横屏,修改XML添加以下代码<applicationandroid:icon"@drawable/ic\_launcher"                    android:label"@string/app\_name"
Wesley13 Wesley13
3年前
PHP算法之判断是否是质数
<h3质数的定义</h3<blockquote质数又称素数。一个大于1的自然数,除了1和它自身外,不能整除其他自然数的数叫做质数;否则称为合数。</blockquote<h3实现思路</h3<p循环所有可能的备选数字,然后和中间数以下且大于等于2的整数进行整除比较,如果能够被整数,则肯定不是质数,相反,就是质数。</p<h3第一种算
Stella981 Stella981
3年前
DevOps世界中的软件开发
!(https://oscimg.oschina.net/oscnet/f40e68cbfe8148deb00f040b4e917a0a.jpg)在整个软件开发过程中,开发人员通常需要花费大量时间来修复错误和漏洞,以便一切按计划进行交付。但是,通过DevOps实践,可以更轻松地管理和保护这些问题。这是由于以下事实:使用DevOps实践的软
CodeZenithPro
CodeZenithPro
Lv1
致富路上请务必身体健康。
文章
3
粉丝
0
获赞
0