MediaScanner源码分析

Android  源码分析  2023年6月30日 am8:08发布1年前 (2023)更新 91es.com站长
70 0 0

前言

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

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

涉及代码目录

#java
frameworks\base\media\java\android\media\MediaScanner.java
#cpp
frameworks\av\media\libmedia\MediaScanner.cpp
frameworks\av\media\libstagefright\StagefrightMediaScanner.cpp
#jni
frameworks\base\media\jni\android_media_MediaScanner.cpp

正文

接上文,扫描涉及到MediaScanner

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

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

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

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

MediaScanner()

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

//volumeName是MediaProvider.EXTERNAL_VOLUME或MediaProvider.INTERNAL_VOLUME
public MediaScanner(Context c, String volumeName) {
    //native 初始化
    native_setup();
    //获取mMediaProvider用于操作数据库
    mMediaProvider = mContext.getContentResolver()
            .acquireContentProviderClient(MediaStore.AUTHORITY);
    //mProcessPlaylists后续需要,从这里看就是external才需要处理播放列表
    if (!volumeName.equals("internal")) {
        mProcessPlaylists = true;
        mProcessGenres = true;
        mPlaylistsUri = Playlists.getContentUri(volumeName);
    } else {
        mProcessPlaylists = false;
        mProcessGenres = false;
        mPlaylistsUri = null;
    }
    //检查是否有忘记关的资源
    mCloseGuard.open("close");
}

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

static {
    System.loadLibrary("media_jni");
    native_init();
}

scanDirectories()

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

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

prescan()
private void prescan(String filePath, boolean prescanFiles) throws RemoteException {
    //这里传入的filePath=null,prescanFiles= true
    //查询数据库,判断数据是否存在,如果不存在就删除
    try {
        if (prescanFiles) {
            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();
                //遍历数据,上面已经限制了1000条
                while (c.moveToNext()) {
                    //判断文件或文件夹是否符合条件,并判断是否存在,如果不存在就从数据库中删除。
                    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)) {
                            MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path);
                            int fileType = (mediaFileType == null ? 0 : mediaFileType.fileType);
                            if (!MediaFile.isPlayListFileType(fileType)) {
                                deleter.delete(rowId);
                                if (path.toLowerCase(Locale.US).endsWith("/.nomedia")) {
                                    deleter.flush();
                                    String parent = new File(path).getParent();
                                    Log.d(TAG, "prescan parent: "+ parent );
                                    mMediaProvider.call(MediaStore.UNHIDE_CALL, parent, null);
                                }
                            }
                        }
                    }
                }
            }
        }
    }
    finally {
        if (c != null) {
            c.close();
        }
        deleter.flush();
    }
}

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

processDirectory()

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

[/storage/emulated/0, /storage/udisk0]

并传入了mClient

private final MyMediaScannerClient mClient = new MyMediaScannerClient();

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

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

public Uri doScanFile(String path, String mimeType, long lastModified,
        long fileSize, boolean isDirectory, boolean scanAlways, boolean noMedia) {
    Uri result = null;
    try {
	//为了后续和MediaProvider打交道,准备一个FileEntry
        FileEntry entry = beginFile(path, mimeType, lastModified,
                fileSize, isDirectory, noMedia);
	//略
        if (entry != null && (entry.mLastModifiedChanged || scanAlways)) {
	    //不是多媒体文件
            if (noMedia) {
		//endFile()用于插入数据和刷新数据
                result = endFile(entry, false, false, false, false, false);
            } else {
                //略
		//音频或视频
                if (isaudio || isvideo) {
                    mScanSuccess = processFile(path, mimeType, this);
                }
		//图片
                if (isimage) {
                    mScanSuccess = processImageFile(path);
                }
		//endFile()用于插入数据和刷新数据
                result = endFile(entry, ringtones, notifications, alarms, music, podcasts);
            }
        }
    } catch (RemoteException e) {
        Log.e(TAG, "RemoteException in MediaScanner.scanFile()", e);
    }
    return result;
}

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

beginFile()
public FileEntry beginFile(String path, String mimeType, long lastModified,
    long fileSize, boolean isDirectory, boolean noMedia) {
	//略
    if (!isDirectory) {
	//是否多媒体文件?
        if (!noMedia && isNoMediaFile(path)) {
            noMedia = true;
        }
        mNoMedia = noMedia;
        // try mimeType first, if it is specified
	//mimeType是否指定,如果是就直接查询文件类型,一般为null
        if (mimeType != null) {
            mFileType = MediaFile.getFileTypeForMimeType(mimeType);
        }
        // 默认是0,当时如果上面制定了,就不等于0
        if (mFileType == 0) {
        	//MediaFile中添加了音视频文件支持后缀。
            MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path);
            if (mediaFileType != null) {
                mFileType = mediaFileType.fileType;
                if (mMimeType == null) {
                    mMimeType = mediaFileType.mimeType;
                }
            }
        }
	//是否启动了DRM数字版权管理
        if (isDrmEnabled() && MediaFile.isDrmFileType(mFileType)) {
            mFileType = getFileTypeFromDrm(path);
        }
    }
    //从MediaProvider中查出该文件或目录对应的FileEntry
    FileEntry entry = makeEntryFor(path);
    if (entry == null || wasModified) {
	//是否被更新过
        if (wasModified) {
            entry.mLastModified = lastModified;
        } else {
        //entry= null时会走这里,也就是没有查询到,new一个新的FileEntry
            entry = new FileEntry(0, path, lastModified,
                    (isDirectory ? MtpConstants.FORMAT_ASSOCIATION : 0));
        }
        entry.mLastModifiedChanged = true;
    }
    //MediaScanner构造函数中时,当volumeName=external时mProcessPlaylists=true
    //如果是多媒体,且外置磁盘,都会走这里
    if (mProcessPlaylists && MediaFile.isPlayListFileType(mFileType)) {
        mPlayLists.add(entry);
        return null;
    }
	//略
    return entry;
}

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

frameworks\base\media\java\android\media\MediaFile.java
endFile()

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

private Uri endFile(FileEntry entry, boolean ringtones, boolean notifications,
        boolean alarms, boolean music, boolean podcasts)
        throws RemoteException {
	//对一些null的数据进行一定赋值
	//比如没有获取到歌曲名,可以用文件名替代。
	//略
    long rowId = entry.mRowId;
    if (MediaFile.isAudioFileType(mFileType) && (rowId == 0 || mMtpObjectHandle != 0)) {
		//音频
        values.put(Audio.Media.IS_RINGTONE, ringtones);
        values.put(Audio.Media.IS_NOTIFICATION, notifications);
        values.put(Audio.Media.IS_ALARM, alarms);
        values.put(Audio.Media.IS_MUSIC, music);
        values.put(Audio.Media.IS_PODCAST, podcasts);
    } else if ((mFileType == MediaFile.FILE_TYPE_JPEG
            || mFileType == MediaFile.FILE_TYPE_HEIF
            || MediaFile.isRawImageFileType(mFileType)) && !mNoMedia) {
		//图片
    }
	//rowid == 0 表示数据库中不存在,就需要进行插入数据
    if (rowId == 0) {
		//略
        if (inserter == null || needToSetSettings) {
            if (inserter != null) {
                inserter.flushAll();
            }
            result = mMediaProvider.insert(tableUri, values);
        } else if (entry.mFormat == MtpConstants.FORMAT_ASSOCIATION) {
            inserter.insertwithPriority(tableUri, values);
        } else {
            inserter.insert(tableUri, values);
        }
    } else {
        //由于rowid不为0,也就是数据库中存在数据,因此进行update
		//略
        mMediaProvider.update(result, values, null, null);
    }
	//是否需要设置 通知一下,铃声 闹钟铃声等
    if(needToSetSettings) {
        if (notifications) {
            setRingtoneIfNotSet(Settings.System.NOTIFICATION_SOUND, tableUri, rowId);
            mDefaultNotificationSet = true;
        } else if (ringtones) {
            setRingtoneIfNotSet(Settings.System.RINGTONE, tableUri, rowId);
            mDefaultRingtoneSet = true;
        } else if (alarms) {
            setRingtoneIfNotSet(Settings.System.ALARM_ALERT, tableUri, rowId);
            mDefaultAlarmSet = true;
        }
    }
    return result;
}

setRingtoneIfNotSet()

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

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

//主要针对如下几种格式
public static final int FILE_TYPE_M3U      = 41;
public static final int FILE_TYPE_PLS      = 42;
public static final int FILE_TYPE_WPL      = 43;
public static final int FILE_TYPE_HTTPLIVE = 44;

private static final int FIRST_PLAYLIST_FILE_TYPE = FILE_TYPE_M3U;
private static final int LAST_PLAYLIST_FILE_TYPE = FILE_TYPE_HTTPLIVE;


public static boolean isPlayListFileType(int fileType) {
    return (fileType >= FIRST_PLAYLIST_FILE_TYPE &&
            fileType <= LAST_PLAYLIST_FILE_TYPE);
}

看看processPlayLists()

private void processPlayLists() throws RemoteException {
	//mPlayLists在beginFile()中
	//当mProcessPlaylists && MediaFile.isPlayListFileType(mFileType)为true时添加
    Iterator<FileEntry> iterator = mPlayLists.iterator();
    Cursor fileList = null;
    try {
        fileList = mMediaProvider.query(mFilesUri, FILES_PRESCAN_PROJECTION,
                "media_type=2", null, null, null);
        while (iterator.hasNext()) {
            FileEntry entry = iterator.next();
            if (entry.mLastModifiedChanged) {
                processPlayList(entry, fileList);
            }
        }
    } catch (RemoteException e1) {
    } finally {
        if (fileList != null) {
            fileList.close();
        }
    }
}

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

private void processPlayList(FileEntry entry, Cursor fileList) throws RemoteException {
	long rowId = entry.mRowId;
	//略
	//rowid =0 表示数据库没有,insert;反之update
    if (rowId == 0) {
        values.put(MediaStore.Audio.Playlists.DATA, path);
        uri = mMediaProvider.insert(mPlaylistsUri, values);
        rowId = ContentUris.parseId(uri);
        membersUri = Uri.withAppendedPath(uri, Playlists.Members.CONTENT_DIRECTORY);
    } else {
        uri = ContentUris.withAppendedId(mPlaylistsUri, rowId);
        mMediaProvider.update(uri, values, null, null);
        // delete members of existing playlist
        membersUri = Uri.withAppendedPath(uri, Playlists.Members.CONTENT_DIRECTORY);
        mMediaProvider.delete(membersUri, null, null);
    }
    String playListDirectory = path.substring(0, lastSlash + 1);
    MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path);
    int fileType = (mediaFileType == null ? 0 : mediaFileType.fileType);
	//针对FILE_TYPE_M3U,FILE_TYPE_PLS和FILE_TYPE_WPL再处理。
    if (fileType == MediaFile.FILE_TYPE_M3U) {
        processM3uPlayList(path, playListDirectory, membersUri, values, fileList);
    } else if (fileType == MediaFile.FILE_TYPE_PLS) {
        processPlsPlayList(path, playListDirectory, membersUri, values, fileList);
    } else if (fileType == MediaFile.FILE_TYPE_WPL) {
        processWplPlayList(path, playListDirectory, membersUri, values, fileList);
    }
}

android_media_MediaScanner.cpp

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

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

private native void processDirectory(String path, MediaScannerClient client);
private native boolean processFile(String path, String mimeType, MediaScannerClient client);
private native void setLocale(String locale);

public native byte[] extractAlbumArt(FileDescriptor fd);

private static native final void native_init();
private native final void native_setup();
private native final void native_finalize();
native_init()

部分代码删除

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

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

接上面我们Java层调用了

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

native层

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

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

MyMediaScannerClient

继承于MediaScannerClient

class MediaScannerClient{
public:
    MediaScannerClient();
    virtual ~MediaScannerClient();
    void setLocale(const char* locale);
    void beginFile();
    status_t addStringTag(const char* name, const char* value);
    void endFile();

    virtual status_t scanFile(const char* path, long long lastModified,
            long long fileSize, bool isDirectory, bool noMedia) = 0;
    virtual status_t handleStringTag(const char* name, const char* value) = 0;
    virtual status_t setMimeType(const char* mimeType) = 0;
};

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

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

class MyMediaScannerClient : public MediaScannerClient
{
public:
    virtual status_t scanFile(const char* path, long long lastModified,
            long long fileSize, bool isDirectory, bool noMedia){
	//略
	//此处的mClient是java层的MyMediaScannerClient,调用的也是java层的scanFile方法
        mEnv->CallVoidMethod(mClient, mScanFileMethodID, pathStr, lastModified,
                fileSize, isDirectory, noMedia);
        mEnv->DeleteLocalRef(pathStr);
        return checkAndClearExceptionFromCallback(mEnv, "scanFile");
    }
    virtual status_t handleStringTag(const char* name, const char* value){
	//略
        return checkAndClearExceptionFromCallback(mEnv, "handleStringTag");
    }
    virtual status_t setMimeType(const char* mimeType){
	//略
        return checkAndClearExceptionFromCallback(mEnv, "setMimeType");
    }
};

StagefrightMediaScanner.cpp

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

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

