android多媒体文件扫描

         Android多媒体文件扫码完整流程:本地存储和外部存储设备监听,多媒体文件扫描,media meta解析,多媒体文件显示,播放。

          Android外部存储空间由 vold init 服务和 StorageManagerService 系统服务共同管理。外部实体存储卷的装载由 vold 处理:通过执行分阶段操作准备好媒体,然后再将其提供给应用。

一  设备监听

 1 系统架构

        vold进程接收来自内核的外部设备消息,用于管理和控制Android平台外部存储设备,包括SD插拨、挂载、卸载、格式化等;当外部设备发生变化时,内核通过Netlink发送uEvent格式的消息给用户空间程序,Netlink 是一种基于异步通信机制,在内核与用户应用间进行双向数据传输的特殊 socket,用户态应用使用标准的socket API 就可以使用 netlink 提供的强大功能;

                                                                    图1 设备监听流程图

1 StorageManager是提供给应用直接使用的类,android Binder客户端,通过Binder IPC和MountService交互。

2 从 Android 8.0 开始,MountService 类已更名为 StorageManagerService,并且StorageManagerService与Vold的通信方式由socket方式变为binder方式,精简了部分代码,以及之前处理信息交互的CommandListener类也被取消了。

StorageManagerService,android Binder服务端,运行在system_server进程中。在StorageManagerService内部。与Vold建立的Socket通信被封装到了NativeDaemonConnector中,MountService便使用NativeDaemonConnector对象来接收和发送socket消息到Vold进程中的。例如向vold发送挂载SD卡命令,或者接收来自vold的外设热插拔事件。

3 Vold(volume daemon),native后台进程,在Vold内部,FrameworkListener接收StorageManagerService发来的Socket消息,NetlinkListener接收Kernel发来的Uevent事件。

                                                                       图2 8.0之前volp架构图

                                                                       图3 uevent消息传送流程

        StorageManagerService启动VoldConnector socket连接线程,用于循环连接服务端,保证连接不被中断,当成功连接Vold时,循环从服务端读取数据。

        CommandListener模块启动时创建一个Socket监听线程,用于跨进程通信,监听MountService连接,Java层客户端MountService就是通过该Socket和服务端Vold进行通信的。

        Netlink是Linux系统中用户空间进程和Kernel进行通信的一种机制,用户空间进程可以接收来自Kernel的消息,同时也可以向Kernel发送一些控制命令。       

        uevent和Linux的设备文件系统及设备模型有关,是sysfs向用户空间发送的消息。消息格式实际上是一串字符串。当外部设备发生变化时,会引起Kernel发送Uevent消息;一般设备在/sys对应的目录下有个叫uevent的文件,往该文件里写入指定数据,也会触发Kernel发送和该设备相关的uevent消息,内核通过uevent告知外部存储系统发生的变化。

        启动NetlinkHandler过程通过创建一个SocketListener工作线程来监听Kernel netlink发送的UEvent消息。NetlinkManager通过NetlinkHandler将接收到Kernel内核发送的Uenvet消息,转化成了NetlinkEvent结构数据传递给VolumeManager处理。

2 StorageManagerService消息接收流程

StorageManagerService需要接收两种类型的消息:
1)当外部存储设备发生热插拔时,kernel将通过netlink方式通知Vold,Vold进程经过一系列处理后最终将uevent事件消息发送给MountService,见图3。
2)当StorageManagerService向Vold发送命令后,将接收到Vold的响应消息;
无论是何种类型的消息,StorageManagerService都是通过VoldConnector线程来循环接收Vold的请求,整个消息接收流程如下:

                                                                       图4 MountService接收消息流程

参考文章:

Android之vold进程启动源码分析_快乐安卓的博客-CSDN博客_vold进程

Android Service之MountService源码分析_快乐安卓的博客-CSDN博客_android mountservice

二 多媒体文件扫描

           在ANDROID系统中,定制了三种事件会触发MediaScanner去扫描磁盘文件:

1 ACTION_BOOT_COMPLETED:系统启动完后发出这个消息,此时会把内部卷标(“internal”)和外部卷标(“external”)都扫描一下。

