Android媒体库你了解多少
Android系統中有一個媒體庫,這個大家應該有所了解,平時在開發過程中如果不涉及媒體文件(圖片、音頻、視頻)這塊則很少接觸到。有些時候我們在本地添加一張圖片,但是在相冊中卻無法搜索到,這里主要原因就是沒有通知系統媒體庫刷新導致的。本篇我們就探討下Android上媒體庫的這些事。
為什么通知媒體庫后,媒體庫里就能找到了呢?興許你還會遇到一種情況,就是明明相冊里可以發現這張圖片,可是到圖片的具體路徑下卻找不到這張圖片。到此應該會猜測到是不是媒體庫和本地相冊都持有一份媒體文件信息呢?基本上猜到了八九不離十了,其實媒體庫就是一個數據庫,專門管理媒體文件的相關信息,例如圖片信息,縮略圖等。
多媒體文件管理
Android多媒體文件掃描管理簡單來說,有以下四部分內容:
- 通知:MediaScannerReceiver
- 掃描:MediaScannerService
- 存儲:MediaProvider
- 查詢:MediaStore
MediaScannerReceiver
MediaScannerReceiver主要用于接受掃描通知,然后啟動MediaScannerService進行掃描操作。
@Overridepublic void onReceive(Context context, Intent intent) {final String action = intent.getAction();final Uri uri = intent.getData();if (Intent.ACTION_BOOT_COMPLETED.equals(action)) {// Scan internal only.scan(context, MediaProvider.INTERNAL_VOLUME);} else if (Intent.ACTION_LOCALE_CHANGED.equals(action)) {scanTranslatable(context);} else {if (uri.getScheme().equals("file")) {// handle intents related to external storageString path = uri.getPath();String externalStoragePath = Environment.getExternalStorageDirectory().getPath();String legacyPath = Environment.getLegacyExternalStorageDirectory().getPath();try {path = new File(path).getCanonicalPath();} catch (IOException e) {Log.e(TAG, "couldn't canonicalize " + path);return;}if (path.startsWith(legacyPath)) {path = externalStoragePath + path.substring(legacyPath.length());}Log.d(TAG, "action: " + action + " path: " + path);if (Intent.ACTION_MEDIA_MOUNTED.equals(action)) {// scan whenever any volume is mountedscan(context, MediaProvider.EXTERNAL_VOLUME);} else if (Intent.ACTION_MEDIA_SCANNER_SCAN_FILE.equals(action) &&path != null && path.startsWith(externalStoragePath + "/")) {scanFile(context, path);}}}。。。private void scan(Context context, String volume) {Bundle args = new Bundle();args.putString("volume", volume);context.startService(new Intent(context, MediaScannerService.class).putExtras(args));}private void scanFile(Context context, String path) {Bundle args = new Bundle();args.putString("filepath", path);context.startService(new Intent(context, MediaScannerService.class).putExtras(args));}private void scanTranslatable(Context context) {final Bundle args = new Bundle();args.putBoolean(MediaStore.RETRANSLATE_CALL, true);context.startService(new Intent(context, MediaScannerService.class).putExtras(args));}從onReceive方法中可以看出,MediaScannerReceiver執行scan的時機有四種:
MediaScannerService
MediaScannerService主要負責媒體文件的掃描過程,因此是耗時的,其內部的scan()方法是掃描核心。
private void scan(String[] directories, String volumeName) {...try {...sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_STARTED, uri));try {if (volumeName.equals(MediaProvider.EXTERNAL_VOLUME)) {openDatabase(volumeName);}try (MediaScanner scanner = new MediaScanner(this, volumeName)) {scanner.scanDirectories(directories);}} catch (Exception e) {Log.e(TAG, "exception in MediaScanner.scan()", e);}...} finally {sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_FINISHED, uri));}}核心內容也很簡單,最終調用MediaScanner的scanDirectories()執行文件掃描。其中MediaScanner是一個專門用于媒體文件掃描的類,其scanDirectories()內部核心就是采用ContentProviderClient來對MediaProvider進行數據的更新或刪除操作。
MediaProvider
我們都知道Android有四大組件,Activity、Service、BroadcastReceiver、ContentProvider。MediaProvider就是Android系統中的一個數據庫,類似的還有TelephonyProvider、CalendarProvider、ContactsProvider,這些數據庫的源碼都在/packages/providers/目錄下。
其中MediaProvider又稱多媒體數據庫,保存了手機上存儲的所有媒體文件的信息。這個數據庫存放在/data/data/com.android.providers.media/databases當中,里面有兩個數據庫:internal.db和external.db,internal.db存放的是系統分區的文件信息,開發者是沒法通過接口獲得其中的信息的,而external.db存放的則是我們用戶能看到的存儲區的文件信息,即包含了手機內置存儲,還包含了SD卡。
MediaStore
MediaStore主要用于提供內部或外部存儲中所有可用的媒體文件的各種信息,我們后邊都需要借助此類來進行媒體庫的操作(添加,刪除等)。
媒體庫信息查詢
上邊我們知道MediaStore是專門用于存放多媒體信息的,通過ContentResolver即可對數據庫進行操作。
MediaStore中的資源有四類:
- MediaStore.Files
- MediaStore.Audio
- MediaStore.Images
- MediaStore.Video
這些內部類中都又包含了Media,Thumbnails和相應的MediaColumns,分別提供了媒體信息,縮略信息和操作字段。
查詢
媒體庫是通過ContentResolver來進行查詢的,其核心掃描方法如下:
public final @Nullable Cursor query(@RequiresPermission.Read @NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder@Nullable CancellationSignal cancellationSignal)開發過程中可能根據不同情況調用此方法的重載方法,下邊我們就解析一下這個方法的相關入參。
Uri uri
Uri uri = MediaStore.Video.Media.INTERNAL_CONTENT_URI; Uri uri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;uri表示數據資源路徑,總共有兩種,分別對應內部存儲和外部存儲。
需要注意的是,MediaStore.Files沒有EXTERNAL_CONTENT_URI,所以只能用getContentUri()自行獲取MediaStore.Images.Files.getContentUri("external")
以MediaStore.Images.Media為例,其URI有三種寫法:
Uri uri1 = MediaStore.Images.Media.EXTERNAL_CONTENT_URI; Uri uri2 = MediaStore.Images.Media.getContentUri("external"); Uri uri3 = Uri.parse("content://media/external/images/media");String[] projection
String[] mediaColumns = {MediaStore.Video.Media._ID,MediaStore.Video.Media.DATA,MediaStore.Video.Media.SIZE,MediaStore.Video.Media.DATE_MODIFIED,MediaStore.Video.Media.DURATION};用于指定查詢后返回給用戶的媒體信息,當然你可以理解為對應媒體數據庫中相關字段。
String selection 和 String[] selectionArgs
String selection = MediaStore.Video.Media.MIME_TYPE + "=? or "+ MediaStore.Video.Media.MIME_TYPE + "=? or "+ MediaStore.Video.Media.MIME_TYPE + "=? or "+ MediaStore.Video.Media.MIME_TYPE + "=? or "+ MediaStore.Video.Media.MIME_TYPE + "=?"String[] selectionArgs = new String[]{"video/mp4", "video/avi", "video/quicktime", "video/webm", "video/x-ms-wmv"}定制化查詢條件,這兩個必須結合使用,前者表示條件語句,后者表示對應的條件參數。
String sortOrder
查詢的排序方式
CancellationSignal cancellationSignal
取消正在進行的操作的信號,如果沒有則為空。如果操作被取消,那么在執行查詢時將拋出OperationCanceledException異常
舉栗
1.利用MediaStore.Files,查詢所有類型的文件:
/*** 獲取所有文件**/public static List<FileEntity> getFilesByType(Context context) {List<FileEntity> files = new ArrayList<>();// 掃描files文件庫Cursor c = null;try {mContentResolver = context.getContentResolver();c = mContentResolver.query(MediaStore.Files.getContentUri("external"), null, null, null, null);int columnIndexOrThrow_ID = c.getColumnIndexOrThrow(MediaStore.Files.FileColumns._ID);int columnIndexOrThrow_MIME_TYPE = c.getColumnIndexOrThrow(MediaStore.Files.FileColumns.MIME_TYPE);int columnIndexOrThrow_DATA = c.getColumnIndexOrThrow(MediaStore.Files.FileColumns.DATA);int columnIndexOrThrow_SIZE = c.getColumnIndexOrThrow(MediaStore.Files.FileColumns.SIZE);// 更改時間int columnIndexOrThrow_DATE_MODIFIED = c.getColumnIndexOrThrow(MediaStore.Files.FileColumns.DATE_MODIFIED); int tempId = 0;while (c.moveToNext()) {String path = c.getString(columnIndexOrThrow_DATA);String minType = c.getString(columnIndexOrThrow_MIME_TYPE);LogUtil.d("FileManager", "path:" + path);int position_do = path.lastIndexOf(".");if (position_do == -1) {continue;}int position_x = path.lastIndexOf(File.separator);if (position_x == -1) {continue;}String displayName = path.substring(position_x + 1, path.length());long size = c.getLong(columnIndexOrThrow_SIZE);long modified_date = c.getLong(columnIndexOrThrow_DATE_MODIFIED);File file = new File(path);String time = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date(file.lastModified()));FileEntity info = new FileEntity();info.setName(displayName);info.setPath(path);info.setSize(ShowLongFileSzie(size));info.setId((tempId++) + "");info.setTime(time);files.add(info);}} catch (Exception e) {e.printStackTrace();} finally {if (c != null) {c.close();}}return files;}2.指定獲取文件字段
String[] columns = new String[]{MediaStore.Files.FileColumns._ID, MediaStore.Files.FileColumns.MIME_TYPE, MediaStore.Files.FileColumns.SIZE, MediaStore.Files.FileColumns.DATE_MODIFIED, MediaStore.Files.FileColumns.DATA}; c = mContentResolver.query(MediaStore.Files.getContentUri("external"), columns, null, null, null);3.根據文件夾的名稱查詢
//查找文件夾ScreenRecord下的文件 c = mContentResolver.query(MediaStore.Files.getContentUri("external"), null, MediaStore.Video.Media.BUCKET_DISPLAY_NAME+"=?", "ScreenRecord", null);4.查詢指定類型的文件
String select = "(" + MediaStore.Files.FileColumns.DATA + " LIKE '%.doc'" + " or " + MediaStore.Files.FileColumns.DATA + " LIKE '%.docx'" + ")"; c = mContentResolver.query(MediaStore.Files.getContentUri("external"), null, select , null, null);5.指定排序類型,如根據id倒序查詢
c = mContentResolver.query(MediaStore.Files.getContentUri("external"), null, null, null, MediaStore.Files.FileColumns._ID+"DESC");刷新媒體庫
媒體庫刷新方法
刷新媒體庫常用的有如下幾種方式:
通過ContentProvider操作媒體數據庫
ContentValues values = new ContentValues(4); values.put(MediaStore.Video.Media.TITLE, ""); values.put(MediaStore.Video.Media.MIME_TYPE, minetype); values.put(MediaStore.Video.Media.DATA, path); values.put(MediaStore.Video.Media.DURATION, duration_int); context.getContentResolver().insert(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, values);和上邊所講的媒體庫信息查詢一樣,直接對數據庫操作。需要注意的是這種方式不能和其他刷新媒體庫方式公用,有可能同時存入兩張一模一樣的文件。
發送廣播更新MediaStore進行刷新
Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE); intent.setData(Uri.fromFile(new File(filePath))); context.sendBroadcast(intent);在Android4.4之前,是可以通過ACTION_MEDIA_MOUNTED廣播,來通知系統刷新MediaStore的,4.4后系統封閉這種方式,取而代之的是ACTION_MEDIA_SCANNER_SCAN_FILE,建議單個文件掃描插入。
通過操作MediaScannerConnection類進行刷新
Android4.0系統API中多了一個更新媒體庫的方法——MediaScannerConnection,這也是我們比較推薦的。MediaScannerConnection有一個靜態方法scanFile(),可直接操作此方法完成媒體庫刷新操作。并且可對其刷新完成后回調更新。
public static void insert(Context context, String[] paths, String[] types) {MediaScannerConnection.scanFile(context,paths,types,new MediaScannerConnection.OnScanCompletedListener() {@Overridepublic void onScanCompleted(String path, Uri uri) {LogUtils.i(TAG, "insert onScanCompleted path " + path + " uri " + uri);}}); }當然你也可以實現MediaScannerConnection.MediaScannerConnectionClient來進行掃描,在構造方法中執行connect(),在onScanCompleted()方法中執行disconnect()關閉鏈接,在onMediaScannerConnected()中執行scanFile()進行掃描。
媒體文件添加后刷新
通常我們在圖片或者音視頻添加后,在需要更新的地方執行刷新媒體庫操作才能在媒體庫中看到。
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {new MediaScanner(context, file); } else {Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);intent.setData(Uri.fromFile(file));context.sendBroadcast(intent); }媒體文件刪除后刷新
有時候我們需要刪除本地圖片同時又希望刷新一下媒體庫,讓媒體庫中去除此圖片,以上邊刷新媒體庫的方式大多都是insert模式,那我們只能直接操作數據庫了。需要注意file.delete()后不可立即將file置為null;
if (!file.exists()) {String filePath = file.getAbsolutePath();if (filePath.endsWith(".mp4")) {context.getContentResolver().delete(MediaStore.Video.Media.EXTERNAL_CONTENT_URI,MediaStore.Audio.Media.DATA + "= \"" + filePath + "\"",null);} else if (filePath.endsWith(".jpg") || filePath.endsWith(".png") || filePath.endsWith(".bmp")) {context.getContentResolver().delete(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,MediaStore.Audio.Media.DATA + "= \"" + filePath + "\"",null);}return; }一個項目中使用的例子:
public class MediaScanner implements MediaScannerConnection.MediaScannerConnectionClient {private static final String TAG = MediaScanner.class.getSimpleName();/*** 刷新媒體庫** @param context* @param file*/public static void refresh(Context context, File file) {if (context == null || file == null) {return;}//如果圖片不存在,刪除媒體庫中記錄if (!file.exists()) {String filePath = file.getAbsolutePath();if (filePath.endsWith(".mp4")) {context.getContentResolver().delete(MediaStore.Video.Media.EXTERNAL_CONTENT_URI,MediaStore.Audio.Media.DATA + "= \"" + filePath + "\"",null);} else if (filePath.endsWith(".jpg") || filePath.endsWith(".png") || filePath.endsWith(".bmp")) {context.getContentResolver().delete(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,MediaStore.Audio.Media.DATA + "= \"" + filePath + "\"",null);}return;}//4.0以上的系統使用MediaScanner更新if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {new MediaScanner(context, file);} else {Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);intent.setData(Uri.fromFile(file));context.sendBroadcast(intent);}}private File mFile;private MediaScannerConnection mMsc;private MediaScanner(Context context, File file) {this.mFile = file;this.mMsc = new MediaScannerConnection(context, this);mMsc.connect();}@Overridepublic void onMediaScannerConnected() {mMsc.scanFile(mFile.getAbsolutePath(), null);}@Overridepublic void onScanCompleted(String path, Uri uri) {mMsc.disconnect();} }刷新過濾
有時候,我們有一些目錄下的媒體文件,并不想讓MediaStore掃描到,例如在SDCard上緩存的圖片、圖標等,這些我們都不想出現在系統相冊內。怎么辦呢?
很簡單,文件夾中新建一個.nomedia的空文件,會屏蔽掉系統默認的媒體庫掃描。帶有該文件的文件夾只能通過文件遍歷的方式進行掃描。
總結
以上是有關媒體庫開發過程中的知識點,系統媒體庫是維護了一個有關媒體文件的數據庫,在開發過程中只有在需要相冊內的圖片或者音視頻更新時才需要刷新媒體庫,這點個人建議適量使用不可濫用,否則有可能會造成媒體庫文件泛濫,或者媒體庫中有相應文件的預覽,本地卻不存在此文件的bug。
參考
- https://developer.android.com/guide/topics/data/data-storage.html
- https://blog.csdn.net/yann02/article/details/92844364
- https://juejin.im/post/5ae0541df265da0b9d77e45a
- https://zhuanlan.zhihu.com/p/46533159
- https://www.bbsmax.com/A/amd0omej5g/
總結
以上是生活随笔為你收集整理的Android媒体库你了解多少的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Uniswap原理
- 下一篇: eog命令在播放图片时候的用法总结