4.0 媒体扫描流程

  • A+
所属分类:安卓 音视频

媒体扫描流程

android.providers.media 进程 其实是一个app,其实这个app 主要由三部分组成

  • - MediaProvider
  • - MediaScannerReceiver
  • - MediaScannerService

MediaScannerReceiver

我们接下来看下 MediaScannerReceiver 的流程

[-->MediaScannerReceiver.java]
public class MediaScannerReceiver extends BroadcastReceiver {
    private final static String TAG = "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)) {
            scan(context, MediaProvider.INTERNAL_VOLUME);
            scan(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));
    }    
}

 

如上,MediaScannerReceiver 接收 ACTION_BOOT_COMPLETED 、ACTION_MEDIA_MOUNTED 和ACTION_MEDIA_SCANNER_SCAN_FILE广播。

进行处理,我们取 ACTION_BOOT_COMPLETED广播进行分析。开机广播中有对内置的扫描和外置的扫描,我们取个复杂的.

[-->MediaScannerReceiver.java]
scan(context, MediaProvider.EXTERNAL_VOLUME);

 

对EXTERNAL_VOLUME 进行分析。

而scan 的函数里面 将参数 传递给MediaScannerService 就结束了,接下来分析MediaScannerService。

MediaScannerService

MediaScannerService 的启动
[-->MediaScannerReceiver.java]
public class MediaScannerService extends Service implements Runnable {
    @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();
    }
}

 

启动的时候,带一个锁,后期开始扫描的时候会使用这个锁保持屏幕是亮的。

StorageManager 拿所有的挂载路径。

[-->MediaScannerReceiver.java]
@Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        Message msg = mServiceHandler.obtainMessage();
        msg.arg1 = startId;
        msg.obj = intent.getExtras();
        mServiceHandler.sendMessage(msg);
        return Service.START_REDELIVER_INTENT;
    }

 

onStartCommand 中直接将刚才MediaScannerReceiver 传过来的参数给了mServiceHandler。

看下这个mServiceHandler;

[-->MediaScannerReceiver.java]
private final class ServiceHandler extends Handler {
        @Override
        public void handleMessage(Message msg) {
            Bundle arguments = (Bundle) msg.obj;
...
            String filePath = arguments.getString("filepath");
...
                if (filePath != null) {
       ...
                    try {
                        uri = scanFile(filePath, arguments.getString("mimetype"));
                    } catch (Exception e) {
                        Log.e(TAG, "Exception scanning file", e);
                    }
          ...
                } else {
                    String volume = arguments.getString("volume");
...
                    if (MediaProvider.INTERNAL_VOLUME.equals(volume)) {
...
                    } else if (MediaProvider.EXTERNAL_VOLUME.equals(volume)) {
...
                        directories = mExternalStoragePaths;
                    }
                    if (directories != null) 
                        scan(directories, volume);
        }
    };

 

在这个handleMessage 中接收到消息 判断filePath参数有没有,我们传过来的是没有的,String volume = arguments.getString("volume"); 中volume 是有的,是扩展的sdcard。

最后选取了扩展的sdcad路径,调用scan函数进行扫描。

[-->MediaScannerReceiver.java]
private void scan(String[] directories, String volumeName) {
        mWakeLock.acquire();
        try {
...
            sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_STARTED, uri));
...
                try (MediaScanner scanner = new MediaScanner(this, volumeName)) {
                    scanner.scanDirectories(directories);
                }
...
        } finally {
            sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_FINISHED, uri));
            mWakeLock.release();
        }
    }

 

开始的时候申请锁,然后发送开始扫描的广播 ,然后创建扫描器MediaScanner,然后扫描,然后发送扫描结束的广播,最后释放锁。

到这里整个MediaScannerService的过程就结束了。

其他的工作交给了扫描器MediaScanner。MediaScannerReciver 和MediaScannerService 并不复杂。

接下来的MediaScanner才复杂。

MediaScanner

MediaScanner 的初始化
[-->MediaScanner.java]
public class MediaScanner implements AutoCloseable {
    static {
        System.loadLibrary("media_jni");
        native_init();
    }
    ...
}

 

MediaScanner类中,初始化了好的成员变量,这边就不看了,除了这些变量 还加载了个so,并调用native_init,他是个jni方法。

它连接到android_media_MediaScanner.cpp中。