2 ACTION_MEDIA_MOUNTED:插卡事件触发的消息

 3 ACTION_MEDIA_SCANNER_SCAN_FILE:一般是在一些文件操作后,开发人员手动发出的一个重新扫描多媒体文件的消息。

发送消息通过sendBroadcast函数完成,比如广播一个ACTION_MEDIA_MOUNTED消息:

sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.parse("file://"

                       + Environment.getExternalStorageDirectory())));

注意:从Android 4.4开始,ACTION_MEDIA_MOUNTED广播只能由系统(系统服务MountService)发出,普通用户是无权发送的。
 

   MediaScannerReceiver:多媒体扫描广播接收者,继承 BroadcastReceiver,主要响应APP发送的广播命令例如android.intent.action.MEDIA_MOUNTED,并开启MediaScannerService 执行扫描工作。此时会把内部卷(“internal”)和外部卷(“external”)都扫描一下。

/external/sl4a/Common/src/com/googlecode/android_scripting/facade/media/MediaScannerFacade.java

public class MediaScannerFacade extends RpcReceiver {

private final Service mService;

private final MediaScanner mScanService;

private final EventFacade mEventFacade;

private final MediaScannerReceiver mReceiver;

public MediaScannerFacade(FacadeManager manager) {

super(manager);

mService = manager.getService();

mScanService = new MediaScanner(mService, "external");

mEventFacade = manager.getReceiver(EventFacade.class);

mReceiver = new MediaScannerReceiver();

}

//作为MediaScannerFacade内部类

public class MediaScannerReceiver extends BroadcastReceiver {

@Rpc(description = "Scan external storage for media files.")

public void mediaScanForFiles() {

mService.sendBroadcast(new Intent(Intent.ACTION_MEDIA_MOUNTED,

Uri.parse("file://" + Environment.getExternalStorageDirectory())));

mService.registerReceiver(mReceiver,

new IntentFilter(Intent.ACTION_MEDIA_SCANNER_FINISHED));

}

@Rpc(description = "Scan for a media file.")

public void mediaScanForOneFile(@RpcParameter(name = "path") String path) {

mService.sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.parse(path)));

}

}

MediaScannerReceiver可以接收4种类型的广播
AndroidManifest.xml
<receiver android:name="MediaScannerReceiver">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" /
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.MEDIA_MOUNTED" />
<data android:scheme="file" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.MEDIA_UNMOUNTED" />
<data android:scheme="file" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.MEDIA_SCANNER_SCAN_FILE" />
<data android:scheme="file" />
</intent-filter>
</receiver>


         MediaScannerService:多媒体扫描服务,继承 Service,主要是处理 APP 发送的请求,用到 Framework 中的 MediaScanner 来共同完成具体扫描工作,并获取媒体文件的 metadata,最后将数据写入或删除 MediaProvider 提供的数据库中。第一次被启动走onCreate,第二次启动走onStartCommand,从广播里面获取信息发送给mServiceHandler,通过handler通知service启动线程扫描。


         MediaProvider:多媒体内容提供者,继承 ContentProvider,主要是负责操作数据库,并提供给别的程序 insert、query、delete、update 等操作。


         MediaStore:查询入口。MediaProvider相当于存储文件的仓库,而MediaStore相当于展示媒体文件的资源管理器。MediaStore把所有的文件分为几类:

MediaStore.Files所有的文件,包括非多媒体文件。

MediaStore.InternalThumbnails,这个类是被图像缩略图,视频缩略图内部使用的。它没有提供uri,所以别的地方应该访问不了。

MediaStore. Images,图像文件存储的地方。

MediaStore.Audio,音频文件类存储的地方。MediaStore.Video,视频文件类存储的地方。

      每种类型都可以通过getContentUri()接口获取具体的引用位置。

     多媒体数据库路径:data/data/com.android.providers.media/database/external.db或者internal.db

MediaScanner.cpp主要负责文件的扫描,深度扫描存储设备,每扫描到文件就通过JNI调用Java侧的scanFile方法。

        从媒体扫描的整体流程图中可以看到,整个扫描的过程中,始终都只有一个工作线程,发现文件,解析文件,插入文件。如果存储里面的文件特别多,这个过程是会非常耗时。

MediaScannerService.java handleMessage
MediaScannerService.java scan 在此处会广播消息Intent.ACTION_MEDIA_SCANNER_STARTED
MediaScanner.java scanDirectories
MediaScanner.java processDirectory
JNI android_media_MediaScanner_processDirectory
MediaScanner::processDirectory
MediaScanner::doProcessDirectory
MediaScanner::doProcessDirectoryEntry 如果还没到实际目录则再调用doProcessDirectory
MediaScanner.java中的scanFile
MediaScanner.java中的doScanFile,在此函数中会先调用beginFile,判断fileType,再分别调用MediaFile.isAudioFileType、isVideoFileType、isImageFileType,判断当前文件是什么类型,最后调用endFile,把文件信息写入数据库。


MediaScanner.java beginFile 在此函数中如果mimeType不是null,mFileType = MediaFile.getFileTypeForMimeType(mimeType),如果mimeType是null,MediaFile.getFileType(path);获取fileType


MediaFile.java getFileType
MediaFile.java isVideoFileType 在此函数中判断fileType是否介于FIRST_VIDEO_FILE_TYPE和LAST_VIDEO_FILE_TYPE,若是则返回true。
MediaScanner.java endFile 再次函数中先调用toValues()获取values,然后根据fileType给values中put数据,最后把values insert到mMediaProvider中。
 

扫码低效率原因及解决方案

       1 媒体文件数量较多,例如2000个音频文件,每个文件解析平均0.5秒左右。 见MediaScanner流程图,扫码,解析,插入媒体信息只有一个工作线程。扫码一个线程,解析和插入媒体信息采用线程池 

2 每次插入U盘,媒体扫描都会扫描所有的外部存储路径。在这里可以调整为插入了哪个U盘,就只扫描对应的盘符。通过去除每次不必要的扫描路径的方式,也能实现提升扫描性能的目的。 

  3 用户通常使用的U盘一般是固定不变的,里面的数据通常也只是部分发生变更。Android原生的数据库存储设计是,插入U盘将U盘内的文件数据插入到external.db里面,拔掉U盘时,会去清理掉这部分的数据。这样下次插入U盘的时候,我们又要重新扫描一次U盘。我们能不能做到第一次插入一个U盘后,永久的记录下这个数据,哪怕是U盘被拔掉了。等第二次插入U盘时,我们可以重新使用之前记录下的数据,仅对发生改变的文件做数据变更就好了。这样也可以大大的提升扫描性能。具体的方案是,将每个U盘的数据插入到文件external-uuid.db,这样可以做到每一个U盘对应到一个external.db文件。 

三 meta解析

MediaExtrator获取音频轨道数

MediaMetadataRetriever获取歌手和时长 

通常实际项目中,2种方法混合使用,可满足需求。

1 MediaExtractor

Extractor在multimedia框架扮演着解析器的角色,用于解析文件的封装。extractor会把视频文件解析成音频流和视频流,把音频文件解析成音频流。这里借用一个大神的multimedia框架图展示一下Extractor所处的位置。从图中我们可以清楚的看到,NuPlayer会为每个播放的文件,创建一个MediaExtractor,这个类的作用就是解析,概念上等同于demuxer或者Parser。MediaExtractor负责从文件中分离音视频数据,并抽象成MediaSource。MediaSource生产数据,送往MediaCodec。MediaCodec又将数据通过ACodec和OMX的接口送往Decoder。Decoder解码后的数据会返回给MediaCodec,之后再由MediaCodec送往Renderer模块。
 

2 MediaMetadataRetriever

MediaMetadataRetriever是Android原生提供的获取音视频文件信息的一个类,我们可以通过这个类的相关方法获取一些基本信息,如视频时长、宽高、帧率、方向、某一帧的图片等。

3 external.db

图片数据库

images:图片信息

thumbnails:缩略图

视频数据库

video:视频信息

videothumbnails:视频缩略图

音频数据库

album_art:专辑封面

albums:专辑信息

android_metadata:当前字符编码

artists:艺术家

audio_genres:流派

audio_genres_map:音频流派映射

audio_meta:音频信息

audio_playlists:播放列表

audio_playlists_map:音频播放列表映射
 

4 mediaProvider

文件路径:

MediaProvider/src/com/android/providers/media/MediaProvider.java

MediaProvider.java就是创建数据库,对外提供URI以实现对数据库的增删改查功能以及URI管理。直接与数据库打交道。如果要修改数据库中的字段,则可以在此文件中修改。比如现在流行的在拍照的时候记录下城市信息,则可以做如下修改:

db.execSQL("CREATE VIEW images AS SELECT _id,_data,_size,_display_name,mime_type,title,"
- + "date_added,date_modified,description,picasa_id,isprivate,latitude,longitude,"
+ + "date_added,date_modified,description,picasa_id,isprivate,city,latitude,longitude,"
+ "datetaken,orientation,mini_thumb_magic,bucket_id,bucket_display_name,width,"
+ "height FROM files WHERE media_type=1");
…
private static final String IMAGE_COLUMNS =
"_data,_size,_display_name,mime_type,title,date_added," +
- "date_modified,description,picasa_id,isprivate,latitude,longitude," +
+ "date_modified,description,picasa_id,isprivate,city,latitude,longitude," +
"datetaken,orientation,mini_thumb_magic,bucket_id,bucket_display_name," +
"width,height";

private static final String IMAGE_COLUMNSv407 =
"_data,_size,_display_name,mime_type,title,date_added," +
- "date_modified,description,picasa_id,isprivate,latitude,longitude," +
+ "date_modified,description,picasa_id,isprivate,city,latitude,longitude," +
"datetaken,orientation,mini_thumb_magic,bucket_id,bucket_display_name";

   

MediaProvider 另外一个非常重要的功能就是通过URI给其他应用提供数据。那么MediaProvider提供的url如下:

URI_MATCHER.addURI("media", "*/images/media", IMAGES_MEDIA);   // * 可换成 external 或者 internal , 下同
URI_MATCHER.addURI("media", "*/images/media/#", IMAGES_MEDIA_ID);
URI_MATCHER.addURI("media", "*/images/thumbnails", IMAGES_THUMBNAILS);
URI_MATCHER.addURI("media", "*/images/thumbnails/#", IMAGES_THUMBNAILS_ID);
URI_MATCHER.addURI("media", "*/audio/media", AUDIO_MEDIA);
URI_MATCHER.addURI("media", "*/audio/media/#", AUDIO_MEDIA_ID);
URI_MATCHER.addURI("media", "*/audio/media/#/genres", AUDIO_MEDIA_ID_GENRES);
URI_MATCHER.addURI("media", "*/audio/media/#/genres/#", AUDIO_MEDIA_ID_GENRES_ID);
URI_MATCHER.addURI("media", "*/audio/media/#/playlists", AUDIO_MEDIA_ID_PLAYLISTS);
URI_MATCHER.addURI("media", "*/audio/media/#/playlists/#", AUDIO_MEDIA_ID_PLAYLISTS_ID);
URI_MATCHER.addURI("media", "*/audio/genres", AUDIO_GENRES);
URI_MATCHER.addURI("media", "*/audio/genres/#", AUDIO_GENRES_ID);
URI_MATCHER.addURI("media", "*/audio/genres/#/members", AUDIO_GENRES_ID_MEMBERS);
URI_MATCHER.addURI("media", "*/audio/genres/all/members", AUDIO_GENRES_ALL_MEMBERS);
URI_MATCHER.addURI("media", "*/audio/playlists", AUDIO_PLAYLISTS);
URI_MATCHER.addURI("media", "*/audio/playlists/#", AUDIO_PLAYLISTS_ID);
URI_MATCHER.addURI("media", "*/audio/playlists/#/members", AUDIO_PLAYLISTS_ID_MEMBERS);
URI_MATCHER.addURI("media", "*/audio/playlists/#/members/#", AUDIO_PLAYLISTS_ID_MEMBERS_ID);
URI_MATCHER.addURI("media", "*/audio/artists", AUDIO_ARTISTS);
URI_MATCHER.addURI("media", "*/audio/artists/#", AUDIO_ARTISTS_ID);
URI_MATCHER.addURI("media", "*/audio/artists/#/albums", AUDIO_ARTISTS_ID_ALBUMS);
URI_MATCHER.addURI("media", "*/audio/albums", AUDIO_ALBUMS);
URI_MATCHER.addURI("media", "*/audio/albums/#", AUDIO_ALBUMS_ID);
URI_MATCHER.addURI("media", "*/audio/albumart", AUDIO_ALBUMART);
URI_MATCHER.addURI("media", "*/audio/albumart/#", AUDIO_ALBUMART_ID);
URI_MATCHER.addURI("media", "*/audio/media/#/albumart", AUDIO_ALBUMART_FILE_ID);
URI_MATCHER.addURI("media", "*/video/media", VIDEO_MEDIA);
URI_MATCHER.addURI("media", "*/video/media/#", VIDEO_MEDIA_ID);
URI_MATCHER.addURI("media", "*/video/thumbnails", VIDEO_THUMBNAILS);
URI_MATCHER.addURI("media", "*/video/thumbnails/#", VIDEO_THUMBNAILS_ID);
URI_MATCHER.addURI("media", "*/media_scanner", MEDIA_SCANNER);
URI_MATCHER.addURI("media", "*/fs_id", FS_ID);
URI_MATCHER.addURI("media", "*/version", VERSION);
URI_MATCHER.addURI("media", "*/mtp_connected", MTP_CONNECTED);
URI_MATCHER.addURI("media", "*", VOLUMES_ID);
URI_MATCHER.addURI("media", null, VOLUMES);
// Used by MTP implementation
URI_MATCHER.addURI("media", "*/file", FILES);
URI_MATCHER.addURI("media", "*/file/#", FILES_ID);
URI_MATCHER.addURI("media", "*/object", MTP_OBJECTS);
URI_MATCHER.addURI("media", "*/object/#", MTP_OBJECTS_ID);
URI_MATCHER.addURI("media", "*/object/#/references", MTP_OBJECT_REFERENCES);
// Used only to trigger special logic for directories
URI_MATCHER.addURI("media", "*/dir", FILES_DIRECTORY);
/**
* @deprecated use the 'basic' or 'fancy' search Uris instead
*/
URI_MATCHER.addURI("media", "*/audio/" + SearchManager.SUGGEST_URI_PATH_QUERY,
AUDIO_SEARCH_LEGACY);
URI_MATCHER.addURI("media", "*/audio/" + SearchManager.SUGGEST_URI_PATH_QUERY + "/*",
AUDIO_SEARCH_LEGACY);
// used for search suggestions
URI_MATCHER.addURI("media", "*/audio/search/" + SearchManager.SUGGEST_URI_PATH_QUERY,
AUDIO_SEARCH_BASIC);
URI_MATCHER.addURI("media", "*/audio/search/" + SearchManager.SUGGEST_URI_PATH_QUERY +
"/*", AUDIO_SEARCH_BASIC);

// used by the music app's search activity
URI_MATCHER.addURI("media", "*/audio/search/fancy", AUDIO_SEARCH_FANCY);
URI_MATCHER.addURI("media", "*/audio/search/fancy/*", AUDIO_SEARCH_FANCY);

在命令行中可以直接通过content query进行数据的查询,比如常用的:

adb shell content query --uri content://media/external/file //查询外部存储器扫描到的文件详情列表

adb shell content query --uri content://media/external/dir //参照上一条

adb shell content query --uri content://media/internal/audio/media/ --where “_id=7” //查询内部存储器中 audio文件,且id 为 7 的内容

adb shell content query --uri content://media/external/images/media/ //查询外部存储器上有多少图片存储

adb shell content query --uri content://media/external/images/media/ --user 0 //只查询user id为0 的用户的外部存储器的图片列表

  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

步基

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值