本文以 Android 9.0 為準(zhǔn)
概述
在《Android MediaProvider》 這篇文章中分析了 MediaProvider 的源碼,不過當(dāng)多個外部存儲設(shè)備,并且存儲大量文件時嘀韧,MediaProvider 會存在掃描慢的情況揪阶,為了加快其掃描速度,修改策略為:
- 在第一次外部存儲設(shè)備插入時睬罗,掃描數(shù)據(jù)存入數(shù)據(jù)庫轨功,拔出時不刪除數(shù)據(jù)庫
- 針對多個外部存儲設(shè)備,存儲多個數(shù)據(jù)表
- 第二次插入外部存儲設(shè)備時容达,找到對應(yīng)的數(shù)據(jù)庫古涧,清理數(shù)據(jù)庫中的臟數(shù)據(jù)
當(dāng)然,這種方式花盐,在第一次掃描速度上羡滑,沒有進(jìn)行優(yōu)化,也沒有優(yōu)化的空間算芯,只能在第二次插拔上做文章柒昏,因為原生的 MediaProvider 會在拔出外部存儲設(shè)備時刪除數(shù)據(jù)庫。
并且目前連接多個外部設(shè)備時熙揍,只操作一個外部數(shù)據(jù)庫(數(shù)據(jù)庫位置:/data/user/userid/com.android.providers.media/databases/external.db
)职祷,這樣就會導(dǎo)致數(shù)據(jù)量大,操作緩慢届囚。
分析過程
通常來說有梆,在 ContentProvider 中打開數(shù)據(jù)庫的操作,應(yīng)該在 onCreate() 方法中進(jìn)行的 意系。
@Override
public boolean onCreate() {
...
// DatabaseHelper緩存
mDatabases = new HashMap<String, DatabaseHelper>();
// 綁定內(nèi)部存儲數(shù)據(jù)庫
attachVolume(INTERNAL_VOLUME);
...
String state = Environment.getExternalStorageState();
if (Environment.MEDIA_MOUNTED.equals(state) ||
Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) {
// 如果已掛載外部存儲泥耀,綁定外部存儲數(shù)據(jù)庫
attachVolume(EXTERNAL_VOLUME);
}
...
return true;
}
可以看到打開數(shù)據(jù)庫的位置在 attachVolume() 方法中:
/**
* Attach the database for a volume (internal or external).
* Does nothing if the volume is already attached, otherwise
* checks the volume ID and sets up the corresponding database.
*
* @param volume to attach, either {@link #INTERNAL_VOLUME} or {@link #EXTERNAL_VOLUME}.
* @return the content URI of the attached volume.
*/
private Uri attachVolume(String volume) {
...
// Update paths to reflect currently mounted volumes
// 更新路徑以反映當(dāng)前裝載的卷
updateStoragePaths();
DatabaseHelper helper = null;
synchronized (mDatabases) {
helper = mDatabases.get(volume);
// 判斷是否已經(jīng)attached過了
if (helper != null) {
if (EXTERNAL_VOLUME.equals(volume)) {
// 確保默認(rèn)的文件夾已經(jīng)被創(chuàng)建在掛載的主要存儲設(shè)備上,
// 對每個存儲卷只做一次這種操作昔字,所以當(dāng)用戶手動刪除時不會打擾
ensureDefaultFolders(helper, helper.getWritableDatabase());
}
return Uri.parse("content://media/" + volume);
}
Context context = getContext();
if (INTERNAL_VOLUME.equals(volume)) {
// 如果是內(nèi)部存儲則直接實例化DatabaseHelper
helper = new DatabaseHelper(context, INTERNAL_DATABASE_NAME, true,
false, mObjectRemovedCallback);
} else if (EXTERNAL_VOLUME.equals(volume)) {
// 如果是外部存儲的操作爆袍,只獲取主要的外部卷ID
// Only extract FAT volume ID for primary public
final VolumeInfo vol = mStorageManager.getPrimaryPhysicalVolume();
if (vol != null) {
final StorageVolume actualVolume = mStorageManager.getPrimaryVolume();
final int volumeId = actualVolume.getFatVolumeId();
// Must check for failure!
// If the volume is not (yet) mounted, this will create a new
// external-ffffffff.db database instead of the one we expect. Then, if
// android.process.media is later killed and respawned, the real external
// database will be attached, containing stale records, or worse, be empty.
// 數(shù)據(jù)庫都是以類似 external-ffffffff.db 的形式命名的首繁,
// 后面的 8 個 16 進(jìn)制字符是該 SD 卡 FAT 分區(qū)的 Volume ID。
// 該 ID 是分區(qū)時決定的陨囊,只有重新分區(qū)或者手動改變才會更改弦疮,
// 可以防止插入不同 SD 卡時數(shù)據(jù)庫沖突。
if (volumeId == -1) {
String state = Environment.getExternalStorageState();
if (Environment.MEDIA_MOUNTED.equals(state) ||
Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) {
// This may happen if external storage was _just_ mounted. It may also
// happen if the volume ID is _actually_ 0xffffffff, in which case it
// must be changed since FileUtils::getFatVolumeId doesn't allow for
// that. It may also indicate that FileUtils::getFatVolumeId is broken
// (missing ioctl), which is also impossible to disambiguate.
// 已經(jīng)掛載但是sd卡是只讀狀態(tài)
Log.e(TAG, "Can't obtain external volume ID even though it's mounted.");
} else {
// 還沒有掛載
Log.i(TAG, "External volume is not (yet) mounted, cannot attach.");
}
throw new IllegalArgumentException("Can't obtain external volume ID for " +
volume + " volume.");
}
// generate database name based on volume ID
// 根據(jù)volume ID設(shè)置數(shù)據(jù)庫的名稱
String dbName = "external-" + Integer.toHexString(volumeId) + ".db";
// 創(chuàng)建外部存儲數(shù)據(jù)庫
helper = new DatabaseHelper(context, dbName, false,
false, mObjectRemovedCallback);
mVolumeId = volumeId;
} else {
// external database name should be EXTERNAL_DATABASE_NAME
// however earlier releases used the external-XXXXXXXX.db naming
// for devices without removable storage, and in that case we need to convert
// to this new convention
// 外部數(shù)據(jù)庫名稱應(yīng)為EXTERNAL_DATABASE_NAME
// 但是較早的版本對沒有可移動存儲的設(shè)備使用external-XXXXXXXX.db命名
// 在這種情況下蜘醋,我們需要轉(zhuǎn)換為新的約定
...
// 根據(jù)之前轉(zhuǎn)換的數(shù)據(jù)庫名胁塞,創(chuàng)建數(shù)據(jù)庫
helper = new DatabaseHelper(context, dbFile.getName(), false,
false, mObjectRemovedCallback);
}
} else {
throw new IllegalArgumentException("There is no volume named " + volume);
}
// 緩存起來,標(biāo)識已經(jīng)創(chuàng)建過了數(shù)據(jù)庫
mDatabases.put(volume, helper);
...
}
if (EXTERNAL_VOLUME.equals(volume)) {
// 給外部存儲創(chuàng)建默認(rèn)的文件夾
ensureDefaultFolders(helper, helper.getWritableDatabase());
}
return Uri.parse("content://media/" + volume);
}
可以總結(jié):
- 如果此存儲卷的 DatabaseHelper 已創(chuàng)建压语,則不執(zhí)行任何操作啸罢。
- 對于內(nèi)部存儲,數(shù)據(jù)存儲在 internal.db 中胎食;
- 對于外部存儲扰才,獲取主要的外部卷 ID,根據(jù) ID 數(shù)據(jù)存儲在 external-XXXXXXXX.db 中厕怜;
- 對于較早版本中的外部存儲衩匣,數(shù)據(jù)保存在 external.db 中,需要轉(zhuǎn)換為新的約定粥航;
- 緩存 DatabaseHelper琅捏。
切入點(diǎn)是,對于外部存儲递雀,創(chuàng)建 DatabaseHelper 時柄延,直接使用參數(shù)傳遞過來的 volumeId,打開 external-XXXXXXXX.db缀程。
代碼修改
一搜吧、MediaProvider
private Uri attachVolume(String volume) {
...
if (INTERNAL_VOLUME.equals(volume)) {
...
} else if (EXTERNAL_VOLUME.equals(volume)) {
// Only extract FAT volume ID for primary public
final VolumeInfo vol = mStorageManager.getPrimaryPhysicalVolume();
if (vol != null) {
final StorageVolume actualVolume = mStorageManager.getPrimaryVolume();
final int volumeId = actualVolume.getFatVolumeId();
...
} else {
...
}
} else {
throw new IllegalArgumentException("There is no volume named " + volume);
}
...
}
修改為
private Uri attachVolume(String volume) {
...
if (INTERNAL_VOLUME.equals(volume)) {
...
} else {
final VolumeInfo vol = mStorageManager.getPrimaryPhysicalVolume();
if (vol != null) {
int volumeId = -1;
try {
volumeId = Integer.valueOf(volume);
} catch (NumberFormatException e) {
e.printStackTrace();
}
...
} else {
...
}
}
...
}
那么 attachVolume(String volume) ,對于外部存儲傳入的 volume 就不應(yīng)是 EXTERNAL_VOLUME 了杠输,而應(yīng)該是外部存儲的 volumeId赎败。
volumeId 可以通過 StorageVolume 的 getFatVolumeId 方法獲取,本質(zhì)就是將 UUID 轉(zhuǎn)換的 int 值蠢甲,而 UUID 是一個長度為 9 的字符串僵刮,類似與 “57E9-73B0”這樣。
繼而需要找到 attachVolume(String volume) 調(diào)用的地方鹦牛,修改參數(shù)為 volumeId搞糕,找到有兩個地方調(diào)用了:
1、onCreate() 方法中:
String state = Environment.getExternalStorageState();
if (Environment.MEDIA_MOUNTED.equals(state) ||
Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) {
attachVolume(EXTERNAL_VOLUME);
}
可以通過 StorageManager.getStorageVolumes() 獲取全部的 StorageVolume曼追,因此修改為
final List<StorageVolume> storageVolumes = mStorageManager.getStorageVolumes();
String state;
for (StorageVolume storageVolume : storageVolumes) {
state = storageVolume.getState();
if (Environment.MEDIA_MOUNTED.equals(state) ||
Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) {
attachVolume(String.valueOf(storageVolume.getFatVolumeId()));
}
}
2窍仰、在 insertInternal() 方法中:
private Uri insertInternal(Uri uri, int match, ContentValues initialValues,
ArrayList<Long> notifyRowIds) {
...
switch (match) {
case VOLUMES:
{
String name = initialValues.getAsString("name");
Uri attachedVolume = attachVolume(name);
if (mMediaScannerVolume != null && mMediaScannerVolume.equals(name)) {
DatabaseHelper dbhelper = getDatabaseForUri(attachedVolume);
if (dbhelper == null) {
Log.e(TAG, "no database for attached volume " + attachedVolume);
} else {
dbhelper.mScanStartTime = SystemClock.currentTimeMicro();
}
}
return attachedVolume;
}
}
...
}
這個方法調(diào)用的源頭是在 MediaScannerService 中,下面分析掃描流程礼殊。
二驹吮、MediaScannerService
private void openDatabase(String volumeName) {
try {
ContentValues values = new ContentValues();
values.put("name", volumeName);
getContentResolver().insert(Uri.parse("content://media/"), values);
} catch (IllegalArgumentException ex) {
Log.w(TAG, "failed to open media database");
}
}
private void scan(String[] directories, String volumeName) {
Uri uri = Uri.parse("file://" + directories[0]);
// don't sleep while scanning
mWakeLock.acquire();
try {
ContentValues values = new ContentValues();
values.put(MediaStore.MEDIA_SCANNER_VOLUME, volumeName);
Uri scanUri = getContentResolver().insert(MediaStore.getMediaScannerUri(), values);
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);
}
getContentResolver().delete(scanUri, null, null);
} finally {
sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_FINISHED, uri));
mWakeLock.release();
}
}
由于 volumeName 不再是 EXTERNAL_VOLUME针史,修改 openDatabase() 執(zhí)行的判斷語句。
if (!volumeName.equals(MediaProvider.INTERNAL_VOLUME)) {
openDatabase(volumeName);
}
注意在調(diào)用 scan() 方法的地方碟狞,傳入的 directories 根據(jù) volume 做了一個判斷啄枕,注意也要修正過來。
private final class ServiceHandler extends Handler {
@Override
public void handleMessage(Message msg) {
...
String volume = arguments.getString("volume");
String[] directories = null;
if (MediaProvider.INTERNAL_VOLUME.equals(volume)) {
// scan internal media storage
directories = new String[] {
Environment.getRootDirectory() + "/media",
Environment.getOemDirectory() + "/media",
Environment.getProductDirectory() + "/media",
};
}
// 這里的判斷要修改 start
/*else if (MediaProvider.EXTERNAL_VOLUME.equals(volume)) {*/
else {
// 這里的判斷要修改 end
// scan external storage volumes
if (getSystemService(UserManager.class).isDemoUser()) {
directories = ArrayUtils.appendElement(String.class,
mExternalStoragePaths,
Environment.getDataPreloadsMediaDirectory().getAbsolutePath());
} else {
directories = mExternalStoragePaths;
}
}
if (directories != null) {
if (false) Log.d(TAG, "start scanning volume " + volume + ": "
+ Arrays.toString(directories));
scan(directories, volume);
if (false) Log.d(TAG, "done scanning volume " + volume);
}
...
}
}
找到 volumeName 賦值的源頭在 MediaScannerReceiver 中族沃。
這里猜測频祝,存在一個優(yōu)化點(diǎn),掃描的路徑 directories脆淹,源碼中是全部的外部存儲路徑常空,為了節(jié)省性能,directories 可以根據(jù)外部設(shè)備的 StorageVolume 的 getPath() 方法盖溺,進(jìn)行單個路徑掃描漓糙。如果要處理的話,我們在這里需要 MediaScannerReceiver 將路徑傳遞過來咐柜。(還未驗證兼蜈,如果不行的話,就不要對 directories 進(jìn)行修改了)拙友。
三、MediaScannerReceiver
@Override
public void onReceive(Context context, Intent intent) {
final String action = intent.getAction();
final Uri uri = intent.getData();
if (Intent.ACTION_BOOT_COMPLETED.equals(action)) {
...
} else if (Intent.ACTION_LOCALE_CHANGED.equals(action)) {
...
} else {
if (uri.getScheme().equals("file")) {
...
if (Intent.ACTION_MEDIA_MOUNTED.equals(action)) {
// scan whenever any volume is mounted
scan(context, MediaProvider.EXTERNAL_VOLUME);
}
...
}
}
}
所以切入點(diǎn)就是歼郭,在接收 ACTION_MEDIA_MOUNTED 廣播的位置遗契,獲取到 Uri,傳入 scan() 方法病曾。
scan(context, uri.toString());
最后在回到 MediaScannerService 修改接收的參數(shù) "volume"牍蜂。
四、MediaScannerService
private final class ServiceHandler extends Handler {
@Override
public void handleMessage(Message msg) {
...
String volume = arguments.getString("volume");
String[] directories = null;
if (MediaProvider.INTERNAL_VOLUME.equals(volume)) {
// scan internal media storage
directories = new String[] {
Environment.getRootDirectory() + "/media",
Environment.getOemDirectory() + "/media",
Environment.getProductDirectory() + "/media",
};
}
// 這里需要修改 start
/*else if (MediaProvider.EXTERNAL_VOLUME.equals(volume)) {
// scan external storage volumes
if (getSystemService(UserManager.class).isDemoUser()) {
directories = ArrayUtils.appendElement(String.class,
mExternalStoragePaths,
Environment.getDataPreloadsMediaDirectory().getAbsolutePath());
} else {
directories = mExternalStoragePaths;
}
}*/
else {
boolean isSinglePath = false;// 標(biāo)記單個路徑掃描
String[] singleStoragePath = new String[1];
try {
// Uri 轉(zhuǎn)換為 File
File file = new File(new URI(volume));
// 根據(jù) File 獲取 StorageVolume
StorageVolume storageVolume = mStorageManager.getStorageVolume(file);
if (storageVolume != null) {
// 重新賦值 volume 為 volumeId
volume = String.valueOf(storageVolume.getFatVolumeId());
singleStoragePath[0] = storageVolume.getPath();
isSinglePath = true;
}
} catch (URISyntaxException e) {
e.printStackTrace();
}
if (getSystemService(UserManager.class).isDemoUser()) {
if (isSinglePath) {
directories = ArrayUtils.appendElement(String.class,
singleStoragePath,
Environment.getDataPreloadsMediaDirectory().getAbsolutePath());
} else {
directories = ArrayUtils.appendElement(String.class,
mExternalStoragePaths,
Environment.getDataPreloadsMediaDirectory().getAbsolutePath());
}
} else {
if (isSinglePath) {
directories = singleStoragePath;
} else {
directories = mExternalStoragePaths;
}
}
}
// 這里需要修改 end
if (directories != null) {
if (false) Log.d(TAG, "start scanning volume " + volume + ": "
+ Arrays.toString(directories));
scan(directories, volume);
if (false) Log.d(TAG, "done scanning volume " + volume);
}
...
}
}
五泰涂、ContentResolver
所有操作外部存儲的地方鲫竞,增刪改查使用的 URI 中的 path 都要修改為 volumeId,才能正常運(yùn)行逼蒙,而 MediaStore.Audio.Media.EXTERNAL_CONTENT_URI 就不能使用了从绘,看下 URI 的組成:
public static final Uri EXTERNAL_CONTENT_URI = getContentUri("external");
public static Uri getContentUri(String volumeName) {
return Uri.parse(CONTENT_AUTHORITY_SLASH + volumeName +
"/audio/media");
}
再看下 MediaProvider 中是根據(jù) URI 的 path 中 volumeName 去獲取 DatabaseHelper:
private DatabaseHelper getDatabaseForUri(Uri uri) {
synchronized (mDatabases) {
if (uri.getPathSegments().size() >= 1) {
return mDatabases.get(uri.getPathSegments().get(0));
}
}
return null;
}
所以開發(fā)者使用中,只要傳入 volumeId 即可完成閉環(huán)是牢。
MediaStore.Audio.Media.getContentUri(volumeId)
問題點(diǎn) :
1僵井、mVolumeId 需要處理嗎,添加多個到 MatrixCursor?
// Used temporarily (until we have unique media IDs) to get an identifier
// for the current sd card, so that the music app doesn't have to use the
// non-public getFatVolumeId method
if (table == FS_ID) {
MatrixCursor c = new MatrixCursor(new String[] {"fsid"});
c.addRow(new Integer[] {mVolumeId});
return c;
}
目前沒有處理驳棱,可以正常運(yùn)行批什,先不處理該邏輯。
參考資料:
android 添加或者取消對于某種媒體文件格式的支持
Android源碼個個擊破之-多媒體掃描
Android媒體掃描詳細(xì)解析之一
Android媒體掃描詳細(xì)解析之二