前言

上一篇《MediaProvider源码分析》分析到,正在对多媒体的扫描是在MediaScanner中,因此进入就进一步分析多媒体扫描逻辑。

这里是用了Android P源码分析,只能是个人流水账哈

涉及代码目录

  1. #java
  2. frameworks\base\media\java\android\media\MediaScanner.java
  3. #cpp
  4. frameworks\av\media\libmedia\MediaScanner.cpp
  5. frameworks\av\media\libstagefright\StagefrightMediaScanner.cpp
  6. #jni
  7. frameworks\base\media\jni\android_media_MediaScanner.cpp
复制

正文

接上文,扫描涉及到MediaScanner

  1. #1先初始化MediaScanner
  2. MediaScanner scanner = new MediaScanner(this, volumeName)
  3. #2 扫描目录或扫描指定文件
  4. scanDirectories(String[] directories)
  5. scanSingleFile(String path, String mimeType)
复制

因此,我们就针对上面其中一个进入跟踪,这里一scanDirectories()为例。

这里涉及JNI,这个推荐看完大致流程可以重新学习一下,会C和Java的调用很用帮助。

  1. 1. processDirectory(String path, MediaScannerClient client);//遍历文件
  2. 2. processFile(String path, String mimeType, MediaScannerClient client);//解析音视频文件
  3. 3. setLocale(String locale);//语言切换
  4. 4. extractAlbumArt(FileDescriptor fd);//专辑图
  5. 5. native_init();
  6. 6. native_setup();
  7. 7. native_finalize();
复制

MediaScanner()

先初始化MediaScanner,先看看做了哪些操作。

  1. //volumeName是MediaProvider.EXTERNAL_VOLUME或MediaProvider.INTERNAL_VOLUME
  2. public MediaScanner(Context c, String volumeName) {
  3. //native 初始化
  4.   native_setup();
  5. //获取mMediaProvider用于操作数据库
  6.   mMediaProvider = mContext.getContentResolver()
  7.           .acquireContentProviderClient(MediaStore.AUTHORITY);
  8. //mProcessPlaylists后续需要,从这里看就是external才需要处理播放列表
  9.   if (!volumeName.equals("internal")) {
  10.       mProcessPlaylists = true;
  11.       mProcessGenres = true;
  12.       mPlaylistsUri = Playlists.getContentUri(volumeName);
  13.   } else {
  14.       mProcessPlaylists = false;
  15.       mProcessGenres = false;
  16.       mPlaylistsUri = null;
  17.   }
  18. //检查是否有忘记关的资源
  19.   mCloseGuard.open("close");
  20. }
复制

在初始化MediaScanner时会提前加载media_jni库。

  1. static {
  2.   System.loadLibrary("media_jni");
  3.   native_init();
  4. }
复制

scanDirectories()

  1. public void scanDirectories(String[] directories) {
  2. //扫描前预准备[先判断数据库中文件是否存在,不存在就删除]
  3. prescan(null, true);
  4. if (ENABLE_BULK_INSERTS) {//true
  5. mMediaInserter = new MediaInserter(mMediaProvider, 500);
  6. }
  7. //directories可能[/storage/emulated/0, /storage/udisk0]
  8. for (int i = 0; i < directories.length; i++) {
  9. // native 函数,调用它来对目标文件夹进行扫描
  10. processDirectory(directories[i], mClient);
  11. }
  12. if (ENABLE_BULK_INSERTS) {
  13. mMediaInserter.flushAll();
  14. mMediaInserter = null;
  15. }
  16. // 扫描后处理 主要是processPlayLists处理
  17. postscan(directories);
  18. }
复制

这里才开始扫描磁盘中的文件。

prescan()
  1. private void prescan(String filePath, boolean prescanFiles) throws RemoteException {
  2. //这里传入的filePath=null,prescanFiles= true
  3. //查询数据库,判断数据是否存在,如果不存在就删除
  4.   try {
  5.       if (prescanFiles) {
  6.           Uri limitUri = mFilesUri.buildUpon().appendQueryParameter("limit", "1000").build();
  7.           while (true) {
  8.               selectionArgs[0] = "" + lastId;
  9.               if (c != null) {
  10.                   c.close();
  11.                   c = null;
  12.               }
  13.               c = mMediaProvider.query(limitUri, FILES_PRESCAN_PROJECTION,
  14.                       where, selectionArgs, MediaStore.Files.FileColumns._ID, null);
  15.               if (c == null) {
  16.                   break;
  17.               }
  18.               int num = c.getCount();
  19. //遍历数据,上面已经限制了1000条
  20.               while (c.moveToNext()) {
  21.                   //判断文件或文件夹是否符合条件,并判断是否存在,如果不存在就从数据库中删除。
  22.                   if (path != null && path.startsWith("/")) {
  23.                       boolean exists = false;
  24.                       try {
  25.                           exists = Os.access(path, android.system.OsConstants.F_OK);
  26.                       } catch (ErrnoException e1) {
  27.                       }
  28. //如果文件不存在,就删除
  29.                       if (!exists && !MtpConstants.isAbstractObject(format)) {
  30.                           MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path);
  31.                           int fileType = (mediaFileType == null ? 0 : mediaFileType.fileType);
  32.                           if (!MediaFile.isPlayListFileType(fileType)) {
  33.                               deleter.delete(rowId);
  34.                               if (path.toLowerCase(Locale.US).endsWith("/.nomedia")) {
  35.                                   deleter.flush();
  36.                                   String parent = new File(path).getParent();
  37. Log.d(TAG, "prescan parent: "+ parent );
  38.                                   mMediaProvider.call(MediaStore.UNHIDE_CALL, parent, null);
  39.                               }
  40.                           }
  41.                       }
  42.                   }
  43.               }
  44.           }
  45.       }
  46.   }
  47.   finally {
  48.       if (c != null) {
  49.           c.close();
  50.       }
  51.       deleter.flush();
  52.   }
  53. }
复制

这种提前扫描可以缩短扫描时间,因为重新解析ID3是比较耗时间的。

processDirectory()

调用了native层方法。传入的directories[i]

  1. [/storage/emulated/0, /storage/udisk0]
复制

并传入了mClient

  1. private final MyMediaScannerClient mClient = new MyMediaScannerClient();
复制

MyMediaScannerClient是MediaScanner中的内部类,native层扫描后会通过scanFile()回调上来处理。

scanFile()直接调用的是doScanFile()

  1. public Uri doScanFile(String path, String mimeType, long lastModified,
  2. long fileSize, boolean isDirectory, boolean scanAlways, boolean noMedia) {
  3. Uri result = null;
  4. try {
  5. //为了后续和MediaProvider打交道,准备一个FileEntry
  6. FileEntry entry = beginFile(path, mimeType, lastModified,
  7. fileSize, isDirectory, noMedia);
  8. //略
  9. if (entry != null && (entry.mLastModifiedChanged || scanAlways)) {
  10. //不是多媒体文件
  11. if (noMedia) {
  12. //endFile()用于插入数据和刷新数据
  13. result = endFile(entry, false, false, false, false, false);
  14. } else {
  15. //略
  16. //音频或视频
  17. if (isaudio || isvideo) {
  18. mScanSuccess = processFile(path, mimeType, this);
  19. }
  20. //图片
  21. if (isimage) {
  22. mScanSuccess = processImageFile(path);
  23. }
  24. //endFile()用于插入数据和刷新数据
  25. result = endFile(entry, ringtones, notifications, alarms, music, podcasts);
  26. }
  27. }
  28. } catch (RemoteException e) {
  29. Log.e(TAG, "RemoteException in MediaScanner.scanFile()", e);
  30. }
  31. return result;
  32. }
复制

这里做了三件事,封装FileEntry,处理音视频图片,插入和刷新数据库