[-->MediaScanner.java]
static JNINativeMethod gMethods[] = {
...
    {
        "native_init",
        "()V",
        (void *)android_media_MediaScanner_native_init
    },
...
};
int register_android_media_MediaScanner(JNIEnv *env)
{
    return AndroidRuntime::registerNativeMethods(env,
                kClassMediaScanner, gMethods, NELEM(gMethods));
}

 

老规矩查看它的注册函数。

[-->android_media_mediascanner.cpp]
static const char* const kClassMediaScanner =
        "android/media/MediaScanner";
        static void
android_media_MediaScanner_native_init(JNIEnv *env)
{
    ALOGV("native_init");
    jclass clazz = env->FindClass(kClassMediaScanner);
    if (clazz == NULL) {
        return;
    }
    fields.context = env->GetFieldID(clazz, "mNativeContext", "J");
    if (fields.context == NULL) {
        return;
    }
}

 

native_init 中没做什么关键的操作,只是设置了个Context。

MediaScaner的构造函数.

[-->MediaScanner.java]
public MediaScanner(Context c, String volumeName) {
        native_setup();
        ...
        mMediaProvider = mContext.getContentResolver()
                .acquireContentProviderClient(MediaStore.AUTHORITY);
                ...

 

又调用jni 方法 native_setup;还创建了个mMediaProvider,就是数据操作的。扫描完成的数据都是通过它 插入到数据库。

[-->android_media_mediascanner.cpp]
static void
android_media_MediaScanner_native_setup(JNIEnv *env, jobject thiz)
{
    ALOGV("native_setup");
    MediaScanner *mp = new StagefrightMediaScanner;
    if (mp == NULL) {
        jniThrowException(env, kRunTimeException, "Out of memory");
        return;
    }

    env->SetLongField(thiz, fields.context, (jlong)mp);
}

 

native_setup 在jni中 创建了个StagefrightMediaScanner 并设置到变量中。

总结 MediaScanner 初始化中做的事情

  • - 链接了java 到 jni 到cpp 的过程
  • - 获取了mediaprovider
  • - 创建了底层的mediascanner (StageFrightMediaScanner)
MediaScaner scanDirectories分析
public void scanDirectories(String[] directories) {
...
    for (int i = 0; i < directories.length; i++) {
        processDirectory(directories[i], mClient);
    }
...
}
private native void processDirectory(String path,MediaScannerClient client);

 

processDirectory :directories 如果有多个。。。 所有for循环。

而processDirectory 是个native 方法。

static void
android_media_MediaScanner_processDirectory(
        JNIEnv *env, jobject thiz, jstring path, jobject client)
{
...
    MediaScanner *mp = getNativeScanner_l(env, thiz);
...
    const char *pathStr = env->GetStringUTFChars(path, NULL);
    MyMediaScannerClient myClient(env, client);
    MediaScanResult result = mp->processDirectory(pathStr, myClient);
...
}

 

在jni中调用getNativeScanner_l 获取到了 native_setup设置的变量对象及StagefrightMediaScanner,并调用那个它的processDirectory,

注意这边还创建了个 MyMediaScannerClient,并传递了下去,这个类是回掉用的,下面会用到。

struct StagefrightMediaScanner : public MediaScanner {
}
StagefrightMediaScanner 是MediaScanner 的子类。所有查看MediaScanner的processDirectory方法


MediaScanResult MediaScanner::processDirectory(
        const char *path, MediaScannerClient &client) {
 ...
    MediaScanResult result = doProcessDirectory(pathBuffer, pathRemaining, client, false);
...
}
MediaScanResult MediaScanner::doProcessDirectory(
        char *path, int pathRemaining, MediaScannerClient &client, bool noMedia) {
 ...
    DIR* dir = opendir(path);
...
    MediaScanResult result = MEDIA_SCAN_RESULT_OK;
    while ((entry = readdir(dir))) {
        doProcessDirectoryEntry(path, pathRemaining, client, noMedia, entry, fileSpot);
        ...
        }
    }
...
}

MediaScanResult MediaScanner::doProcessDirectoryEntry(
        char *path, int pathRemaining, MediaScannerClient &client, bool noMedia,
        struct dirent* entry, char* fileSpot) {
...
    int type = entry->d_type;
   ...
    if (type == DT_DIR) {
    ...

        if (stat(path, &statbuf) == 0) {
            status_t status = client.scanFile(path, statbuf.st_mtime, 0,
                    true /*isDirectory*/, childNoMedia);
...
        }

...
        MediaScanResult result = doProcessDirectory(path, pathRemaining - nameLength - 1,
                client, childNoMedia);
  ...
    } else if (type == DT_REG) {
...
        status_t status = client.scanFile(path, statbuf.st_mtime, statbuf.st_size,
                false /*isDirectory*/, noMedia);
...
    }

    return MEDIA_SCAN_RESULT_OK;
}

 

  • processDirectory 中调用 doProcessDirectory ,
  • doProcessDirectory 中开始对sdcard的每个目录取出俩 给doProcessDirectoryEntry
  • doProcessDirectoryEntry 对目录里的内容又取出来 判断如果是 目录 调用client.scanFile
  • 通知上层扫这个目录,又doProcessDirectory有点递归的感觉了,如果是文件client.scanFile通知上层扫这个文件。

而这个client 就是我们上面创建的MyMediaScannerClient。

总结下这里面的意思,就是从上往下传递了一个sdcard 目录后,这边对这个目录进行递归,如果取到的是目录或者文件都调用client.scanFile 进行解析,唯一的不同是中间的参数,如果是目录isDirectory是true的,如果是文件是false的。

难么接下来看下 MyMediaScannerClient 的scanFile。

MyMediaScannerClient
class MyMediaScannerClient : public MediaScannerClient
{
public:
    MyMediaScannerClient(JNIEnv *env, jobject client)
...
    {
...
            mScanFileMethodID = env->GetMethodID(
                                    mediaScannerClientInterface,
                                    "scanFile",
                                    "(Ljava/lang/String;JJZZ)V");

            mHandleStringTagMethodID = env->GetMethodID(
                                    mediaScannerClientInterface,
                                    "handleStringTag",
                                    "(Ljava/lang/String;Ljava/lang/String;)V");

            mSetMimeTypeMethodID = env->GetMethodID(
                                    mediaScannerClientInterface,
                                    "setMimeType",
                                    "(Ljava/lang/String;)V");
    }
...

    virtual status_t scanFile(const char* path, long long lastModified,
            long long fileSize, bool isDirectory, bool noMedia)
    {
...
        mEnv->CallVoidMethod(mClient, mScanFileMethodID, pathStr, lastModified,
                fileSize, isDirectory, noMedia);
    }
}

 

这个类在刚才创建的时候 没讲到 ,他的构造 传入了client 这个client 是上层java的对象(MediaScaner.java),并且记录它的好几个方法

  • - scanFile
  • - handleStringTag
  • - setMimeType

在 scanFile 函数中就直接调用了上层java的scanFile 方法。至此又回到了java,难么思考下,千辛万苦,将路径传递下来,就在下面做了个递归遍历。(如果是单个文件就不会下cpp,就直接调用这个java层的scanFile
)

public void scanFile(String path, long lastModified, long fileSize,
                boolean isDirectory, boolean noMedia) {
...
            doScanFile(path, null, lastModified, fileSize, isDirectory, false, noMedia);
        }

        public Uri doScanFile(String path, String mimeType, long lastModified,..{
...
                FileEntry entry = beginFile(path, mimeType, lastModified,
                        fileSize, isDirectory, noMedia);
...
                if (entry != null && (entry.mLastModifiedChanged || scanAlways)) {
...
                        boolean isaudio = MediaFile.isAudioFileType(mFileType);
                        boolean isvideo = MediaFile.isVideoFileType(mFileType);
                        boolean isimage = MediaFile.isImageFileType(mFileType);
                        if (isaudio || isvideo) {
                            processFile(path, mimeType, this);
                        }
                        result = endFile(entry, ringtones, notifications, alarms, music, podcasts);
                    }
                }
...
            return result;
        }

 

这里意思就说扫单个文件了。

  • - beginFile
  • - processFile
  • - endFile

主要是这三个过程

  • - beginFile 将文件或者目录的一些属性解析出来,比如文件的时间 文件的类型等等 。
  • - 接着判断这个属性 是否是null 还有entry.mLastModifiedChanged 修改时间 是否是改过,
  • - 然后 判断是不是 音频,视频和图片等一些属性。
  • - 如果是音频视频就调用processFile ,解析出一些媒体属性,如:音频格式,视频宽高等等。如果不是就结束了endFile
  • - endFile 就是将上面解析出来的信息统计 然后插入到数据库。

