媒体扫描时序图
看了上面的时序图是否感觉比较绕,一会Java层,一会Native层。其实只要了解它们为什么这样做就比较好理解为什么这样做了。
第一次:扫描路径,查找媒体文件,找到媒体文件之后就告知上层
第二次:上层收到Native层已找到媒体文件后,经过初步处理,再调用Native层去解析媒体文件。解析完成后就把
解析数据上报给Java层,然后插入数据库。
媒体扫描之代码分析
MediaScannerReceiver.java
@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)) {
// Scan internal only.
scan(context, MediaProvider.INTERNAL_VOLUME);
} else {
//外部存储挂载后,开始扫描外部存储
if (Intent.ACTION_MEDIA_MOUNTED.equals(action)) {
// scan whenever any volume is mounted
scan(context, MediaProvider.EXTERNAL_VOLUME);
} else if (Intent.ACTION_MEDIA_SCANNER_SCAN_FILE.equals(action) &&
path != null && path.startsWith(externalStoragePath + "/")) {//接收到扫描文件的广播
scanFile(context, path);
}
}
}
}
该广播属于静态广播,当接收到启动完成和外部存储挂载成功后开始扫描文件。本次咱们直接分析外部存储挂载成功后的媒体扫描。
MediaScannerReceiver.java
private void scan(Context context, String volume) {
Bundle args = new Bundle();
args.putString("volume", volume);
//携带volume参数,就是用来标志内部和外部存储。然后启动MediaScannerService
context.startService(
new Intent(context, MediaScannerService.class).putExtras(args));
}
MediaScannerService创建时会执行onCreate、onStartCommand,等MediaScannerService创建完成后,再次调用startService时,不在执行onCreate,但是会执行onStartCommand.下面咱们看看这两个生命周期方法都做了些什么。
MediaScannerService.java
@Override
public void onCreate() {
PowerManager pm = (PowerManager)getSystemService(Context.POWER_SERVICE);
//获取电源锁,防止在扫描过程中休眠
mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
StorageManager storageManager = (StorageManager)getSystemService(Context.STORAGE_SERVICE);
mExternalStoragePaths = storageManager.getVolumePaths();
// Start up the thread running the service. Note that we create a
// separate thread because the service normally runs in the process's
// main thread, which we don't want to block.
Thread thr = new Thread(null, this, "MediaScannerService");
thr.start();
}
在onCreate做的事件有两件事:
1.获取电源锁,防止在媒体扫描的时候休眠
2.创建线程并开启
那么在线程中又做了什么事情呢?其实就使用子线程的Looper创建一个Handler,代码就不看,有兴趣的自行分析。
下面看看Handler做了些什么?
MediaScannerService.java
private final class ServiceHandler extends Handler {
@Override
public void handleMessage(Message msg) {
Bundle arguments = (Bundle) msg.obj;
if (arguments == null) {
Log.e(TAG, "null intent, b/20953950");
return;
}
String filePath = arguments.getString("filepath");
try {
//扫描文件时走这里
if (filePath != null) {
IBinder binder = arguments.getIBinder("listener");
IMediaScannerListener listener =
(binder == null ? null : IMediaScannerListener.Stub.asInterface(binder));
Uri uri = null;
try {
uri = scanFile(filePath, arguments.getString("mimetype"));
} catch (Exception e) {
Log.e(TAG, "Exception scanning file", e);
}
if (listener != null) {
listener.scanCompleted(filePath, uri);
}
} else {//扫描内部和外部存储走这里
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",
};
}
//扫描外部存储
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;
}
}
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);
}
}
} catch (Exception e) {
Log.e(TAG, "Exception in handleMessage", e);
}
stopSelf(msg.arg1);
}
};
那什么时候开始执行handler呢?刚才咱们说了MediaScannerService的生命周期,咱们仅看到了onCreate,现在看看onStartCommand做了什么事情。
MediaScannerService.java
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
//确保Handler不为null
while (mServiceHandler == null) {
synchronized (this) {
try {
wait(100);
} catch (InterruptedException e) {
}
}
}
if (intent == null) {
Log.e(TAG, "Intent is null in onStartCommand: ",
new NullPointerException());
return Service.START_NOT_STICKY;
}
//Handler就是在这里发送的Message
Message msg = mServiceHandler.obtainMessage();
msg.arg1 = startId;
msg.obj = intent.getExtras();
mServiceHandler.sendMessage(msg);
// Try again later if we are killed before we can finish scanning.
return Service.START_REDELIVER_INTENT;
}
下面再接着Handler处理看,Handler接收到外部存储的扫描消息后就看是执行scan了。
MediaScannerService.java
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);
//MSS通过insert这个特殊Uri让MediaProvider做一些准备工作
Uri scanUri = getContentResolver().insert(MediaStore.getMediaScannerUri(), values);
//向系统发送开始扫描的广播
sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_STARTED, uri));
try {
// 如果是外部存储的话,通过insert打开meidaProvider数据库
if (volumeName.equals(MediaProvider.EXTERNAL_VOLUME)) {
//此方法仅仅是把volumeName插入数据库
openDatabase(volumeName);
}
//创建MediaScanner对象,在创建的过程中会做一些初始化的操作,例如更新当前系统使用的语言等。
try (MediaScanner scanner = new MediaScanner(this, volumeName)) {
//扫描路径
scanner.scanDirectories(directories);
}
} catch (Exception e) {
Log.e(TAG, "exception in MediaScanner.scan()", e);
}
//通过特殊Uri做一些收尾清理工作
getContentResolver().delete(scanUri, null, null);
} finally {
//向系统发送扫描完成的广播
sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_FINISHED, uri));
mWakeLock.release();
}
}
本章的贴的代码里注释的都比较清楚,所以就不做过多的解释。
public void scanDirectories(String[] directories) {
try {
prescan(null, true); ------------1
if (ENABLE_BULK_INSERTS) {
// create MediaInserter for bulk inserts
mMediaInserter = new MediaInserter(mMediaProvider, 500);
}
for (int i = 0; i < directories.length; i++) {
/**
processDirectory是一个native方法,调用它来对目标文件进行扫描,其中
mClient为MyMediaScannerClient类型,它是从MediaScannerClient类派生的
**/
processDirectory(directories[i], mClient);---------------2
}
if (ENABLE_BULK_INSERTS) {
// flush remaining inserts
mMediaInserter.flushAll();
mMediaInserter = null;
}
//扫描后的处理
postscan(directories); ----------------3
下面显卡preSande 的
/**
上面 prescan 函数比较关键,首先让我们试想一个问题。
在媒体扫描过程中,有个令人头疼的问题,来举个例子:
假设某次扫描前 SD 卡中有 100 个媒体文件,数据库中
会有 100 条关于这些文件的记录。现删除其中的 50 个
文件,那么媒体数据库什么时候会被更新呢?
MediaScanner 考虑到了这一点,prescan 函数的主要作用
就是在扫描之前把上次扫描获取的数据库信息取出遍历并检
测是否丢失,如果丢失,则从数据库中删除
**/
private void prescan(String filePath, boolean prescanFiles) throws RemoteException {
Cursor c = null;
String where = null;
String[] selectionArgs = null;
//用于保存从数据库中获取的信息
mPlayLists.clear();
if (filePath != null) {
// query for only one file
where = MediaStore.Files.FileColumns._ID + ">?" +
" AND " + Files.FileColumns.DATA + "=?";
selectionArgs = new String[] { "", filePath };
} else {
where = MediaStore.Files.FileColumns._ID + ">?";
selectionArgs = new String[] { "" };
}
//获取数据库的值并赋值给对应的变量
mDefaultRingtoneSet = wasRingtoneAlreadySet(Settings.System.RINGTONE);
mDefaultNotificationSet = wasRingtoneAlreadySet(Settings.System.NOTIFICATION_SOUND);
mDefaultAlarmSet = wasRingtoneAlreadySet(Settings.System.ALARM_ALERT);
// Tell the provider to not delete the file.
// If the file is truly gone the delete is unnecessary, and we want to avoid
// accidentally deleting files that are really there (this may happen if the
// filesystem is mounted and unmounted while the scanner is running).
Uri.Builder builder = mFilesUri.buildUpon();
//通过分析此处设置为false是跳过删除
builder.appendQueryParameter(MediaStore.PARAM_DELETE_DATA, "false");
MediaBulkDeleter deleter = new MediaBulkDeleter(mMediaProvider, builder.build());
// Build the list of files from the content provider
try {
if (prescanFiles) {
// First read existing files from the files table.
// Because we'll be deleting entries for missing files as we go,
// we need to query the database in small batches, to avoid problems
// with CursorWindow positioning.
long lastId = Long.MIN_VALUE;
Uri limitUri = mFilesUri.buildUpon().appendQueryParameter("limit", "1000").build();
while (true) {
selectionArgs[0] = "" + lastId;
if (c != null) {
c.close();
c = null;
}
//查询数据库
c = mMediaProvider.query(limitUri, FILES_PRESCAN_PROJECTION,
where, selectionArgs, MediaStore.Files.FileColumns._ID, null);
if (c == null) {
break;
}
int num = c.getCount();
if (num == 0) {
break;
}
while (c.moveToNext()) {
long rowId = c.getLong(FILES_PRESCAN_ID_COLUMN_INDEX);
String path = c.getString(FILES_PRESCAN_PATH_COLUMN_INDEX);
int format = c.getInt(FILES_PRESCAN_FORMAT_COLUMN_INDEX);
long lastModified = c.getLong(FILES_PRESCAN_DATE_MODIFIED_COLUMN_INDEX);
lastId = rowId;
// Only consider entries with absolute path names.
// This allows storing URIs in the database without the
// media scanner removing them.
if (path != null && path.startsWith("/")) {
boolean exists = false;
try {
//检查文件是否存在
exists = Os.access(path, android.system.OsConstants.F_OK);
} catch (ErrnoException e1) {
}
if (!exists && !MtpConstants.isAbstractObject(format)) {
// do not delete missing playlists, since they may have been
// modified by the user.
// The user can delete them in the media player instead.
// instead, clear the path and lastModified fields in the row
//获取文件类型
MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path);
int fileType = (mediaFileType == null ? 0 : mediaFileType.fileType);
if (!MediaFile.isPlayListFileType(fileType)) {
//如果不是播放的文件类型,则删除它
deleter.delete(rowId);
//如果以"/.nomedia"结束,则表示已经无文件,则删除所有
if (path.toLowerCase(Locale.US).endsWith("/.nomedia")) {
deleter.flush();
String parent = new File(path).getParent();
//告诉MediaProvider重新扫描
mMediaProvider.call(MediaStore.UNHIDE_CALL, parent, null);
}
}
}
}
}
}
}
}
finally {
if (c != null) {
c.close();
}
deleter.flush();
}
// compute original size of images
mOriginalCount = 0;
c = mMediaProvider.query(mImagesUri, ID_PROJECTION, null, null, null, null);
if (c != null) {
mOriginalCount = c.getCount();
c.close();
}
}
下面开始处理路径信息了
static void
android_media_MediaScanner_processDirectory(
JNIEnv *env, jobject thiz, jstring path, jobject client)
{
ALOGV("processDirectory");
//获取在native_setup创建的MediaScanncer对象
MediaScanner *mp = getNativeScanner_l(env, thiz);
//通过Java层传递的MyMediaScannerClient构造一个native层MyMediaScannerClient
//其实主要目的还是通过它调用Java层的方法
MyMediaScannerClient myClient(env, client);
//扫描路径
MediaScanResult result = mp->processDirectory(pathStr, myClient);
}
MediaScanResult MediaScanner::processDirectory(
const char *path, MediaScannerClient &client) {
//获取路径长度,长度不能超过4096
int pathLength = strlen(path);
//设置本地语言,以便于在解析media信息时尽量转换成对应的语言信息
client.setLocale(locale());
MediaScanResult result = doProcessDirectory(pathBuffer, pathRemaining, client, false);
}
MediaScanResult MediaScanner::doProcessDirectory(
char *path, int pathRemaining, MediaScannerClient &client, bool noMedia) {
// place to copy file or directory name
char* fileSpot = path + strlen(path);
struct dirent* entry;
//如果扫描的路径在白名单列表中,则直接返回不做任何处理。
if (shouldSkipDirectory(path)) {
ALOGD("Skipping: %s", path);
return MEDIA_SCAN_RESULT_OK;
}
// Treat all files as non-media in directories that contain a ".nomedia" file
if (pathRemaining >= 8 /* strlen(".nomedia") */ ) {
strcpy(fileSpot, ".nomedia");
//如果存在“.nomedia”,则表示没有媒体文件
if (access(path, F_OK) == 0) {
ALOGV("found .nomedia, setting noMedia flag");
noMedia = true;
}
// restore path
fileSpot[0] = 0;
}
//打开目录
DIR* dir = opendir(path);
MediaScanResult result = MEDIA_SCAN_RESULT_OK;
while ((entry = readdir(dir))) {//遍历整个目录
//处理遍历的子文件或子文件夹
if (doProcessDirectoryEntry(path, pathRemaining, client, noMedia, entry, fileSpot)
== MEDIA_SCAN_RESULT_ERROR) {
result = MEDIA_SCAN_RESULT_ERROR;
break;
}
}
closedir(dir);
return result;
}
MediaScanResult MediaScanner::doProcessDirectoryEntry(
char *path, int pathRemaining, MediaScannerClient &client, bool noMedia,
struct dirent* entry, char* fileSpot) {
struct stat statbuf;
const char* name = entry->d_name;//获取子文件夹或子文件的名称
// ignore "." and ".."
if (name[0] == '.' && (name[1] == 0 || (name[1] == '.' && name[2] == 0))) {
return MEDIA_SCAN_RESULT_SKIPPED;
}
int nameLength = strlen(name);
if (nameLength + 1 > pathRemaining) {
// path too long!
return MEDIA_SCAN_RESULT_SKIPPED;
}
//之前fileSpot指向的时搜索路径的末尾,现在指向子文件夹或子文件的全路径
strcpy(fileSpot, name);
int type = entry->d_type;
if (type == DT_UNKNOWN) {
// If the type is unknown, stat() the file instead.
// This is sometimes necessary when accessing NFS mounted filesystems, but
// could be needed in other cases well.
if (stat(path, &statbuf) == 0) {
if (S_ISREG(statbuf.st_mode)) {
type = DT_REG;
} else if (S_ISDIR(statbuf.st_mode)) {
type = DT_DIR;
}
} else {
ALOGD("stat() failed for %s: %s", path, strerror(errno) );
}
}
//文件夹
if (type == DT_DIR) {
bool childNoMedia = noMedia;
// set noMedia flag on directories with a name that starts with '.'
// for example, the Mac ".Trashes" directory
if (name[0] == '.')
childNoMedia = true;
// report the directory to the client
if (stat(path, &statbuf) == 0) {//此path是一个文件,开始扫描文件
status_t status = client.scanFile(path, statbuf.st_mtime, 0,
true /*isDirectory*/, childNoMedia);
if (status) {
return MEDIA_SCAN_RESULT_ERROR;
}
}
//执行到这表示path依然是一个路径,所以继续递归doProcessDirectory
// and now process its contents
strcat(fileSpot, "/");
MediaScanResult result = doProcessDirectory(path, pathRemaining - nameLength - 1,
client, childNoMedia);
if (result == MEDIA_SCAN_RESULT_ERROR) {
return MEDIA_SCAN_RESULT_ERROR;
}
} else if (type == DT_REG) {
stat(path, &statbuf);
status_t status = client.scanFile(path, statbuf.st_mtime, statbuf.st_size,
false /*isDirectory*/, noMedia);
if (status) {
return MEDIA_SCAN_RESULT_ERROR;
}
}
return MEDIA_SCAN_RESULT_OK;
}
由于client.scanFile就是调用MediaScanner.java的scanFile,且scanFile又直接调用doScanFile,所以下面就直接看MediaScanner.java的doScanFile代码
//scanAlways用于控制是否强制扫描。有时候一些文件在前后两次扫描过程中没有发生变化,这时候
//MediaScanner可以不处理这些文件。如果为true,则即使没有发生变化也要扫描。
public Uri doScanFile(String path, String mimeType, long lastModified,
long fileSize, boolean isDirectory, boolean scanAlways, boolean noMedia) {
//做一些初始化工作。查询数据库,判断上传的路径信息并构建FileEntry
FileEntry entry = beginFile(path, mimeType, lastModified,
fileSize, isDirectory, noMedia);
// rescan for metadata if file was modified since last scan
if (entry != null && (entry.mLastModifiedChanged || scanAlways)) {
if (noMedia) {
result = endFile(entry, false, false, false, false, false);
} else {//存在有效的媒体文件
String lowpath = path.toLowerCase(Locale.ROOT);
//看看是哪种目录类型
boolean ringtones = (lowpath.indexOf(RINGTONES_DIR) > 0);
boolean notifications = (lowpath.indexOf(NOTIFICATIONS_DIR) > 0);
boolean alarms = (lowpath.indexOf(ALARMS_DIR) > 0);
boolean podcasts = (lowpath.indexOf(PODCAST_DIR) > 0);
boolean music = (lowpath.indexOf(MUSIC_DIR) > 0) ||
(!ringtones && !notifications && !alarms && !podcasts);
//判断是哪种文件类型
boolean isaudio = MediaFile.isAudioFileType(mFileType);
boolean isvideo = MediaFile.isVideoFileType(mFileType);
boolean isimage = MediaFile.isImageFileType(mFileType);
if (isaudio || isvideo || isimage) {
path = Environment.maybeTranslateEmulatedPathToInternal(new File(path))
.getAbsolutePath();
}
// we only extract metadata for audio and video files
if (isaudio || isvideo) {
processFile(path, mimeType, this);//处理媒体文件
}
if (isimage) {//处理图片的,比较简单,就不做分析了
processImageFile(path);
}
result = endFile(entry, ringtones, notifications, alarms, music, podcasts);
}
下面直接看processFile,中间的一些简单步骤省略了,如果不知道请查看时序图。
MediaScanResult StagefrightMediaScanner::processFile(
const char *path, const char *mimeType,
MediaScannerClient &client) {
ALOGV("processFile '%s'.", path);
//设置本地语言
client.setLocale(locale());
//空实现
client.beginFile();
//真正处理文件的是在这个地方
MediaScanResult result = processFileInternal(path, mimeType, client);
client.endFile();
return result;
}
MediaScanResult StagefrightMediaScanner::processFileInternal(
const char *path, const char * /* mimeType */,
MediaScannerClient &client) {
//查找path路径中最后一个“.”
const char *extension = strrchr(path, '.');
sp<MediaMetadataRetriever> mRetriever(new MediaMetadataRetriever);
int fd = open(path, O_RDONLY | O_LARGEFILE);
for (size_t i = 0; i < kNumEntries; ++i) {//循环解析各个属性
const char *value;
if ((value = mRetriever->extractMetadata(kKeyMap[i].key)) != NULL) {
//通过调用JNI的client对象将解析到的属性传回Java层,虽然此时调用的
//是addStringTag,但是只是一个中转,其实它调用了handleStringTag
status = client.addStringTag(kKeyMap[i].tag, value);
}
直接看MediaScanner.java的handleStringTag,一目了然,就不做过多解释。
public void handleStringTag(String name, String value) {
if (name.equalsIgnoreCase("title") || name.startsWith("title;")) {
// Don't trim() here, to preserve the special \001 character
// used to force sorting. The media provider will trim() before
// inserting the title in to the database.
mTitle = value;
} else if (name.equalsIgnoreCase("artist") || name.startsWith("artist;")) {
mArtist = value.trim();
} else if (name.equalsIgnoreCase("albumartist") || name.startsWith("albumartist;")
|| name.equalsIgnoreCase("band") || name.startsWith("band;")) {
mAlbumArtist = value.trim();
} else if (name.equalsIgnoreCase("album") || name.startsWith("album;")) {
mAlbum = value.trim();
} else if (name.equalsIgnoreCase("composer") || name.startsWith("composer;")) {
mComposer = value.trim();
} else if (mProcessGenres &&
(name.equalsIgnoreCase("genre") || name.startsWith("genre;"))) {
mGenre = getGenreName(value);
} else if (name.equalsIgnoreCase("year") || name.startsWith("year;")) {
mYear = parseSubstring(value, 0, 0);
} else if (name.equalsIgnoreCase("tracknumber") || name.startsWith("tracknumber;")) {
// track number might be of the form "2/12"
// we just read the number before the slash
int num = parseSubstring(value, 0, 0);
mTrack = (mTrack / 1000) * 1000 + num;
} else if (name.equalsIgnoreCase("discnumber") ||
name.equals("set") || name.startsWith("set;")) {
// set number might be of the form "1/3"
// we just read the number before the slash
int num = parseSubstring(value, 0, 0);
mTrack = (num * 1000) + (mTrack % 1000);
} else if (name.equalsIgnoreCase("duration")) {
mDuration = parseSubstring(value, 0, 0);
} else if (name.equalsIgnoreCase("writer") || name.startsWith("writer;")) {
mWriter = value.trim();
} else if (name.equalsIgnoreCase("compilation")) {
mCompilation = parseSubstring(value, 0, 0);
} else if (name.equalsIgnoreCase("isdrm")) {
mIsDrm = (parseSubstring(value, 0, 0) == 1);
} else if (name.equalsIgnoreCase("date")) {
mDate = parseDate(value);
} else if (name.equalsIgnoreCase("width")) {
mWidth = parseSubstring(value, 0, 0);
} else if (name.equalsIgnoreCase("height")) {
mHeight = parseSubstring(value, 0, 0);
} else {
//Log.v(TAG, "unknown tag: " + name + " (" + mProcessGenres + ")");
}
}
媒体信息都解析出来了,下面就该保存数据了。插入数据库的处理就在endFile里,这里不分析,有兴趣的自行分析。好了,本章就讲到这里。有什么问题请多多指点。