Android 使用的文件系统类似于其他平台上基于磁盘的文件系统。该系统为您提供了以下几种保存应用数据的选项:
下表汇总了这些选项的特点:
内容类型 | 访问方法 | 所需权限 | 其他应用是否可以访问 | 卸载应用是否移除 |
---|---|---|---|---|
仅供您的应用使用的文件 | 从内部存储空间访问,可以使用 getFilesDir() 或 getCacheDir() 方法; 从外部存储空间访问,可以使用 getExternalFilesDir() 或 getExternalCacheDir() 方法 |
从内部存储空间访问不需要任何权限 如果应用在搭载 Android 4.4(API 级别 19)或更高版本的设备上运行,从外部存储空间访问不需要任何权限 |
如果文件存储在内部存储空间中的目录内,则不能访问 如果文件存储在外部存储空间中的目录内,则可以访问 |
是 |
可共享的媒体文件(图片、音频文件、视频) | MediaStore API | 在 Android 10(API 级别 29)或更高版本中,访问其他应用的文件需要 READ_EXTERNAL_STORAGE 或 WRITE_EXTERNAL_STORAGE 权限 在 Android 9(API 级别 28)或更低版本中,访问所有文件均需要相关权限 |
是,但其他应用需要 READ_EXTERNAL_STORAGE 权限 | 否 |
键值对 | SharedPreferences | 无 | 否 | 是 |
结构化数据 | 数据库 Room | 无 | 否 | 是 |
外部共享存储空间文件 | 通过路径或者Uri访问 | 需要 READ_EXTERNAL_STORAGE 或 WRITE_EXTERNAL_STORAGE 权限 android 11增加MANAGE_EXTERNAL_STORAGE权限 |
是 | 否 |
内部存储:/data/data/packagename/
外部存储:/sdcard/Android/data/packagename/
这两个目录本App无需申请访问权限即可使用。使用方式也很简单,直接通过路径访问即可。
public static String readFile(Context context, String fileName) {
File file = new File(context.getCacheDir(), fileName);
try {
FileInputStream fis = new FileInputStream(file);
BufferedReader br = new BufferedReader(new InputStreamReader(fis));
Log.d(TAG, "readFile: " + br.readLine());
return br.readLine();
} catch (Exception e) {
e.printStackTrace();
}
return "";
}
public static void writeFile(Context context, String fileName, String content) {
File file = new File(context.getCacheDir(), fileName);
try {
FileOutputStream fos = new FileOutputStream(file);
fos.write(content.getBytes());
fos.close();
} catch (Exception e) {
e.printStackTrace();
}
}
Android 6.0 之前是无需申请动态权限的,在AndroidManifest.xml 里声明存储权限:
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
Android 6.0 之后除了在AndroidManifest.xml 里声明存储权限,还需要动态申请权限。
//检查权限,并返回需要申请的权限列表
private List<String> checkPermission(Context context, String[] checkList) {
List<String> list = new ArrayList<>();
for (int i = 0; i < checkList.length; i++) {
if (PackageManager.PERMISSION_GRANTED != ActivityCompat
.checkSelfPermission(context, checkList[i])) {
list.add(checkList[i]);
}
}
return list;
}
//申请权限
private void requestPermission(Activity activity, String requestPermissionList[]) {
ActivityCompat.requestPermissions(activity, requestPermissionList, 100);
}
//用户作出选择后,返回申请的结果
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
@NonNull int[] grantResults) {
if (requestCode == 100) {
for (int i = 0; i < permissions.length; i++) {
if (permissions[i].equals(Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
if (grantResults[i] == PackageManager.PERMISSION_GRANTED) {
Toast.makeText(MainActivity.this, "存储权限申请成功", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(MainActivity.this, "存储权限申请失败", Toast.LENGTH_SHORT).show();
}
}
}
}
}
//测试申请存储权限
private void testPermission(Activity activity) {
String[] checkList = new String[]{
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.READ_EXTERNAL_STORAGE};
List<String> needRequestList = checkPermission(activity, checkList);
if (needRequestList.isEmpty()) {
Toast.makeText(MainActivity.this, "无需申请权限", Toast.LENGTH_SHORT).show();
} else {
requestPermission(activity, needRequestList.toArray(new String[needRequestList.size()]));
}
}
以图片为例,假设图片存储在/sdcard/Pictures/test.jpg
private Bitmap getBitmap(String fileName) {
//获取目录:/storage/emulated/0/
File rootFile = Environment.getExternalStorageDirectory();
String imagePath = rootFile.getAbsolutePath() + File.separator + Environment.DIRECTORY_PICTURES + File.separator + fileName;
return BitmapFactory.decodeFile(imagePath);
}
private Bitmap getBitmap(Context context, String fileName) {
ContentResolver contentResolver = context.getContentResolver();
//查询条件
String selection = MediaStore.Images.Media.DISPLAY_NAME + " = ?";
String[] selectionArgs = new String[]{
fileName};
Cursor cursor = contentResolver
.query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, null, selection, selectionArgs, null);
while (cursor.moveToNext()) {
//获取图片路径
String imagePath = cursor.getString(cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATA));
return BitmapFactory.decodeFile(imagePath);
}
return null;
}
private Bitmap getBitmap(Context context, String fileName) throws FileNotFoundException {
ContentResolver contentResolver = context.getContentResolver();
String selection = MediaStore.Images.Media.DISPLAY_NAME + " = ?";
String[] selectionArgs = new String[]{
fileName};
Cursor cursor = contentResolver
.query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, null, selection, selectionArgs, null);
while (cursor.moveToNext()) {
long id = cursor.getLong(cursor.getColumnIndex(MediaStore.Images.Media._ID));
//获取Uri
Uri contentUri = ContentUris.withAppendedId(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
id
);
//通过Uri构造InputStream
InputStream inputStream = contentResolver.openInputStream(contentUri);
//通过InputStream获取Bitmap
return BitmapFactory.decodeStream(inputStream);
}
return null;
}
Android 10及以上版本无法直接通过路径获取到文件,即 BitmapFactory.decodeFile(imagePath)
无法正常使用,会提示没有权限。只能通过 Uri
方式获取到图片资源。
SharedPreferences 主要用来保存相对较小键值对集合,使用也十分简单。
Context context = getActivity();
//创建SharedPreferences
SharedPreferences sharedPref = context.getSharedPreferences(
name, Context.MODE_PRIVATE);
//通过editor 写入数据
SharedPreferences.Editor editor = sharedPref.edit();
editor.putInt(key, value);
editor.commit();
//读取数据
int data = sharedPref.getInt(key, defaultValue);
关于 Room 的基本使用可以参考我的另外一篇文章:Android Room 使用
除了应用专属空间之外的存储空间。
与共享媒体文件一样,都需要申请 WRITE_EXTERNAL_STORAGE
和 READ_EXTERNAL_STORAGE
与媒体文件一样,直接构造路径进行访问。
通过 Environment.getExternalStorageDirectory()
获取外部存储路径,然后通过这个路径进行操作即可。
private void testPublicFile() {
File rootFile = Environment.getExternalStorageDirectory();
String imagePath = rootFile.getAbsolutePath() + File.separator + "myDir";
File myDir = new File(imagePath);
if (!myDir.exists()) {
myDir.mkdir();
}
}
在/sdcard/目录下创建 myDir 的文件夹。
Storage Access Framework 简称SAF:存储访问框架。相当于系统内置了文件选择器,通过它可以拿到想要访问的文件信息。
private void startSAF() {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
//选择图片
intent.setType("image/jpeg");
//会跳转到一个文件选择器中
startActivityForResult(intent, 100);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == 100 && data != null) {
//选中返回的图片封装在uri里
Uri uri = data.getData();
Bitmap bitmap = openUri(uri);
if (bitmap != null) {
binding.iv.setImageBitmap(bitmap);
}
}
}
private Bitmap openUri(Uri uri) {
try {
//从uri构造输入流
InputStream fis = getContentResolver().openInputStream(uri);
return BitmapFactory.decodeStream(fis);
} catch (Exception e) {
Log.e(TAG, "openUri: ", e);
}
return null;
}
跳转到系统内置的文件选择器:
该方式不需要申请读写权限,也可以访问外部文件,但是无法直接获取到外部文件的路径,需要通过Uri进行转换。
Android 10 开始增加了分区存储功能,限制APP只能访问应用专属存储空间,无法直接通过路径访问sdcard中的文件,只能使用SAF的方式。
可以避免各个APP无节操的一直往sdcard卡中写入数据。而申请文件读写权限的提示语也做了修改。
Android 6 - Android 9
Android 10及以上版本:
对比低版本,只允许访问照片和媒体的内容。
Android 11 增加了一个所有文件访问权限 MANAGE_EXTERNAL_STORAGE
,该权限比文件读写权限更为严格,不是弹出框提示,而是需要跳转到设置界面进行授权。如果授权该权限,就允许开发者使用路径方式访问外部存储的所有文件。
AndroidManifest.xml 中新增以下权限:
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
代码中动态申请权限:
private void requestPermission() {
//需要判断版本
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
// 先判断有没有权限
if (Environment.isExternalStorageManager()) {
//已经授权
} else {
Intent intent = new Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION);
intent.setData(Uri.parse("package:" + getPackageName()));
startActivityForResult(intent, 101);
}
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == 101 && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
if (Environment.isExternalStorageManager()) {
Toast.makeText(this, "所有文件访问权限授权成功", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(this, "所有文件访问权限授权失败", Toast.LENGTH_SHORT).show();
}
}
}