带你彻底搞懂 Android 存储!(建议收藏)
BAT
作者:小鱼人爱编程
链接:https://www.jianshu.com/p/93c9f5e2d2a7
在持久化数据的时候,一般都是选择存入到文件里,本篇将着重分析Android 存储相关的知识, 通过本篇文章,你将了解到:
存储划分 内部存储 外部存储 易混淆点说明
1. 存储划分
1.1 Android 4.4 之前
在Android 4.4 之前,由于硬件发展受限,手机自身的存储空间有限,需要通过外置SD卡来扩展存储空间。
如上图,手机自身的存储空间,称之为机身存储,在Android 4.4 之前作为内部存储使用。当然内部存储空间一般是不够用的,所以需要通过插入外置SD卡来扩充存储空间,这当做外部存储。
1.2 Android 4.4之后
在Android 4.4 之后(含),手机机身存储扩大了:
如上图,机身存储划分为两部分:内部存储和外部存储
当然,依然可以插入SD卡来扩充存储空间,这部分的存储空间称为扩展的外部存储空间。只是现在机身存储都比较大,很少插入SD卡了。
接下来将以Android 4.4 之后的存储划分来分析具体的存储方案。
2. 内部存储
2.1 存储位置
回想一下平时使用的持久化方案:
-
SharedPreferences:适用于存储小文件 -
数据库:存储结构比较复杂的大文件
以上这些文件都是默认放在内部存储里。"/" 表示根目录,内部存储里给每个应用按照其包名各自划分了目录,假设App的包名为:com.fish.myapplication
那么该文件在内部存储里的目录为:/data/user/0/com.fish.myapplication/
第一个"/"表示根目录,其后每个"/"表示目录分割符。"0" 表示是第一个用户,后续添加了多用户则生成相应的用户目录:
如上图,新增了两个用户,生成的目录分别是:"11"、"12"。目前来说,很少开启多用户的。一般来说,adb shell
里是没有权限查看/data
目录的。若要查看内部存储,通常是通过 Android Studio侧边栏Device File Explorer
选择对应的目标设备查看。
同样的,如果包名为:com.fish.myapplication
,则对应的内部存储目录为:/data/data/com.fish.myapplication/
/data/user/0/com.fish.myapplication/
会将值转换到/data/data/com.fish.myapplication/
路径下。每个App的内部存储空间仅允许自己访问(除非有更高的权限,如root),程序卸载后,该目录也会被删除。
2.2 存储内容
除了SharedPreferences、数据库文件,内部存储还存放了哪些文件呢?为方便起见,只查看/data/data/
目录下的。
刚开始有只有两个空目录。当进行写入SharedPreferences,创建数据库、写入文件等操作后新增了几个目录:
大致介绍一下以上目录作用:
目录 | 用途 |
---|---|
cache | 存放缓存文件 |
code_cache | 存放运行时代码优化等产生的缓存 |
databases | 存放数据库文件 |
files | 存放一般文件 |
shared_prefs | 存放 SharedPreferences 文件 |
lib | 存放App依赖的so库 是软链接,指向/data/app/ 某个子目录下 |
2.3 访问方式
既然知道了各类文件存储的目录,那么如何读写这些文件呢?我们知道在Java 的世界里,操作文件有两种方式:字符流和字节流
以字节流为为例,一个简单的读取写入文件Demo:
//写入文件
private void writeFile(String filePath) {
if (TextUtils.isEmpty(filePath))
return;
try {
File file = new File(filePath);
FileOutputStream fileOutputStream = new FileOutputStream(file);
BufferedOutputStream bos = new BufferedOutputStream(fileOutputStream);
String writeContent = "hello world\n";
bos.write(writeContent.getBytes());
bos.flush();
bos.close();
} catch (Exception e) {
}
}
//从文件读取
private void readFile(String filePath) {
if (TextUtils.isEmpty(filePath))
return;
try {
File file = new File(filePath);
FileInputStream fileInputStream = new FileInputStream(file);
BufferedInputStream bis = new BufferedInputStream(fileInputStream);
byte[] readContent = new byte[1024];
int readLen = 0;
while (readLen != -1) {
readLen = bis.read(readContent, 0, readContent.length);
if (readLen > 0) {
String content = new String(readContent);
Log.d("test", "read content:" + content.substring(0, readLen));
}
}
fileInputStream.close();
} catch (Exception e) {
}
}
可以看出,通过 FileInputStream/FileOutputStream
构造函数传入 File
对象即可实现文件读写,而 File
对象的构造依赖于文件的存放路径,因此重点在于如何获取文件的路径。分别说明各个目录下文件的读写:
2.3.1 读写files目录下文件
#Context.java
public abstract File getFilesDir();
使用方式:
private String getFilePath(Context context) {
//获取files根目录
File fileDir = context.getFilesDir();
//获取文件
File myFile = new File(fileDir, "myFile");
return myFile.getAbsolutePath();
}
context.getFilesDir()
的结果是返回files目录:
/data/user/0/com.fish.myapplication/files/
拿到对应文件的File对象后,构造相应的输入输出流即可实现对该文件的读写。可以看出,过程虽然简单但是有点枯燥,因此Google将这些步骤封装好了,直接返回对应文件的 FileOutputStream/FileInputStream
:
#Context.java
public abstract FileInputStream openFileInput(String name)
throws FileNotFoundException;
public abstract FileOutputStream openFileOutput(String name, @FileMode int mode)
throws FileNotFoundException;
其中name 表示文件名,mode表示访问权限。
2.3.2 读写cache目录下文件
与读取files目录相似:
#Context.java
public abstract File getCacheDir();
context.getCacheDir()的结果是返回cache目录:
/data/user/0/com.fish.myapplication/cache/
2.3.3 读写shared_prefs目录下文件
SharedPreferences 提供了简易的快速持久化数据的方案。
private void testSP(String fileName, String key, String value) {
if (TextUtils.isEmpty(fileName) || TextUtils.isEmpty(key) || TextUtils.isEmpty(value))
return;
//构造SP文件
SharedPreferences sp = getSharedPreferences(fileName, MODE_PRIVATE);
//写入SP
sp.edit().putString(key, value).commit();
//读取SP
String myValue = sp.getString(key, "");
}
其内部也是使用了输入输出流,以写入SP文件为例:
#SharedPreferencesImpl.java
private void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) {
...
//构造输出流
FileOutputStream str = createFileOutputStream(mFile);
XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);
FileUtils.sync(str);
str.close();
...
}
2.3.4 读写数据库目录下文件
创建数据库:
MyDatabaseHelper myDatabaseHelper = new MyDatabaseHelper(v.getContext(), "myDB", null, 10);
myDB是数据库文件名。打开数据库的相应表,即可读写数据。
获取数据库文件路径:
#Context.java
Context.public abstract File getDatabasePath(String name);
获取结果如下:
/data/user/0/com.fish.myapplication/databases/myDB
2.3.5 读写code_cache目录下文件
#Context.java API>=21
public abstract File getCodeCacheDir();
获取结果如下:
/data/user/0/com.fish.myapplication/code_cache/
以上是分别列举了各个子目录/文件的获取方式,如果想获取:/data/user/0/com.fish.myapplication/
,可通过:
#Context.java
public abstract File getDataDir();
该方法需要 API>=24。
3. 外部存储
外部存储分为两部分:自带外部存储和扩展外部存储(外置SD卡)
3.1 自带外部存储存储
3.1.1 存储位置
自带外部存储的根目录是:"/"。
根目录下几个需要关注的目录:
-
/data/ -
/sdcard/ -
/storage/
其中/data/
目录前面已经分析过。
/sdcard/
是软链接,指向/storage/self/primary
而/storage/
下有几个目录:
/storage/self/primary/
是软链接,指向/storage/emulated/0/
也就是说/sdcard/、/storage/self/primary/ 真正指向的是/storage/emulated/0/
3.1.2 存储内容
自带外部存储主要有以下内容:
如上图所示,/sdcard/目录下的子目录看起来都比较眼熟。这些子目录分为分为三部分:
第一部分:共享存储空间
也就是所有App共享的部分,比如相册、音乐、铃声、文档等。共享存储空间按文件类型又分为两部分:
-
媒体文件
目录 | 用途 |
---|---|
DCIM/ 和 Pictures/ | 存储图片 |
DCIM/、Movies/ 和 Pictures | 存储视频 |
Alarms/、Audiobooks/、Music/、Notifications/、Podcasts/ 和 Ringtones/ | 存储音频文件 |
Download/ | 下载的文件 |
-
文档和其它文件
目录 | 用途 |
---|---|
Documents | 存储如.pdf类型等文件 |
第二部分:App外部私有目录
目录 | 用途 |
---|---|
Android/data/ | 存储各个App的外部私有目录,与内部存储类似,命名方式是:Android/data/xx(xx指应用的包名)。如:/sdcard/Android/data/com.fish.myapplication |
Android/data/--->存储各个App的外部私有目录 与内部存储类似,命名方式是:Android/data/xx------>xx指应用的包名。如:/sdcard/Android/data/com.fish.myapplication
第三部分:其它目录
比如各个App在/sdcard/
目录下创建的目录,如支付宝创建的目录:alipy/
,微博创建的目录:com.sina.weibo/
,qq创建的目录:com.tencent.mobileqq/
等。
3.1.3 访问方式
与访问内部存储文件类似,外部存储也可以通过构造输入输出流访问文件。
读写共享存储空间
视频、图片等可能分散存储在各个不同的目录里,如果想要获取所有的图片地址,那么得需要遍历不同的目录寻找,效率显而易见的低。Android 将视频、图片等信息存储在数据库里,每当某个App想要访问这些共享的媒体文件时只需要查找数据库对应的表,读取符合条件的行,找出每个媒体的文件路径等信息。
App查询共享存储空间的媒体方式是:通过ContentProvider访问。
-
访问媒体文件
以查询图片为例:
private void getImagePath(Context context) {
ContentResolver contentResolver = context.getContentResolver();
Cursor cursor = contentResolver.query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, null, null, null, null);
while(cursor.moveToNext()) {
String imagePath = cursor.getString(cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATA));
}
}
查询到图片的地址,当然就可以展示图片了。
-
访问文档和其它文件Storage Access Framework 简称SAF:存储访问框架
以查看.pdf
文件为例:
private void startSAF() {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("application/pdf");
startActivityForResult(intent, 100);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == 100) {
Uri uri = data.getData();
}
}
SAF实际上就是调用系统提供的选择器,选中后在 onActivityResult(xx)
里接收结果,拿到Uri
后当然就可以读写对应的文件了。
读写App外部私有目录
刚开始并没有自己App的包名。
调用如下方法后:
private void testAppDir(Context context) {
//4个基本方法
File fileDir = context.getExternalFilesDir(null);
//API>=19
File[] fileList = context.getExternalFilesDirs(null);
File cacheDir = context.getExternalCacheDir();
//API>=19
File[] cacheList = context.getExternalCacheDirs();
//指定目录,自动生成对应的子目录
File fileDir2 = context.getExternalFilesDir(Environment.DIRECTORY_DCIM);
}
再查看目录树:
可以看出再/sdcard/Android/data/
目录下生成了com.fish.myapplication/
目录,该目录下有两个子目录分别是:files/
、cache/
。当然也可以选择创建其它目录。
App卸载的时候,两者都会被清除。
读写其它目录
只要拿到根目录,就可以遍历寻找其它子目录/文件。
private void testOtherDir(Context context) {
File rootDir = Environment.getExternalStorageDirectory();
}
返回的rootDir路径:/storage/emulated/0/
。
3.2 扩展外部存储(外置SD卡)
3.2.1 存储位置
当给设备插入SD卡后,查看其目录:
/sdcard/
依然指向 /storage/self/primary
,继续来看/storage/
:
可以看出,多了sdcard1,软链接指向了 /storage/77E4-07E7/
。
3.2.2 存储内容
取决于SD卡上装了什么东西。
3.2.3 访问方式
还记得上面获取外部存储-App私有目录方式吗?
File[] fileList = context.getExternalFilesDirs(null);
返回File对象数组,当有多个外部存储时候,存储在数组里。
返回的数组有两个元素,一个是自带外部存储存储,另一个是刚插入的SD卡。拿到路径后,当然就可以访问相应的文件了。
4. 易混淆点说明
以上分别阐述了内部存储、自带外部存储、扩展外部存储等,这几者关系如下:
其中比较容易混淆的是:内部存储与外部存储里的App私有目录,两者命名风格很像。
4.1 不同点
/data/data/com.fish.myapplication/
位于内部存储,一般用于存储容量较小的,私密性较强的文件。而/sdcard/Android/data/com.fish.myapplication/
位于外部存储,作为App私有目录,一般用于存储容量较大的文件,即使删除了也不影响App正常功能。
4.2 相同点
-
属于App专属,App自身访问两者无需任何权限。 -
App卸载后,两者皆被删除。 -
两者目录下增加的文件最终会被统计到"设置->存储和缓存"里。
另外,常见的在设置里的"存储与缓存"项:
当点击"Clear cache" 时:
-
内部存储 /data/data/com.fish.myapplication/cache/
、/data/data/com.fish.myapplication/code_cache/
目录会被清空 -
外部存储 /sdcard/Android/data/com.fish.myapplication/cache/
会被清空
当点击"Clear storage" 时:
-
内部存储 /data/data/com.fish.myapplication/
下除了lib/,其余子目录皆被删除 -
外部存储 /sdcard/Android/data/com.fish.myapplication/
被清空
注:该功能慎用,因为会删除用户数据库,SP文件等,相当于重置了App
推荐阅读
• 耗时2年,Android进阶三部曲第三部《Android进阶指北》出版!
BATcoder技术群,让一部分人先进大厂
大家好,我是刘望舒,腾讯TVP,著有三本业内知名畅销书,连续四年蝉联电子工业出版社年度优秀作者,谷歌开发者社区特邀讲师,百度百科收录的高级技术专家。
前华为技术专家,现大厂技术负责人。
想要加入 BATcoder技术群,公号回复BAT
即可。
为了防止失联,欢迎关注我的小号
微信改了推送机制,真爱请星标本公号