一、數(shù)據(jù)存儲方式介紹
Android 使用的文件系統(tǒng)類似于其他平臺上基于磁盤的文件系統(tǒng)。該系統(tǒng)為您提供了以下幾種保存應(yīng)用數(shù)據(jù)的選項:
- 應(yīng)用專屬存儲空間:存儲僅供應(yīng)用使用的文件歪玲,可以存儲到內(nèi)部存儲卷中的專屬目錄或外部存儲空間中的其他專屬目錄载矿。使用內(nèi)部存儲空間中的目錄保存其他應(yīng)用不應(yīng)訪問的敏感信息漆羔。
- 共享存儲:存儲您的應(yīng)用打算與其他應(yīng)用共享的文件剿干,包括媒體、文檔和其他文件抓谴。
- 偏好設(shè)置:以鍵值對形式存儲私有原始數(shù)據(jù)暮蹂。
- 數(shù)據(jù)庫:使用 Room 持久性庫將結(jié)構(gòu)化數(shù)據(jù)存儲在專用數(shù)據(jù)庫中。
下表匯總了這些選項的特點:
內(nèi)容類型 | 訪問方法 | 所需權(quán)限 | 其他應(yīng)用是否可以訪問 | 卸載應(yīng)用是否移除 |
---|---|---|---|---|
僅供您的應(yīng)用使用的文件 | 從內(nèi)部存儲空間訪問癌压,可以使用 getFilesDir() 或 getCacheDir() 方法仰泻; 從外部存儲空間訪問,可以使用 getExternalFilesDir() 或 getExternalCacheDir() 方法 |
從內(nèi)部存儲空間訪問不需要任何權(quán)限 如果應(yīng)用在搭載 Android 4.4(API 級別 19)或更高版本的設(shè)備上運行滩届,從外部存儲空間訪問不需要任何權(quán)限 |
如果文件存儲在內(nèi)部存儲空間中的目錄內(nèi)集侯,則不能訪問 如果文件存儲在外部存儲空間中的目錄內(nèi)被啼,則可以訪問 |
是 |
可共享的媒體文件(圖片、音頻文件棠枉、視頻) | MediaStore API | 在 Android 10(API 級別 29)或更高版本中浓体,訪問其他應(yīng)用的文件需要 READ_EXTERNAL_STORAGE 或 WRITE_EXTERNAL_STORAGE 權(quán)限 在 Android 9(API 級別 28)或更低版本中,訪問所有文件均需要相關(guān)權(quán)限 |
是辈讶,但其他應(yīng)用需要 READ_EXTERNAL_STORAGE 權(quán)限 | 否 |
鍵值對 | SharedPreferences | 無 | 否 | 是 |
結(jié)構(gòu)化數(shù)據(jù) | 數(shù)據(jù)庫 Room | 無 | 否 | 是 |
外部共享存儲空間文件 | 通過路徑或者Uri訪問 | 需要 READ_EXTERNAL_STORAGE 或 WRITE_EXTERNAL_STORAGE 權(quán)限 android 11增加MANAGE_EXTERNAL_STORAGE權(quán)限 |
是 | 否 |
二命浴、不同存儲方式使用
2.1 應(yīng)用專屬存儲空間
內(nèi)部存儲:/data/data/packagename/
外部存儲:/sdcard/Android/data/packagename/
這兩個目錄本App無需申請訪問權(quán)限即可使用。使用方式也很簡單贱除,直接通過路徑訪問即可生闲。
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();
}
}
2.2 共享媒體文件
申請權(quán)限
Android 6.0 之前是無需申請動態(tài)權(quán)限的,在AndroidManifest.xml 里聲明存儲權(quán)限:
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
Android 6.0 之后除了在AndroidManifest.xml 里聲明存儲權(quán)限月幌,還需要動態(tài)申請權(quán)限碍讯。
//檢查權(quán)限,并返回需要申請的權(quán)限列表
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;
}
//申請權(quán)限
private void requestPermission(Activity activity, String requestPermissionList[]) {
ActivityCompat.requestPermissions(activity, requestPermissionList, 100);
}
//用戶作出選擇后扯躺,返回申請的結(jié)果
@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, "存儲權(quán)限申請成功", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(MainActivity.this, "存儲權(quán)限申請失敗", Toast.LENGTH_SHORT).show();
}
}
}
}
}
//測試申請存儲權(quán)限
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, "無需申請權(quán)限", Toast.LENGTH_SHORT).show();
} else {
requestPermission(activity, needRequestList.toArray(new String[needRequestList.size()]));
}
}
訪問方式
1. 通過路徑訪問
以圖片為例捉兴,假設(shè)圖片存儲在/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);
}
2. 通過MediaStore獲取路徑
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;
}
3. 通過MediaStore獲取Uri
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構(gòu)造InputStream
InputStream inputStream = contentResolver.openInputStream(contentUri);
//通過InputStream獲取Bitmap
return BitmapFactory.decodeStream(inputStream);
}
return null;
}
適配Android 10
Android 10及以上版本無法直接通過路徑獲取到文件,即 BitmapFactory.decodeFile(imagePath)
無法正常使用录语,會提示沒有權(quán)限倍啥。只能通過 Uri
方式獲取到圖片資源。
2.3 SharedPreferences
SharedPreferences 主要用來保存相對較小鍵值對集合钦无,使用也十分簡單逗栽。
Context context = getActivity();
//創(chuàng)建SharedPreferences
SharedPreferences sharedPref = context.getSharedPreferences(
name, Context.MODE_PRIVATE);
//通過editor 寫入數(shù)據(jù)
SharedPreferences.Editor editor = sharedPref.edit();
editor.putInt(key, value);
editor.commit();
//讀取數(shù)據(jù)
int data = sharedPref.getInt(key, defaultValue);
2.4 數(shù)據(jù)庫
關(guān)于 Room 的基本使用可以參考我的另外一篇文章:Android Room 使用
2.5 外部非媒體的共享存儲空間
除了應(yīng)用專屬空間之外的存儲空間。
申請權(quán)限
與共享媒體文件一樣失暂,都需要申請 WRITE_EXTERNAL_STORAGE
和 READ_EXTERNAL_STORAGE
訪問方式
1. 通過路徑訪問
與媒體文件一樣,直接構(gòu)造路徑進行訪問鳄虱。
通過 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/目錄下創(chuàng)建 myDir 的文件夾拙已。
2. 通過SAF訪問
Storage Access Framework 簡稱SAF:存儲訪問框架决记。相當于系統(tǒng)內(nèi)置了文件選擇器,通過它可以拿到想要訪問的文件信息倍踪。
private void startSAF() {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
//選擇圖片
intent.setType("image/jpeg");
//會跳轉(zhuǎn)到一個文件選擇器中
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構(gòu)造輸入流
InputStream fis = getContentResolver().openInputStream(uri);
return BitmapFactory.decodeStream(fis);
} catch (Exception e) {
Log.e(TAG, "openUri: ", e);
}
return null;
}
跳轉(zhuǎn)到系統(tǒng)內(nèi)置的文件選擇器:
該方式不需要申請讀寫權(quán)限系宫,也可以訪問外部文件,但是無法直接獲取到外部文件的路徑建车,需要通過Uri進行轉(zhuǎn)換扩借。
適配Android 10
Android 10 開始增加了分區(qū)存儲功能,限制APP只能訪問應(yīng)用專屬存儲空間缤至,無法直接通過路徑訪問sdcard中的文件潮罪,只能使用SAF的方式。
可以避免各個APP無節(jié)操的一直往sdcard卡中寫入數(shù)據(jù)。而申請文件讀寫權(quán)限的提示語也做了修改嫉到。
Android 6 - Android 9:
Android 10及以上版本:
對比低版本沃暗,只允許訪問照片和媒體的內(nèi)容。
適配Android 11
Android 11 增加了一個所有文件訪問權(quán)限 MANAGE_EXTERNAL_STORAGE
何恶,該權(quán)限比文件讀寫權(quán)限更為嚴格孽锥,不是彈出框提示,而是需要跳轉(zhuǎn)到設(shè)置界面進行授權(quán)细层。如果授權(quán)該權(quán)限惜辑,就允許開發(fā)者使用路徑方式訪問外部存儲的所有文件。
AndroidManifest.xml 中新增以下權(quán)限:
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
代碼中動態(tài)申請權(quán)限:
private void requestPermission() {
//需要判斷版本
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
// 先判斷有沒有權(quán)限
if (Environment.isExternalStorageManager()) {
//已經(jīng)授權(quán)
} 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, "所有文件訪問權(quán)限授權(quán)成功", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(this, "所有文件訪問權(quán)限授權(quán)失敗", Toast.LENGTH_SHORT).show();
}
}
}
授權(quán)方式如下圖: