安卓6.0已经发布一段时间了,不过安卓6.0的新特性仍在发掘中。现在,又有一个有趣的新特性被发现了——安卓6.0可以把microSD卡也就是TF卡,作为设备的内置存储使用。这个新特性之所以被发现得晚,大概是由于能够率先升级安卓6.0的Nexus设备不支持TF卡扩展吧。

在之前,安卓系统虽然可以支持TF卡扩展,但TF卡扩展的容量只能当成外置存储。虽然用户可以把照片、音乐等内容放进去,但App不能安装到其中。在安卓6.0中,当首次插入TF卡的时候,系统会询问会将TF卡作为外置还是内置储存。如果选择作为内置存储,系统则会对TF卡格式化并加密——注意,这会丢失数据!然后,TF卡的容量就和设备的内置存储融合了。

  一般来说,设备的内置存储速度会比TF卡快上不少,稳定性也更高,因此如果不是容量特别紧张,并不建议把TF当成内置储存植入。同时,市面上支持TF卡扩展的安卓设备也越来越少,安卓6.0的这个新特性也许会遭到冷遇。




Android手机上的外置SD卡,起初的时候,即在Android出世的前几年,那时手机的存储是十分有限的,不像现在到处可见16G、32G和64G的存储,因而那时候的手机有的厂商允许插入外置的SD卡,此时这张卡仍处于手机的扩展部分。后来,随着手机的发展以及存储能力的增加,这张外置SD卡,逐渐成为了手机的一部分,不再允许可插拔了,当然现在依然有的手机允许对存储进行拓展,比如三星等。


那张拓展的存储卡,现在叫做TF卡,且不是所有的手机都支持它,但是有时候有些奇葩需求偏要优先存储在TF卡里面,这叫不得不要求开发人员去检查这张卡是否存在、是否可用。又因为这是手机厂商可拓展、可自定义的部分,所有不同厂商生产的手机,以及同一厂商生产的不同型号的手机,TF卡的位置都相差很大,并没有一个统一的名称或位置。因而这是比较困难的一部分,但是还好Android是开源的,我们可以通过运行时来判断手机是否有TF卡,以及TF卡是否可用。


下面这个方法可以获取手机的可以存储,包括SD卡、TF卡等,对多存储卡进行了匹配,详细的代码如下



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





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


Environment里有这样一个方法isExternalStorageRemovable(),注释如下,大概意思是:
如果返回true,external storage是用户可以移除的,如SD卡、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         (         )         {


          final                   StorageVolume          primary                   =                   getPrimaryVolume         (         )         ;


          return                   (         primary                   !=                   null                   &&                   primary         .         isRemovable         (         )         )         ;


}


核心: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模块的位置:
  1. Deviceinfo/StorageSettings
  2. clone到AndroidStudio里更方便查看。

StorageSettings.java

第三种:

这个是看得别人的,在Environment类里找到的方法。但是,也是由于版本问题,在部分低版本和高版本上无法使用,所以不建议使用。两行代码:

  • SD卡:System.getenv(“SECONDARY_STORAGE“)
  • 内置存储:System.getenv(“EXTERNAL_STORAGE“)

他们返回的都是path

这里使用第一种方法,具体步骤(完整代码后在会面贴出):

①.获取StorageManager






final                  StorageManager         storageManager         =         (         StorageManager         )         pContext         .         getSystemService         (         Context         .         STORAGE_SERVICE         )         ;


②.反射得到StorageManger里的getVolumeList()方法






这个方法会返回系统中所有的存储设备(包含未挂载的,不含内存盘)

//得到StorageManager中的getVolumeList()方法的对象


final                   Method          getVolumeList         =         storageManager         .         getClass         (         )         .         getMethod         (         "getVolumeList"         )         ;



③.反射得到StorageVolume类的对象






//得到StorageVolume类的对象


finalClass         <         ?         >                  storageValumeClazz         =         Class         .         forName         (         "android.os.storage.StorageVolume"         )         ;


④.反射得到StorageVolume类里的getPath()、isRemovable()、getState()方法





//获得StorageVolume中的一些方法


final                   Method          getPath         =         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         (         )         ;


          }


}


⑤.反射获取属性的核心方法,最终会得到每个StorageVolume对象的path、removable和state属性。






//调用getVolumeList方法,参数为:“谁”中调用这个方法


final                   Objectinvoke          VolumeList                  =                  getVolumeList         .         invoke         (         storageManager         )         ;


final                  int                  length         =         Array         .         getLength         (         invokeVolumeList         )         ;


ArrayList          list         =                  new                  ArrayList         <>         (         )         ;


for         (         int                  i         =         0         ;         i         <         length         ;         i         ++         )         {         


         final                  Object                  storageValume         =         Array         .         get         (         invokeVolumeList         ,         i         )         ;         //得到StorageVolume对象


         final                  Stringpath         =         (         String         )         getPath         .         invoke         (         storageValume         )         ;         f


         inal          booleanremovable         =         (         Boolean         )         isRemovable         .         invoke         (         storageValume         )         ;         


         Stringstate         =         null         ;         


if         (         mGetState         !=         null         )         {         


         state         =         (         String         )         mGetState         .         invoke         (         storageValume         )         ;         


}         else         {         


         if         (         Build         .         VERSION         .         SDK_INT         >=         Build         .         VERSION_CODES         .         KITKAT         )         {


         state         =         Environment         .         getStorageState         (         newFile         (         path         )         )         ;


         }         else         {


         if         (         removable         )         {


         state         =         EnvironmentCompat         .         getStorageState         (         newFile         (         path         )         )         ;


         }         else         {


         //不能移除的存储介质,一直是mounted


         state         =         Environment         .         MEDIA_MOUNTED         ;


         }


         final                  FileexternalStorageDirectory         =         Environment         .         getExternalStorageDirectory         (         )         ;


         Log         .         e         (         TAG         ,         "externalStorageDirectory=="         +         externalStorageDirectory         )         ;


         }


}


}


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

如果今后有时间,再研究一下上面提到的第二种方式。