beginFile()
  1. public FileEntry beginFile(String path, String mimeType, long lastModified,
  2. long fileSize, boolean isDirectory, boolean noMedia) {
  3. //略
  4. if (!isDirectory) {
  5. //是否多媒体文件?
  6. if (!noMedia && isNoMediaFile(path)) {
  7. noMedia = true;
  8. }
  9. mNoMedia = noMedia;
  10. // try mimeType first, if it is specified
  11. //mimeType是否指定,如果是就直接查询文件类型,一般为null
  12. if (mimeType != null) {
  13. mFileType = MediaFile.getFileTypeForMimeType(mimeType);
  14. }
  15. // 默认是0,当时如果上面制定了,就不等于0
  16. if (mFileType == 0) {
  17. //MediaFile中添加了音视频文件支持后缀。
  18. MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path);
  19. if (mediaFileType != null) {
  20. mFileType = mediaFileType.fileType;
  21. if (mMimeType == null) {
  22. mMimeType = mediaFileType.mimeType;
  23. }
  24. }
  25. }
  26. //是否启动了DRM数字版权管理
  27. if (isDrmEnabled() && MediaFile.isDrmFileType(mFileType)) {
  28. mFileType = getFileTypeFromDrm(path);
  29. }
  30. }
  31. //从MediaProvider中查出该文件或目录对应的FileEntry
  32. FileEntry entry = makeEntryFor(path);
  33. if (entry == null || wasModified) {
  34. //是否被更新过
  35. if (wasModified) {
  36. entry.mLastModified = lastModified;
  37. } else {
  38. //entry= null时会走这里,也就是没有查询到,new一个新的FileEntry
  39. entry = new FileEntry(0, path, lastModified,
  40. (isDirectory ? MtpConstants.FORMAT_ASSOCIATION : 0));
  41. }
  42. entry.mLastModifiedChanged = true;
  43. }
  44. //MediaScanner构造函数中时,当volumeName=external时mProcessPlaylists=true
  45. //如果是多媒体,且外置磁盘,都会走这里
  46. if (mProcessPlaylists && MediaFile.isPlayListFileType(mFileType)) {
  47. mPlayLists.add(entry);
  48. return null;
  49. }
  50. //略
  51. return entry;
  52. }
复制

对于音视频支持的文件后缀,可以看MediaFile.java

  1. frameworks\base\media\java\android\media\MediaFile.java
复制
endFile()

主要是插入和更新数据,需要注意的,这里会更新判断进行更新电话铃声,通知音和闹钟铃声。

  1. private Uri endFile(FileEntry entry, boolean ringtones, boolean notifications,
  2. boolean alarms, boolean music, boolean podcasts)
  3. throws RemoteException {
  4. //对一些null的数据进行一定赋值
  5. //比如没有获取到歌曲名,可以用文件名替代。
  6. //略
  7. long rowId = entry.mRowId;
  8. if (MediaFile.isAudioFileType(mFileType) && (rowId == 0 || mMtpObjectHandle != 0)) {
  9. //音频
  10. values.put(Audio.Media.IS_RINGTONE, ringtones);
  11. values.put(Audio.Media.IS_NOTIFICATION, notifications);
  12. values.put(Audio.Media.IS_ALARM, alarms);
  13. values.put(Audio.Media.IS_MUSIC, music);
  14. values.put(Audio.Media.IS_PODCAST, podcasts);
  15. } else if ((mFileType == MediaFile.FILE_TYPE_JPEG
  16. || mFileType == MediaFile.FILE_TYPE_HEIF
  17. || MediaFile.isRawImageFileType(mFileType)) && !mNoMedia) {
  18. //图片
  19. }
  20. //rowid == 0 表示数据库中不存在,就需要进行插入数据
  21. if (rowId == 0) {
  22. //略
  23. if (inserter == null || needToSetSettings) {
  24. if (inserter != null) {
  25. inserter.flushAll();
  26. }
  27. result = mMediaProvider.insert(tableUri, values);
  28. } else if (entry.mFormat == MtpConstants.FORMAT_ASSOCIATION) {
  29. inserter.insertwithPriority(tableUri, values);
  30. } else {
  31. inserter.insert(tableUri, values);
  32. }
  33. } else {
  34. //由于rowid不为0,也就是数据库中存在数据,因此进行update
  35. //略
  36. mMediaProvider.update(result, values, null, null);
  37. }
  38. //是否需要设置 通知一下,铃声 闹钟铃声等
  39. if(needToSetSettings) {
  40. if (notifications) {
  41. setRingtoneIfNotSet(Settings.System.NOTIFICATION_SOUND, tableUri, rowId);
  42. mDefaultNotificationSet = true;
  43. } else if (ringtones) {
  44. setRingtoneIfNotSet(Settings.System.RINGTONE, tableUri, rowId);
  45. mDefaultRingtoneSet = true;
  46. } else if (alarms) {
  47. setRingtoneIfNotSet(Settings.System.ALARM_ALERT, tableUri, rowId);
  48. mDefaultAlarmSet = true;
  49. }
  50. }
  51. return result;
  52. }
复制

setRingtoneIfNotSet()

  1. private void setRingtoneIfNotSet(String settingName, Uri uri, long rowId) {
  2. ContentResolver cr = mContext.getContentResolver();
  3. String existingSettingValue = Settings.System.getString(cr, settingName);
  4. if (TextUtils.isEmpty(existingSettingValue)) {
  5. final Uri settingUri = Settings.System.getUriFor(settingName);
  6. final Uri ringtoneUri = ContentUris.withAppendedId(uri, rowId);
  7. //设置铃声
  8. RingtoneManager.setActualDefaultRingtoneUri(mContext,
  9. RingtoneManager.getDefaultType(settingUri), ringtoneUri);
  10. }
  11. Settings.System.putInt(cr, settingSetIndicatorName(settingName), 1);
  12. }
复制
postscan()
  1. private void postscan(final String[] directories) throws RemoteException {
  2. //上面MediaScanner()中有当external时mProcessPlaylists = true
  3. if (mProcessPlaylists) {
  4. processPlayLists();
  5. }
  6. mPlayLists.clear();
  7. }
复制

除了mProcessPlaylists,还需要满足isPlayListFileType(),才会放入mPlayLists

  1. //主要针对如下几种格式
  2. public static final int FILE_TYPE_M3U = 41;
  3. public static final int FILE_TYPE_PLS = 42;
  4. public static final int FILE_TYPE_WPL = 43;
  5. public static final int FILE_TYPE_HTTPLIVE = 44;
  6.  
  7. private static final int FIRST_PLAYLIST_FILE_TYPE = FILE_TYPE_M3U;
  8. private static final int LAST_PLAYLIST_FILE_TYPE = FILE_TYPE_HTTPLIVE;
  9.  
  10.  
  11. public static boolean isPlayListFileType(int fileType) {
  12. return (fileType >= FIRST_PLAYLIST_FILE_TYPE &&
  13. fileType <= LAST_PLAYLIST_FILE_TYPE);
  14. }
复制

看看processPlayLists()

  1. private void processPlayLists() throws RemoteException {
  2. //mPlayLists在beginFile()中
  3. //当mProcessPlaylists && MediaFile.isPlayListFileType(mFileType)为true时添加
  4. Iterator<FileEntry> iterator = mPlayLists.iterator();
  5. Cursor fileList = null;
  6. try {
  7. fileList = mMediaProvider.query(mFilesUri, FILES_PRESCAN_PROJECTION,
  8. "media_type=2", null, null, null);
  9. while (iterator.hasNext()) {
  10. FileEntry entry = iterator.next();
  11. if (entry.mLastModifiedChanged) {
  12. processPlayList(entry, fileList);
  13. }
  14. }
  15. } catch (RemoteException e1) {
  16. } finally {
  17. if (fileList != null) {
  18. fileList.close();
  19. }
  20. }
  21. }
复制

真正干活的是processPlayList(FileEntry entry, Cursor fileList)

  1. private void processPlayList(FileEntry entry, Cursor fileList) throws RemoteException {
  2. long rowId = entry.mRowId;
  3. //略
  4. //rowid =0 表示数据库没有,insert;反之update
  5. if (rowId == 0) {
  6. values.put(MediaStore.Audio.Playlists.DATA, path);
  7. uri = mMediaProvider.insert(mPlaylistsUri, values);
  8. rowId = ContentUris.parseId(uri);
  9. membersUri = Uri.withAppendedPath(uri, Playlists.Members.CONTENT_DIRECTORY);
  10. } else {
  11. uri = ContentUris.withAppendedId(mPlaylistsUri, rowId);
  12. mMediaProvider.update(uri, values, null, null);
  13. // delete members of existing playlist
  14. membersUri = Uri.withAppendedPath(uri, Playlists.Members.CONTENT_DIRECTORY);
  15. mMediaProvider.delete(membersUri, null, null);
  16. }
  17. String playListDirectory = path.substring(0, lastSlash + 1);
  18. MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path);
  19. int fileType = (mediaFileType == null ? 0 : mediaFileType.fileType);
  20. //针对FILE_TYPE_M3U,FILE_TYPE_PLS和FILE_TYPE_WPL再处理。
  21. if (fileType == MediaFile.FILE_TYPE_M3U) {
  22. processM3uPlayList(path, playListDirectory, membersUri, values, fileList);
  23. } else if (fileType == MediaFile.FILE_TYPE_PLS) {
  24. processPlsPlayList(path, playListDirectory, membersUri, values, fileList);
  25. } else if (fileType == MediaFile.FILE_TYPE_WPL) {
  26. processWplPlayList(path, playListDirectory, membersUri, values, fileList);
  27. }
  28. }