MediaScanResult StagefrightMediaScanner::processFile(
        const char *path, const char *mimeType,
        MediaScannerClient &client) {
    //调用native层的MyMediaScannerClient对象进行local信息,语言设置
    client.setLocale(locale());
   //空方法,没有作用,下面的endFile也是一样
    client.beginFile();
   //具体的方法是调用processFileInternal实现的
    MediaScanResult result = processFileInternal(path, mimeType, client);
   //失败时
    if (mimeType == NULL && result != MEDIA_SCAN_RESULT_OK) {
        client.setMimeType("application/octet-stream");
    }
    client.endFile();
    return result;
}

MediaScanResult StagefrightMediaScanner::processFileInternal(
        const char *path, const char * /* mimeType */,
        MediaScannerClient &client) {

    //获取文件的元数据【ID3相关信息】
    sp<MediaMetadataRetriever> mRetriever(new MediaMetadataRetriever);

    const char *value;
    if ((value = mRetriever->extractMetadata(
                    METADATA_KEY_MIMETYPE)) != NULL) {
        // 关键函数setMimeType
	//mimeType回调java中的MyMediaScannerClient
        status = client.setMimeType(value);
    }

    for (size_t i = 0; i < kNumEntries; ++i) {
        const char *value;
        if ((value = mRetriever->extractMetadata(kKeyMap[i].key)) != NULL) {
	// 关键函数addStringTag。
	//真正调用handleStringTag(name, value)会回调java中的MyMediaScannerClient的handleStringTag()
            status = client.addStringTag(kKeyMap[i].tag, value);
        }
    }

    return MEDIA_SCAN_RESULT_OK;
}

通过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()

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

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之多媒体扫描过程分析(二)

 历史上的今天

  1. 2024: 余秋雨:什么是文化?(0条评论)
  2. 2021: [ijkplayer专题] Ubuntu 18.3 编译ijkplayer-android(0条评论)
  3. 2021: 林徽因:一片阳光(0条评论)
  4. 2020: Android Studio重构清除无引用资源(0条评论)
  5. 2019: 再见,老何(0条评论)
版权声明 1、 本站名称: 91易搜
2、 本站网址: 91es.com3xcn.com
3、 本站内容: 部分来源于网络,仅供学习和参考,若侵权请留言
3、 本站申明: 个人流水账日记,内容并不保证有效

暂无评论

暂无评论...

随机推荐

许立志:梦想

夜,好像深了他用脚试了试这深,没膝而过而睡眠却极浅极浅 他,一个远道而来的异乡人在六月的光阴里流浪或者漂泊 风吹,吹落他几根未白的白发那些夕阳沉睡的傍晚他背着满满的乡愁徘徊于生活的十字路口这疼痛,重于故乡连绵万里的青山弓着腰,他遍地寻找 妈妈说的梦...

Android获取使用MediaBrowserService的app

前言简单记录一下获取Android中使用MediaBrowserService的的音乐信息,然后进行绑定,这样就可以控制相关应用,比如上下曲,播放暂停等。正文这里只是简单记录,部分代码片段。PackageManager packageManager = getPackageManager(...

XXXX: unexpected operator

最近在用.sh脚本时,明明可以运行的语句,但报错了比如“[: -ne: unexpected operator”因此查询到《解决Linux下编译.sh文件报错 “[: XXXX: unexpected operator”》这篇文章,觉得不错,摘抄于此。使用粗体字语句就可以搞定。3q然后我就表...

JNI之函数的参数介绍

前言之前学过JNI的动态注册和静态注册,到目前为止,简单的可以依葫芦画瓢了,但对于细节却还有很多的不知道。因此后面慢慢记录一下。方便自己查阅和学习。正文如果不知道静态注册和动态注册的使用,请看《JNI静态注册》和《JNI动态注册》,这次只关注详细JNI中方法的前两个参数的解释。在之前JNI...

龙应台:明白

十岁的时候,我们的妈妈五十岁。我们是怎么谈她们的?我和家萱在一个浴足馆按摩,并排懒坐,有一句每一句地闲聊。一面落地大窗,外面看不进来,我们却可以把过路的人看个清楚。这是上海,这是衡山路。每一个亚洲城市都曾经有过这么一条路——餐厅特别时髦,酒吧特别昂贵,时装店冷气极强,灯光特别亮,墙上的海报一定有...

init的启动

前言init的启动之前也跟过,目前用的是Android P,可能跟之前的存在一定的差异。因此重新记录一下,方便自己查阅。这里只是走走流程,大部分内容来之《Android P (9.0) 之Init进程源码分析》正文涉及文件,没出现顺序。\system\core\init\init.c...