获取SD卡路径——完美解决

写在最前面:
(这篇文章写于三年多以前,由于域名备案、服务器到期等问题,以前的文章没备份,都丢失了。现重新将它搬回来。)由于公司的项目里有个视频下载的功能,而且这个是产品比较重要的功能。但是,由于众所周知的原因,通过传统方式获取的SD卡路径,在不同厂商的设备上都不准确,可能SD卡和内存存储介质倒置了,也可能获取出来的路径无法读写。就算是相同厂商不同的产品,获取出来的SD卡路径和内置存储路径都是五花八门。
网上到处找资料,还是没法完全解决上述问题,连有些主流的机型都无法覆
盖。经过一段时间探索,算是解决了问题。

阅读Environment、StorageManager和StorageVolume的源码,找到突破口。我想大部分的APP的解决方案都是这样。

Environment里有这样一个方法isExternalStorageRemovable(),注释如下,大概意思是:
如果返回true,external storage是用户可以移除的,如SD卡、U盘(这一翻译是我自己加的,因为通过数据线等方式挂载到手机上的U盘也属于可移除的存储介质)等。如果返回false,说明external是集成到设备中的,不可以进行物理移除。
Returns whether the primary “external” storage device is removable. If true is returned, this device is for example an SD card that the user can remove. If false is returned, the storage is built into the device and can not be physically removed.
See getExternalStorageDirectory() for more information.
public static boolean isExternalStorageRemovable() {}

核心SD卡对系统而言是可移除的,而内置存储不可以移除。

解决思路有多种:

方案一:

用反射,调用
StorageManager类的隐藏方法
getVolumeList()

StorageVolume 类的隐藏方法
getPath()
isRemovable()
getState()

这里需要注意的是getState方法不一定在所有版本中都有,对比多个版本的源码后得知,此方法是在4.4_r1之后新增的,使用时需要注意,要判断磁盘的挂载状态,不能只依赖getState。另外,不要试图调用StorageVolume类中的其它方法,原因上面提过,本人对比过,有些方法在其它版本中不一定有,比如isPrimary()——是否是主存储器,就是在4.2_r1版本之后才有的方法。

方案二:

看系统设置APP中Storage模块的具体实现。既然系统设置中可以正确的获取到SD卡位置,那么可以看看SettingActivity到底是怎么做的(我还没有具体去看,但可以确定的是,SettingActivity里也利用了StorageManager的隐藏方法,只不过SettingActivity里用的是getDisks()来获取磁盘信息,如果要用这些方法,还是得用反射)。
Setting模块的源码:
https://github.com/android/platform_packages_apps_settings/tree/master/src/com/android/settings

Storage模块的位置:
Deviceinfo->StorageSettings
clone到AndroidStudio里更方便查看。
https://github.com/android/platform_packages_apps_settings/blob/master/src/com/android/settings/deviceinfo/StorageSettings.java

方案三:

这个是看得别人的,在Environment类里找到的方法。但是,也是由于版本问题,在部分低版本和高版本上无法使用,所以不建议使用
两行代码:
SD卡:System.getenv(“SECONDARY_STORAGE”)
内置存储:System.getenv(“EXTERNAL_STORAGE”)
他们返回的都是path。

方案四:

(这个方案是后来Android 7.0发布之后,看了这部分源码才补充的此方案)Android API24中,已经将StorageValume放开,作为公共API了,可喜可贺,API24及以上可以不用反射了。https://developer.android.com/reference/android/os/storage/StorageVolume

这里具体讲解方案一的实现
具体步骤(完整代码后在会面贴出):

    1. 获取StorageManager
      final StorageManager storageManager=(StorageManager)pContext.getSystemService(Context.STORAGE_SERVICE);
    2. 反射得到StorageManger里的getVolumeList()方法
      这个方法会返回系统中所有的存储设备(包含未挂载的,不含内存盘)
      //得到StorageManager中的getVolumeList()方法的对象
      final Method getVolumeList=storageManager.getClass().getMethod("getVolumeList");
    3. 反射得到StorageVolume类的对象
      //得到StorageVolume类的对象
      finalClass<?> storageValumeClazz=Class.forName("android.os.storage.StorageVolume");
    4. 反射得到StorageVolume类里的getPath()、isRemovable()、getState()方法
      //获得StorageVolume中的一些方法
      final MethodgetPath=storageValumeClazz.getMethod("getPath");
      Method isRemovable=storageValumeClazz.getMethod("isRemovable");
      
      Method mGetState=null;
      //getState方法是在4.4_r1之后的版本加的,之前版本(含4.4_r1)没有
      //(http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/4.4_r1/android/os/Environment.java/)
      if(Build.VERSION.SDK_INT>Build.VERSION_CODES.KITKAT){
      try{
          mGetState=storageValumeClazz.getMethod("getState");
      }catch(NoSuchMethodExceptione){
          e.printStackTrace();
      }
      }
    5. 反射获取属性的核心方法,最终会得到每个StorageVolume对象的path、removable和state属性。
//调用getVolumeList方法,参数为:“谁”中调用这个方法
final Object invokeVolumeList = getVolumeList.invoke(storageManager);
//---------------------------------------------------------------------
final int length = Array.getLength(invokeVolumeList);
ArrayList<StorageBean> list = new ArrayList<>();
for (int i = 0; i < length; i++) {
 final Object storageValume = Array.get(invokeVolumeList, i);//得到StorageVolume对象
 final String path = (String) getPath.invoke(storageValume);
 final boolean removable = (Boolean) isRemovable.invoke(storageValume);
 String state = null;
 if (mGetState != null) {
    state = (String) mGetState.invoke(storageValume);
 } else {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
        state = Environment.getStorageState(new File(path));
    } else {
        if (removable) {
            state = EnvironmentCompat.getStorageState(new File(path));
        } else {
            //不能移除的存储介质,一直是mounted
            state = Environment.MEDIA_MOUNTED;
        }
        final File externalStorageDirectory = Environment.getExternalStorageDirectory();
        Log.e(TAG, "externalStorageDirectory==" + externalStorageDirectory);
    }
 }
}

经过这几步,SD卡路径已经能完美获取了,而且准确无误(就目前测试过的设备而言)。具体代码可以下载我写的demo。
源码戳这里:https://github.com/gongshoudao/SDcardScanner
效果图如下:
内置存储信息

 

SD卡信息