复制

android_media_MediaScanner.cpp

上面介绍过MediaScanner初始化是就会加重JNI并初始化,同时部分方法还是native实现的。

MediaScanner.java中设计的native方法有如下几个:

  1. private native void processDirectory(String path, MediaScannerClient client);
  2. private native boolean processFile(String path, String mimeType, MediaScannerClient client);
  3. private native void setLocale(String locale);
  4.  
  5. public native byte[] extractAlbumArt(FileDescriptor fd);
  6.  
  7. private static native final void native_init();
  8. private native final void native_setup();
  9. private native final void native_finalize();
复制
native_init()

部分代码删除

  1. static void android_media_MediaScanner_native_init(JNIEnv *env){
  2. //获取上下文context
  3. fields.context = env->GetFieldID(clazz, "mNativeContext", "J");
  4. }
复制
native_setup()
  1. static void android_media_MediaScanner_native_setup(JNIEnv *env, jobject thiz){
  2. // mp 是StagefrightMediaScanner的对象,后面需要使用
  3. MediaScanner *mp = new StagefrightMediaScanner;
  4. env->SetLongField(thiz, fields.context, (jlong)mp);
  5. }
复制

等setLocale(),进行一定的初始化。

接上面我们Java层调用了

  1. for (int i = 0; i < directories.length; i++) {
  2. processDirectory(directories[i], mClient);
  3. }
复制

native层

  1. static void android_media_MediaScanner_processDirectory(
  2. JNIEnv *env, jobject thiz, jstring path, jobject client){
  3. //mp就是native_setup创建StagefrightMediaScanner的对象
  4. MediaScanner *mp = getNativeScanner_l(env, thiz);
  5. //创建native层的MyMediaScannerClient,后面需要回调数据给Java层
  6. MyMediaScannerClient myClient(env, client);
  7. //调用StagefrightMediaScanner.processDirectory()
  8. MediaScanResult result = mp->processDirectory(pathStr, myClient);
  9. }
复制

由于StagefrightMediaScanner类没有重写,但父类有实现,因此就跳到MediaScanner.cpp中。

MyMediaScannerClient

继承于MediaScannerClient

  1. class MediaScannerClient{
  2. public:
  3. MediaScannerClient();
  4. virtual ~MediaScannerClient();
  5. void setLocale(const char* locale);
  6. void beginFile();
  7. status_t addStringTag(const char* name, const char* value);
  8. void endFile();
  9.  
  10. virtual status_t scanFile(const char* path, long long lastModified,
  11. long long fileSize, bool isDirectory, bool noMedia) = 0;
  12. virtual status_t handleStringTag(const char* name, const char* value) = 0;
  13. virtual status_t setMimeType(const char* mimeType) = 0;
  14. };
复制

setLocale在MediaScannerClient.cpp有实现。至于其他的都是空的方法。

MyMediaScannerClient中实现的只有scanFile(),handleStringTag()和setMimeType(),这三个会回调到Java层的MyMediaScannerClient中。

  1. class MyMediaScannerClient : public MediaScannerClient
  2. {
  3. public:
  4. virtual status_t scanFile(const char* path, long long lastModified,
  5. long long fileSize, bool isDirectory, bool noMedia){
  6. //略
  7. //此处的mClient是java层的MyMediaScannerClient,调用的也是java层的scanFile方法
  8. mEnv->CallVoidMethod(mClient, mScanFileMethodID, pathStr, lastModified,
  9. fileSize, isDirectory, noMedia);
  10. mEnv->DeleteLocalRef(pathStr);
  11. return checkAndClearExceptionFromCallback(mEnv, "scanFile");
  12. }
  13. virtual status_t handleStringTag(const char* name, const char* value){
  14. //略
  15. return checkAndClearExceptionFromCallback(mEnv, "handleStringTag");
  16. }
  17. virtual status_t setMimeType(const char* mimeType){
  18. //略
  19. return checkAndClearExceptionFromCallback(mEnv, "setMimeType");
  20. }
  21. };
复制

StagefrightMediaScanner.cpp

StagefrightMediaScanner的父类是MediaScanner,可以看StagefrightMediaScanner.h中的定义。

主要实现了解析音视频文件processFile()方法和extractAlbumArt()音频专辑图,解析媒体相关的信息,并通过MediaScannerClient回到给MediaScanner.java。

  1. MediaScanResult StagefrightMediaScanner::processFile(
  2. const char *path, const char *mimeType,
  3. MediaScannerClient &client) {
  4. //调用native层的MyMediaScannerClient对象进行local信息,语言设置
  5. client.setLocale(locale());
  6. //空方法,没有作用,下面的endFile也是一样
  7. client.beginFile();
  8. //具体的方法是调用processFileInternal实现的
  9. MediaScanResult result = processFileInternal(path, mimeType, client);
  10. //失败时
  11. if (mimeType == NULL && result != MEDIA_SCAN_RESULT_OK) {
  12. client.setMimeType("application/octet-stream");
  13. }
  14. client.endFile();
  15. return result;
  16. }
  17.  
  18. MediaScanResult StagefrightMediaScanner::processFileInternal(
  19. const char *path, const char * /* mimeType */,
  20. MediaScannerClient &client) {
  21.  
  22. //获取文件的元数据【ID3相关信息】
  23. sp<MediaMetadataRetriever> mRetriever(new MediaMetadataRetriever);
  24.  
  25. const char *value;
  26. if ((value = mRetriever->extractMetadata(
  27. METADATA_KEY_MIMETYPE)) != NULL) {
  28. // 关键函数setMimeType
  29. //mimeType回调java中的MyMediaScannerClient
  30. status = client.setMimeType(value);
  31. }
  32.  
  33. for (size_t i = 0; i < kNumEntries; ++i) {
  34. const char *value;
  35. if ((value = mRetriever->extractMetadata(kKeyMap[i].key)) != NULL) {
  36. // 关键函数addStringTag。
  37. //真正调用handleStringTag(name, value)会回调java中的MyMediaScannerClient的handleStringTag()
  38. status = client.addStringTag(kKeyMap[i].tag, value);
  39. }
  40. }
  41.  
  42. return MEDIA_SCAN_RESULT_OK;
  43. }
复制

通过JNI,Java跟C通信,C也反馈信息给Java。具体看android_media_MediaScanner.cpp

PS: 好烦,JNI如何让C和Java相互调用又忘了,下次重新整理一下。

MediaScanner.cpp

MediaScanner是StagefrightMediaScanner父类,android_media_MediaScanner.cpp调初始化的是StagefrightMediaScanner,但由于StagefrightMediaScanner只重写部分父类方法,因此没有重写的调用的还是父类的方法。

主要设计下面两个方法。

  1. setLocale()

  2. processDirectory()

主要是第二个方法,最终也是通过

  1. status_t status = client.scanFile(path, statbuf.st_mtime, 0,true, childNoMedia);
复制

更上面一样,client也是MediaScannerClient对象,最后也就是返回到Java层的MyMediaScannerClient中的scanFile()方法。

PS:MediaScannerClient是MyMediaScannerClient父类。

上面的MediaScanner流程都大概走完了。对于JNI,准备重新学习一下并记录一下,都忘记了。

参考文章

  1. Android MediaScanner:(二)MediaScannerReceiver

  2. MediaScannerService 研究

  3. Media Data之多媒体扫描过程分析(二)

相关文章

暂无评论

none
暂无评论...