processFile 还是比较重要的 我们看下它是如何去解析出音视频信息的。processFile 又是个native 方法。看来又要下去了。

static void
android_media_MediaScanner_processFile(
        JNIEnv *env, jobject thiz, jstring path,
        jstring mimeType, jobject client)
{
    MediaScanner *mp = getNativeScanner_l(env, thiz);
    MyMediaScannerClient myClient(env, client);
    MediaScanResult result = mp->processFile(pathStr, mimeTypeStr, myClient);

}

 

还是和上次的差不多 拿出StagefrightMediaScanner 的对象 建立个MyMediaScannerClient对象 往下 调用mp->processFile 方法和传递myClient;

在StagefrightMediaScanner 中如下 processFile 直接调用了processFileInternal。

MediaScanResult StagefrightMediaScanner::processFile(
        const char *path, const char *mimeType,
        MediaScannerClient &client) {
...
    MediaScanResult result = processFileInternal(path, mimeType, client);
...
    return result;
}

MediaScanResult StagefrightMediaScanner::processFileInternal(
        const char *path, const char * /* mimeType */,
        MediaScannerClient &client) {
    const char *extension = strrchr(path, '.');
...
    sp<MediaMetadataRetriever> mRetriever(new MediaMetadataRetriever);
    int fd = open(path, O_RDONLY | O_LARGEFILE);
 ...
        status = mRetriever->setDataSource(fd, 0, 0x7ffffffffffffffL);
...
    for (size_t i = 0; i < kNumEntries; ++i) {
        const char *value;
        if ((value = mRetriever->extractMetadata(kKeyMap[i].key)) != NULL) {
            status = client.addStringTag(kKeyMap[i].tag, value);
..
        }
    }
    return MEDIA_SCAN_RESULT_OK;
}

 

processFileInternal 中 建立了MediaMetadataRetriever 然后调用setDataSource 将数据源 设置进去,在通过mRetriever->extractMetadata获取到了解析出来的信息。

关于MediaMetadataRetriever 怎么解析的涉及到音视频的解码知识。这里就不多讲了。

最后通过addStringTag将一个个属性添加进去。主要addStringTag 是client 的接口。

所以我们又要往上走了。

class MyMediaScannerClient : public MediaScannerClient
{
public:
    MyMediaScannerClient(JNIEnv *env, jobject client)
        :   mEnv(env),
...
    {
 ...
            mHandleStringTagMethodID = env->GetMethodID(
                                    mediaScannerClientInterface,
                                    "handleStringTag",
                                    "(Ljava/lang/String;Ljava/lang/String;)V");
...
        }
    }
    virtual status_t handleStringTag(const char* name, const char* value)
    {
...
        mEnv->CallVoidMethod(
            mClient, mHandleStringTagMethodID, nameStr, valueStr);
...
    }

 

所以通过两次进过这个类 MediaScannerClient 我们懂得了 它只是个过客。其实jni中的都是个过客,从MediaScanner.java <-->MediaScanner.cpp(StagefrightMediaScanner.cpp) ,中间就是个jni,回到MediaScanner.java 中。

public void handleStringTag(String name, String 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 (name.equalsIgnoreCase("height")) {
                mHeight = parseSubstring(value, 0, 0);
            } else {

            }
        }

}

 

MediaScanner.java 的handleStringTag中就将一些信息变量记录下,方便后面的endFile。

最终的归宿

private Uri endFile(FileEntry entry, boolean ringtones, boolean notifications,
                boolean alarms, boolean music, boolean podcasts)
                throws RemoteException {
ContentValues map = new ContentValues();
map.put(MediaStore.MediaColumns.DATA, mPath);
map.put(MediaStore.MediaColumns.TITLE, mTitle);
map.put(Video.Media.ALBUM, mAlbum );
...
mMediaProvider.insert(tableUri, values);
...
}

 

到此所以媒体扫描的流程基本上都清楚了。总的流程如下

MediaScannerReciver.java-->MediaScannerService.java -->MediaScanner.java-->android_media_mediascan.cpp-->StagefrightMediaScanner.cpp-->MediaScanner.cpp;

 

这个从java 到cpp的上上下下

但是有很多细节还是需要在实际锻炼中才能注意到的。

 

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: