一. 前言

1. 关于什么是外部存储

  • 我一直以为Android设备的外部存储就是前几年可以插入,可以拔出的内存卡。但这是错误的。
  • 过去早期的Android设备中,内部存储的确是固定的,而外部存储可以像U盘一样移动。
  • 现在的Android设备中,很多安卓设备都将自身的存储扩充到8GB以上,比如我用的小米8扩充到了64GB。所以,它们将存储在概念上分为 内部internal 和 外部external 两部分,但其实都是在手机内部,也就是说那些8GB,16GB,32GB,64GB都是外部存储。
  • 因此,不管Android手机有没有可移动的SDCard,它们总是有外部存储和内部存储,最关键的是,我们可以通过相同的api来访问手机自带的存储和可移动的SDCard。

2. 外部存储迷惑人的位置

  • 一般情况下/storeage/emulated/0是我们见过最多的外部存储位置。但是/mnt/sdcard/sdcard是什么情况?答案是它们也是外部存储位置。
  • 要理解这三个路径的关系,我们需要先要了解一下linux文件挂载的概念,有兴趣的可以自己了解。具体可以参考这一篇文章:彻底搞懂Android文件存储—内部存储,外部存储以及各种存储路径解惑
  • 总结一下,它们都是同一个路径的不同”指针”,指向的是同一个地方,只是不同Android版本的叫法不一样。

二. 关于外部存储的划分

1. 外部根目录

1
2
3
4
5
6
7
/**
* Environment类的静态方法
* 获取外部存储根目录 /storage/emulate/0/
*/
public static File getExternalStorageDirectory();

//API28的模拟器上的测试结果:/storage/emulated/0

2. 外部九大公共目录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* Environment类的静态方法
* 获取九大公共目录 /storage/emulate/0/....
* context:上下文
* type:类型如下 以及API28的模拟器上的测试结果
*/
public static File getExternalStoragePublicDirectory(String type);

//DIRECTORY_MUSIC:音乐类型 /storage/emulated/0/Music
//DIRECTORY_PICTURES:图片类型 /storage/emulated/0/Pictures
//DIRECTORY_MOVIES:电影类型 /storage/emulated/0/Movies
//DIRECTORY_DCIM:照片类型,相机拍摄的照片视频都在这个目录 /storage/emulated/0/DCIM
//DIRECTORY_DOWNLOADS:下载文件类型 /storage/emulated/0/Download
//DIRECTORY_DOCUMENTS:文档类型 /storage/emulated/0/Documents
//DIRECTORY_RINGTONES:铃声类型 /storage/emulated/0/Ringtones
//DIRECTORY_ALARMS:闹钟提示音类型 /storage/emulated/0/Alarms
//DIRECTORY_NOTIFICATIONS:通知提示音类型 /storage/emulated/0/Notifications

3. 外部私有目录

1
2
3
4
5
6
7
8
/**
* Context调用的方法
* 获取外部存储私有目录
* type:类型和公共目录一致,取null时就是私有目录的根目录
*/
public File getExternalFilesDir(String type);

//API28的模拟器上的测试结果:/storage/emulated/0/Android/data/包名/files
1
2
3
4
5
6
7
/**
* Context调用的方法
* 获取外部存储缓存目录
*/
public File getExternalCacheDir();

//API28的模拟器上的测试结果:/storage/emulated/0/Android/data/包名/cache

4. 总结

无论外部内部,只要路径中有包名,就是私有的,用户需要root才能访问。获取路径的方法均是Context调用的,且随着用户删除app而销毁,没有包名的路径均是Environment调用的。

所以,这就需要讲解两个概念,App专属文件 vs App独立文件。

App专属文件:会随着app删除而删除的,它们可以被存储在两个地方:内部存储和外部存储的带有包名的存储区域。

App独立文件:在我们删除应用之后,还应该保留在手机上的,例如拍照的照片,不应该随着删除应用而被删除掉。对于这类文件,Android给我们提供了特定的目录,这些目录都是以DIRECTORY开头的,也就是外部存储的九大公共目录。

三. 外部存储私有目录的文件创建并写入内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
* 外部私有目录写入文件测试
*/
public void privateDirTest(){
try {
//1.在内部存储空间创建self_dir文件夹
File externalFilesDir = getExternalFilesDir(null);
//2.获取输出流
File file = new File(externalFilesDir, "xl.txt");
if (!file.exists()){
file.createNewFile();
}
FileOutputStream fos = new FileOutputStream(file);//覆盖写入
//FileOutputStream fos = new FileOutputStream(file,true); 追加写入
//3.写入信息
User user = new User("xl", 22);
fos.write(user.toString().getBytes());
fos.flush();
fos.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}

四. 外部存储操作的工具类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
public class ExternalStorageHelper {

/**
* 判断是否有外部存储
* @return
*/
public static boolean isExternalStorageMounted() {
// return Environment.getExternalStorageState().equals("mounted");
return Environment.getExternalStorageState().equals(
Environment.MEDIA_MOUNTED);
}

/**
* 获取外部存储的根目录
* @return
*/
public static String getExternalStorageBaseDir() {
if (isExternalStorageMounted()) {
return Environment.getExternalStorageDirectory().getAbsolutePath();
}
return null;
}

/**
* 获取外部存储的完整空间大小
* 单位:MB
* @return
*/
public static long getExternalStorageSize() {
if (isExternalStorageMounted()) {
StatFs fs = new StatFs(getExternalStorageBaseDir());
long count = fs.getBlockCountLong();
long size = fs.getBlockSizeLong();
return count * size / 1024 / 1024;
}
return 0;
}

/**
* 获取外部存储的剩余空间大小
* 单位:MB
* @return
*/
public static long getExternalStorageFreeSize() {
if (isExternalStorageMounted()) {
StatFs fs = new StatFs(getExternalStorageBaseDir());
long count = fs.getFreeBlocksLong();
long size = fs.getBlockSizeLong();
return count * size / 1024 / 1024;
}
return 0;
}

/**
* 获取外部存储的可用空间大小
* 单位:MB
* @return
*/
public static long getExternalStorageAvailableSize() {
if (isExternalStorageMounted()) {
StatFs fs = new StatFs(getExternalStorageBaseDir());
long count = fs.getAvailableBlocksLong();
long size = fs.getBlockSizeLong();
return count * size / 1024 / 1024;
}
return 0;
}

/**
* 保存信息至外部存储的九大公共目录中
* @param data 信息
* @param type 公共目录的类型
* @param fileName 文件名
* @return
*/
public static boolean saveFileToExternalStoragePublicDir(byte[] data, String type, String fileName) {
BufferedOutputStream bos = null;
if (isExternalStorageMounted()) {
File file = Environment.getExternalStoragePublicDirectory(type);
try {
bos = new BufferedOutputStream(new FileOutputStream(new File(file, fileName)));
bos.write(data);
bos.flush();
return true;
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
bos.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
return false;
}

/**
* 保存信息至外部存储根目录下的自定义目录中
* @param data 信息
* @param dir 自定义目录名
* @param fileName 文件名
* @return
*/
public static boolean saveFileToExternalStorageCustomDir(byte[] data, String dir, String fileName) {
BufferedOutputStream bos = null;
if (isExternalStorageMounted()) {
File file = new File(getExternalStorageBaseDir() + File.separator + dir);
if (!file.exists()) {
file.mkdirs();
}
try {
bos = new BufferedOutputStream(new FileOutputStream(new File(file, fileName)));
bos.write(data);
bos.flush();
return true;
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
bos.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
return false;
}

/**
* 保存信息至外部存储的私有目录中
* @param data 信息
* @param type 目录类型
* @param fileName 文件名
* @param context 上下文
* @return
*/
public static boolean saveFileToExternalStoragePrivateFilesDir(byte[] data, String type, String fileName, Context context) {
BufferedOutputStream bos = null;
if (isExternalStorageMounted()) {
File file = context.getExternalFilesDir(type);
try {
bos = new BufferedOutputStream(new FileOutputStream(new File(file, fileName)));
bos.write(data);
bos.flush();
return true;
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
bos.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
return false;
}

/**
* 保存信息至外部存储的缓存目录中
* @param data
* @param fileName
* @param context
* @return
*/
public static boolean saveFileToExternalStoragePrivateCacheDir(byte[] data, String fileName, Context context) {
BufferedOutputStream bos = null;
if (isExternalStorageMounted()) {
File file = context.getExternalCacheDir();
try {
bos = new BufferedOutputStream(new FileOutputStream(new File(file, fileName)));
bos.write(data);
bos.flush();
return true;
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
bos.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
return false;
}

/**
* 获取外部存储中指定位置的文件
* @param fileDir 指定位置
* @return
*/
public static byte[] loadFileFromExternalStorage(String fileDir) {
BufferedInputStream bis = null;
ByteArrayOutputStream baos = new ByteArrayOutputStream();

try {
bis = new BufferedInputStream(new FileInputStream(new File(fileDir)));
byte[] buffer = new byte[8 * 1024];
int c = 0;
while ((c = bis.read(buffer)) != -1) {
baos.write(buffer, 0, c);
baos.flush();
}
return baos.toByteArray();
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
baos.close();
bis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return null;
}


/**
* 保存bitmap图片到外部存储的私有Files目录
* @param bitmap
* @param fileName
* @param context
* @return
*/
public static boolean saveBitmapToExternalStoragePrivateCacheDir(Bitmap bitmap, String fileName, Context context) {
if (isExternalStorageMounted()) {
BufferedOutputStream bos = null;
// 获取私有的Cache缓存目录
File file = context.getExternalCacheDir();

try {
bos = new BufferedOutputStream(new FileOutputStream(new File(file, fileName)));
if (fileName != null && (fileName.contains(".png") || fileName.contains(".PNG"))) {
bitmap.compress(Bitmap.CompressFormat.PNG, 100, bos);
} else {
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, bos);
}
bos.flush();
} catch (Exception e) {
e.printStackTrace();
} finally {
if (bos != null) {
try {
bos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return true;
} else {
return false;
}
}

/**
* 保存bitmap图片到外部存储的私有Cache目录
* @param bitmap
* @param fileName
* @param context
* @return
*/
public static boolean saveBitmapToExternalStoragePrivateFilesDir(Bitmap bitmap, String fileName, Context context) {
if (isExternalStorageMounted()) {
BufferedOutputStream bos = null;
// 获取私有的Cache缓存目录
File file = context.getExternalFilesDir(null);

try {
bos = new BufferedOutputStream(new FileOutputStream(new File(file, fileName)));
if (fileName != null && (fileName.contains(".png") || fileName.contains(".PNG"))) {
bitmap.compress(Bitmap.CompressFormat.PNG, 100, bos);
} else {
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, bos);
}
bos.flush();
} catch (Exception e) {
e.printStackTrace();
} finally {
if (bos != null) {
try {
bos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return true;
} else {
return false;
}
}

/**
* 读取外部存储中指定位置的Bitmap
* @param filePath
* @return
*/
public Bitmap loadBitmapFromExternalStorage(String filePath) {
byte[] data = loadFileFromExternalStorage(filePath);
if (data != null) {
Bitmap bm = BitmapFactory.decodeByteArray(data, 0, data.length);
if (bm != null) {
return bm;
}
}
return null;
}

/**
* 获取外部存储公有目录的路径
* @param type
* @return
*/
public static String getExternalStoragePublicDir(String type) {
return Environment.getExternalStoragePublicDirectory(type).toString();
}

/**
* 获取外部存储私有Files目录的路径
* @param context
* @param type
* @return
*/
public static String getExternalStoragePrivateFilesDir(Context context, String type) {
return context.getExternalFilesDir(type).getAbsolutePath();
}

/**
* 获取外部存储私有Cache目录的路径
* @param context
* @return
*/
public static String getExternalStoragePrivateCacheDir(Context context) {
return context.getExternalCacheDir().getAbsolutePath();
}

/**
* 判断外部存储中指定位置的文件是否存在
* @param filePath
* @return
*/
public static boolean isFileExist(String filePath) {
File file = new File(filePath);
return file.isFile();
}

/**
* 删除外部存储中指定位置的文件
* @param filePath
* @return
*/
public static boolean removeFileFromExternalStorage(String filePath) {
File file = new File(filePath);
if (file.exists()) {
try {
file.delete();
return true;
} catch (Exception e) {
return false;
}
} else {
return false;
}
}
}

五. 源代码

Github地址-external_storage模块

参考文章

Environment.getExternalStorageDirectory()获取的到底是内部存储卡还是外部存储卡?

Android系统中内部存储和外部存储(公有目录、私有目录、缓存目录)详解

是时候弄清楚getExternalStorageDirectory()和getExternalFilesDir()的区别了

彻底理解android中的内部存储与外部存储