一 概述:
不知道谷歌是何原因,把API: Environment.getExternalStorageDirectory() 设计成返回单个路径。这在手机有多个扩展存贮(External Storage)时就出问题了。或者谷歌是想先支持单个扩展存贮,后面再支持多个扩展存贮吧(这只是我猜的^0^)。然而无论谷歌的原来的想法是怎么样都好,手机生产厂商侧没有管谷歌那么多,照样生产出大量有多个扩展存贮的安卓手机。这样的厂商典型的有三星,国内的山寨厂商情况更加混乱。这里对该问题的解决方法进行梳理,并加上一些我自己的发现,望大家能用得上。推荐大家先看看如下这文章:
《Android应用正确使用扩展SD卡》
http://my.oschina.net/liucundong/blog/314520
最起码可知道内部存贮在Android中被称为“primary storage”,而外置存贮被称为“secondary external storage device”. 这样查找相关文章时就会方便很多。
二 问题描述:
2.1 Environment.getExternalStorageDirectory()不能精确外置存贮卡
在官方版的Android系统,Environment.getExternalStorageDirectory()返回的是扩展存贮的挂载路径。有材料显示该路径在官方版的系统中实质上是写死的。按照一般的理解,扩展存贮是指人们常说的外置存贮卡。但实质上,在不同的真机中调用该函数时,有的真机会返 回外置存贮卡的挂载路径,而有些真的则会返回内置存贮的挂载径。Android提供了另一个函数 Environment.isExternalStorageRemovable()来判断存贮设备是否可以删除,我们可以调用这函数获知 Environment.getExternalStorageDirectory()返回的路径其挂载的储贮是否为外置的。但问题又来了:如果Environment.getExternalStorageDirectory()返回的是内置的存贮,那么我们怎么样才能获取到外置存贮的挂载路径呢?
2.2 多存贮卡, 模拟存贮卡
上文已经提及Environment.getExternalStorageDirectory()只返回一个写死的路,其实是有设计上的局限的。如果所谓的扩展存贮有两个或两个以上,而该函数却只能返回一个路径,那里够用!别的不说,就拿我用的手机 三星 GT-I9158P,就有两个扩展存贮,Environment.getExternalStorageDirectory()返回的是内部存贮的路径,而外置存贮卡的路径,我却找不到开放的API能直接返回。
三 解决方法:
老实话,大家共享出来的方法还是挺多的,大概有如下几种:利用反射调用隐藏函数\“mount”命令结果过滤\vold.fstab文件内容过滤\环境变量"SECONDARY_STORAGE"内容过滤。 另外我在研究这问题过程中,发现了"df"命令及数个环境变量,对解决这个问题都很有帮助。
3.1 利用反射调用隐藏函数
网上文章的主要介始的主要是反射“StorageManager”相关的隐藏函数。有一法是反射“getVolumePaths”而还有另一法是反射“getVolumeList”。功能上而言“getVolumePaths”只返回路径,“getVolumeList”既返回路径,也返回是否可以删除(可删除则为外置)。
反射“getVolumeList”的方法,在如下文章有详细说明:
《获取Android设备挂载的所有存储器》
http://vjson.com/wordpress/%E8%8E%B7%E5%8F%96android%E8%AE%BE%E5%A4%87%E6%8C%82%E8%BD%BD%E7%9A%84%E6%89%80%E6%9C%89%E5%AD%98%E5%82%A8%E5%99%A8.html
反射“getVolumePaths”的方法,在如下文章有详细说明:
《android上通过反射,获取存储器列表 》
http://blog.csdn.net/victoryckl/article/details/8015050
就这两种反射方法比较而言,我推荐使用反射“getVolumeList”的方法。主要是因为Android系统代码中getVolumePaths已经被示记为“@Deprecated”,即不建议使用(可能会在日后的代码中删除),或有更好的函数(或者就是指getVolumeList)。如下图:
整体上利用反射调用隐藏函数是解决获取外置存贮卡路径问题的有效方法。该方法的风险在于调用隐藏函数。所有调用隐藏函数的功能都不能保证在日后的系统版本中有效(公开的API则有长久性的保证)。
3.2 “mount”命令结果过滤
在Android的源代码中StorageManager.getVolumeList()实质是调用了mount service来实现具体的功能的。详细可以可以看该函数中mMountService个的使用。可能这受到了这个的启发,有开发人员想到了利用Android系统中的命令"mount"来获取外置存贮卡的挂载路径。 那么"mount"命令的输出到底是什么呢?如下图:
这是"mount"命令在我的手机上运行结果的部分截图(当然是adb shell连接的)。我的实验结果是,只要是挂载上系统的的存贮设备无论内外,都会在"mount"命令中输出其挂载路径。然而,"mount"除了会输出存贮设备的挂载信息,还会把一些有特殊作用的目录的挂载信息也会输出来,例如obb目录。即使排除了这些特殊目录的干扰。如何区分内外存贮设备的问题还是没有解决(可能是因为我没搞清楚输出信息的完整含义吧)。网上,有网友就是在"mount"的输出结果中找外置存贮的。具体的方法:就是获取"mount"输出的所有挂载路径,然后用“obb”,"secure",''asec"等关键字过滤。如果,过滤结果不只一个,则把最后一个当作外置存贮。
”把最后一个当作外置存贮“,这样的处理不知道是有何根据,网上的评论中有人提出了质疑。我的观点是:虽然我也觉得最后一个并不一定是外置存贮(因为我的手机就不是),但我仍然感谢说出该方法的人,因为至少他说出了一种一定可以实时获取外置存贮路径的方法,虽然不能确定结果中那个才是外置的。
类似于"mount",“df”命令也有输出挂载路径的功能。因为“df”命令是用来显示分区空间使用情况的,所以其输出路径列表,会有不同。当然“df”命令也会输出内外存贮的挂载路径。“df”命令输出结果如下图:
输出的路径也不少!同样也是不能直接用结果中的信息来区分内外存贮。下面是我写的一个函数,用来获取“mount”或“df”输出的可读写目录:
//作用:获取可读写分区\挂载的目录(如果外置SD卡的话,其挂载目录会在返回结果中) //参数:bIsDFCmd 为真时用df命令,进行路径获取;为假时则用mount 命令。 //注意:df 查的是分区的信息;mount查的是挂载的信息;外置SD卡可以被看成时被挂载的一个或多个分区。 //返回:分区挂载目录列表。这些目录是既可读也可写的。 public static ArrayList<String> getCmdAvaibleDirectory(boolean bIsDFCmd) { ArrayList<String> arPaths = new ArrayList<String>(); if(null == arPaths) { return null; } try { String strCmd = bIsDFCmd ? "df" : "mount"; int iPathIdx = bIsDFCmd ? 0 : 1; int iColumsLimit = bIsDFCmd ? 0 : 1; Runtime runtime = Runtime.getRuntime(); Process proc = runtime.exec(strCmd); InputStream is = proc.getInputStream(); InputStreamReader isr = new InputStreamReader(is); String line, strPath; BufferedReader br = new BufferedReader(isr); File file = null; while ((line = br.readLine()) != null) { strPath = null; line = line.trim(); String columns[] = line.split(" "); if (columns != null && columns.length > iColumsLimit) { strPath = columns[iPathIdx]; } if (null == strPath || strPath.length() <= 0) { continue; } strPath = strPath.trim(); if ('/' != strPath.charAt(strPath.length() - 1)) { strPath += "/"; } file = new File(strPath); if (null == file || !file.exists() || !file.isDirectory() || !file.canRead() || !file.canWrite()) { continue; } file = null; arPaths.add(strPath); } } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } return arPaths; }
由于“mount”及“df”的输出信息所限,上面函数并不能确定输出的列表中那一个是外置的存贮路径。但至少外置的存贮路径会在该列表中,如果有外置的存贮的话。
使用“mount”或“df”命令获取外置的存贮路径最大的好处就使用的都是Android对外公开的工具及函数,而且数据都是实时数据。最大的缺点不能精确区分内外存贮。
3.3 vold.fstab文件内容过滤 及 环境变量"SECONDARY_STORAGE"内容过滤
本小节的两种方法,可以在如下文章找到实现代码:
《android 获取SDCard路径》
http://panccp.blog.163.com/blog/static/2655604420136432233302/
3.3.1 vold.fstab文件内容过滤
vold.fstab该文件记录了Android系统中USB的预设挂载信息。使用该文件中的信息时需要注意如下几个问题:
* 信息是预设的不是实时的。与mount命令及df命令的实时结果不同,无论你的手机当前有没有插入外置的存贮卡,vold.fstab文件中的信息都不会变。换言之,即使你把外置的存贮卡都拔了下来,该文件中照样会有外置的存贮卡的路径。
* vold.fstab的存放路径,会因厂家不同而有所变化。例如:我的三星手机是在/etc/vold.fstab,而红米侧是在/system/etc/vold.fstab
* vold.fstab文件中的信息中实质上既有内置存贮,也有外置存贮的内容。这个可以具体可以看一看如下文章《详述红米内置SD卡与外置SD互换》(http://bbs.xiaomi.cn/t-7959547)
3.3.2 环境变量"SECONDARY_STORAGE"内容过滤
环境变量"SECONDARY_STORAGE",记录外置存贮的路径列表。该变量的内容非常符合我们的需求,但使用该变量时还是要注意如下问题:
* 该变量记录的仍然是预设信息,而非实时信息。这与vold.fstab文件的情况一样。
* 具体系统中不一定有这个环境变量。据我的测试,API LEVEL 19 的模拟器及小米3是没有这个环境变量的。
应该说该变量还是很有用的,因为只要该变量存在就可以确定其内容是外部存贮的路径了。这里我建议大家通过adb shell来查看Android系统各个环境环境变量(可以用 export命令),这对于深入了解Android的内部路径体系大有助益!例如,大家可以留意一下:EXTERNAL_STORAGE,ASEC_MOUNTPOINT,ANDROID_STORAGE,ANDROID_DATA,LOOP_MOUNTPOINT等的环境变量。
vold.fstab文件内容过滤 及 环境变量"SECONDARY_STORAGE"内容过滤。两个都是获取预设信息的;两个在真实机型中都不能保证通用,而且此风险比较高;环境变量"SECONDARY_STORAGE"内容过滤这个方法可区别外置存贮。
3.4 综合性的解决方法
以上所说的几个方法,实际上都存在风险。因为官方对此问题没有标准的解方法。在此情况下,上面这些都是退而求次的选择。所以这里我建议大家综合地使用上面几种方法。具体来说是:能用隐藏函数则以隐藏函数为准;没有隐藏函数,就用mount命令获取实时列表,“SECONDARY_STORAGE”环境变量过滤实时内容。
四 小结
其实,最好是谷哥给出一个标准的解决方法,来解决这个问题。而各大厂商也乐意使用该法。这是最好不过的了。只是,最起码当前我不敢这么乐观。我个人比较倾向于使用“合性的解决方法”,该法的代码我暂时不便贴出来。不过大家有兴趣的话,其实参看本文内容,也可以写出来了^0^。