Android 之MediaScanner流程解析

MediaScanner详解

OK, 我们现在开始来大概分析一下android framework中MediaScanner部分的流程,若大家发现分析过程中有错误,欢迎拍砖指正。

分析流程之前,我们先给自己定个要用MediaScanner解决的问题,这样我们才会有目标感,才知道我们要干什么。否则,干巴巴的分析流程,一般都会很容易的迷失在各种code的迷雾中。

我们这里要定的目标是:获取某个MP3文件的artist & album

我们可以假定,现在有个媒体播放器,在播放music的时候,需要在UI上显示出来artist& album.

从UI的角度来说,android提供了两套可以获取mediaInfo的方法:

1.      通过MediaScanner  Scan文件后,从DB中查询相关信息;

2.      直接通过android提供的MediaMetadataRetriever类来获取相关信息,基本用法如下:

public
void
getMetaData(String filePath) {

String title, album,artist, composer, genre, mime;

MediaMetadataRetriever retriever =newMediaMetadataRetriever();

retriever.setDataSource(filePath);

title =retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE);

album =retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ALBUM);

artist =retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ARTIST);

composer =retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_COMPOSER);

genre =retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_GENRE);

mime =retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_MIMETYPE);

Log.i(TAG, “title=” +title + “,album=” + album + “,artist=” + artist + “,composer=” + composer +“,genre=” + genre + “,mime=” + mime);

}

既然我们现在要说的是MediaScanner,那自然我们要介绍的是通过第一种方法获取相关信息的流程了。

OverView

我们这里会分为3个层次来详细解读一下MediaScanner的工作流程:

1.      UI APK调用的接口;

2.      Java层调用flow;

3.      Native层调用flow;

UI调用接口

现在我们的目标是获取某个MP3文件的artist& album,因此自然需要分为两步:

1.      Scan media file

2.      Query data

1.     Scan Media File


public void scanfile(path)

{

String[] paths = new String[1];

//设置需要扫描的文件路径,这里path就是我们所要扫描的music file的路径

paths[0] = path;

// 调用MediaScannerConnection的scanFile,进行扫描。

//scanFile的第三个参数是mimeTypes,若为null,则会根据文件后缀来判断。

//scanCb是MediaScannerConnection.OnScanCompletedListener

MediaScannerConnection.scanFile(this.context, paths, null, scanCb);

}

2.     Query data

Scan完成之后,就可以在DB中查询相关的audio信息。


private void getInfo()

{

String[] colume = { "_id", "album_id", "title", "artist", "album", "year", "duration", "_size", "_data" };

StringBuilder localStringBuilder = new StringBuilder();

localStringBuilder.append("_data");

localStringBuilder.append(" LIKE ‘%");

localStringBuilder.append(musicUtils.convertToQueryString(this.mPath));

localStringBuilder.append("%‘");

localStringBuilder.append(" ESCAPE ‘\\‘");

/*colume是要选择的列,localStringBuilder是选择的条件,MediaStore.Audio.Media.EXTERNAL_CONTENT_URI是要查询的URI*/

Cursor localCursor =query(this.context, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, colume, localStringBuilder.toString(), null, null);

if (localCursor != null);

try

{

//查询到的结果会存储在localCursor中,随后可从localCursor中获取相关信息

if (localCursor.moveToFirst())

{

mSongId = localCursor.getLong(0);

mAlbumId = localCursor.getLong(1);

mTitle = localCursor.getString(2);

mSonger = localCursor.getString(3);

mAlbum = localCursor.getString(4);

mReleaseYear = localCursor.getInt(5);

mDuration = localCursor.getLong(6);

mSize = localCursor.getLong(7);

mDataPath = localCursor.getString(8);

}

catch (RuntimeException localRuntimeException)

{

}

finally

{

if (localCursor != null)

localCursor.close();

}

}

这里查询的URI为MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,对应的db存储的路径为/data/data/com.android.providers.media/external.db。可用SQLiteSpy查看此DB的内容。

至此从UI的角度来看,已经完成了获取mp3文件info的任务。

Java层MediaScanflow

我们这里主要分析Scanfile的流程,不再对查询信息的流程进行解析。

OK,先看java层的时序图:

MediaScannerClient主要用途是给app提供一个和MediaScannerService交互的桥梁。App可以通过MediaScannerClient给MediaScanenrService传递需要scan的文件。Mediascanner service会对传入的文件进行scan,读取metadata,并将文件添加进mediacontent provider。MediaScannerConnectionClient也为mediascanner service提供了一个返回最近被scan的文件的Uri。

MediaScannerService类其实起到的主要作用就是创建出来MediaScanner,之后调用MediaScanner的ScanFile接口进行工作,最终做具体工作的其实是MediaScanner和MyMediaScannerClient。

下面我们从头追一下code。

Scan动作的发起者是MediaScannerConnection.

Code位于\frameworks\base\media\java\android\media\MediaScannerConnection.java:


public static void scanFile(Context context, String[] paths, String[] mimeTypes,

OnScanCompletedListener callback) {

ClientProxy client = new ClientProxy(paths, mimeTypes, callback);

MediaScannerConnection connection = new MediaScannerConnection(context, client);

client.mConnection = connection;

//scanFile函数只是调用了一下MediaScannerConnection的connect()函数。

connection.connect();

}

MediaScannerConnection中有两个名叫scanFile的函数,不过一个是public static的,另外一个则是普通的private函数。Static修饰的scanFile是对UI的接口。通过上面的code可以看出,其实scanFile并未做任何scan的动作,只是new了一个MediaScannerConnection,调用了一下它的connect()函数。

我们再来看一下MediaScannerConnection的connect()函数:


public void connect() {

synchronized (this) {

if (!mConnected) {

Intent intent = new Intent(IMediaScannerService.class.getName());

mContext.bindService(intent, this, Context.BIND_AUTO_CREATE);

mConnected = true;

}

}

}

当Service绑定之后,会触发MediaScannerConnection的onServiceConnected函数,而在这个函数中,又调用了ClientProxy的onMediaScannerConnected函数(ClientProxy继承自MediaScannerConnectionClient,ClientProxy是在调用MediaScannerConnection的scanFile函数时new 出来的,大家可以回过头看看前面的scanFile函数),在onMediaScannerConnected中,会调用ClientProxy的scanNextPath(),scanNextPath()中会调用MediaScannerConnection的scanFile()【注意,这里的scanFile就是我们之前提到过的privatescanFile】。scanFile()中会调用到IMediaScannerService的requestScanFile。在requestScanFile中则会启动MediaScannerService.这部分代码比较简单,就不一一列举出来了。

到现在为止,看看我们有什么了。从scanFile开始一直到requestScanFile,我们最终启动了MediaScannerService。那么下面的任务就交给MediaScannerService来完成了。

MediaScannerService

MediaScannerService.java在packages\providers\MediaProvider\src\com\android\providers\media目录下。

在MediaScannerService启动的时候会new一个Looper,在Looper中又new了一个ServiceHandler。

下来我们看一下MediaScannerService的onStartCommand


public int onStartCommand(Intent intent, int flags, int startId)

{

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;

}

Message msg = mServiceHandler.obtainMessage();

msg.arg1 = startId;

msg.obj = intent.getExtras();

//此处给mServiceHandler发送了一个消息,其handlerMessage会收到这个消息。

mServiceHandler.sendMessage(msg);

// Try again later if we are killed before we can finish scanning.

return Service.START_REDELIVER_INTENT;

}

接着看下handlerMessage做了什么事情:


public void handleMessage(Message msg)

{

Bundle arguments = (Bundle) msg.obj;

String filePath = arguments.getString("filepath");

try {

//我们传入了MP3文件路径,所以会走这个分支

if (filePath != null) {

IBinder binder = arguments.getIBinder("listener");

IMediaScannerListener listener =

(binder == null ? null : IMediaScannerListener.Stub.asInterface(binder));

Uri uri = null;

try {

//这里才开始了scanFile的流程。此时传入的mimetype为null.

uri = scanFile(filePath, arguments.getString("mimetype"));

} catch (Exception e) {

Log.e(TAG, "Exception scanning file", e);

}

if (listener != null) {

//scan 完成之后,通知listener

listener.scanCompleted(filePath, uri);

}

} else {

……

}

} catch (Exception e) {

Log.e(TAG, "Exception in handleMessage", e);

}

stopSelf(msg.arg1);

}

我们来看一下MediaScannerService的scanFile都干了什么。


private Uri scanFile(String path, String mimeType) {

String volumeName = MediaProvider.EXTERNAL_VOLUME;

//打开数据库。这个MediaProvider.EXTERNAL_VOLUME为external。

openDatabase(volumeName);

//创建MediaScanner,并设置其language&country

MediaScanner scanner = createMediaScanner();

try {

// make sure the file path is in canonical form

String canonicalPath = new File(path).getCanonicalPath();

//扫描文件

return scanner.scanSingleFile(canonicalPath, volumeName, mimeType);

} catch (Exception e) {

Log.e(TAG, "bad path " + path + " in scanFile()", e);

return null;

}

}

MediaScanner被创建时,先会设置language & country,之后会调用到jni层的接口native_init()& native_setup(),这里最主要的其实是在native_setup中创建native层的MediaScanner。这里我们创建的其实是一个StagefrightMediaScanner。这个我们后面再说。

MediaScanner的scanSingleFile会调用到MyMediaScannerClient(位置也在MediaScaner.java中)doScanFile。

MyMediaScannerClient

几乎所有的事情都是在MyMediaScannerClient中完成的。


public Uri doScanFile(String path, String mimeType, long lastModified,

long fileSize, boolean isDirectory, boolean scanAlways, boolean noMedia) {

Uri result = null;

……

boolean isaudio = MediaFile.isAudioFileType(mFileType);

boolean isvideo = MediaFile.isVideoFileType(mFileType);

boolean isimage = MediaFile.isImageFileType(mFileType);

……

// we only extract metadata for audio and video files

if (isaudio || isvideo) {

/*processFile是一个native方法,最终实现的地方在native层的StagefrightMediaScanner中,获取media
file的metadata,都是在这个函数中完成的*/

processFile(path, mimeType, this);

}

if (isimage) {

processImageFile(path);

}

/*获取到的Metadata存入db,是由endFile完成的。*/

result = endFile(entry, ringtones, notifications, alarms, music, podcasts);

……

return result;

}

获取到的metaData会被暂时保存在MyMediaScannerClient的成员变量中,之后通过endFile,将这些成员变量的值通过MediaProvider存入DB中。

这里还要提到一个函数:MyMediaScannerClient的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("width")) {

mWidth = parseSubstring(value, 0, 0);

} else if (name.equalsIgnoreCase("height")) {

mHeight = parseSubstring(value, 0, 0);

} else {

//Log.v(TAG, "unknown tag: " + name + " (" + mProcessGenres + ")");

}

}

此函数最终调用的地方,其实是通过JNI被native层的MediaScannerClient的endfile调用。用来将通过MediaMetadataRetriever获取到的metadata,送给java层MyMediaScannerClient的成员变量中。

至此,java层MediaScan的流程就算全部结束了。

Native 层MediaScanner  flow

废话不多说,先上时序图:

在上一节我们提到过在MyMediaScannerClient的doScanFile中,会调用到一个native层的函数processFile.

这个processFile实现的地方在StagefrightMediaScanner类中。位置在:frameworks\av\media\libstagefright\StagefrightMediaScanner.cpp中。


MediaScanResult StagefrightMediaScanner::processFile(

const char *path, const char *mimeType,

MediaScannerClient &client) {

ALOGV("processFile ‘%s‘.", path);

//设置mLocaleEncoding,此变量用以在endfile中判断编码格式。

client.setLocale(locale());

//beginFile很简单,就是new了两个buffer,用以存放metadata的名称和值

client.beginFile();

/*processFileInternal中通过MediaMetadataRetriever,来获取到metadata,并将其一一对应的放入在beginFile中new出来的两个buffer(mNames&mValues)之中。*/

MediaScanResult result = processFileInternal(path, mimeType, client);

/*endFile中将得到的metadata,先转码成UTF8,之后通过JNI调用到java层handleStringTag函数,将metadata相关信息保存到MyMediaScannerClient的成员变量中,前面已经有提到过了*/

client.endFile();

return result;

}

我们来看一下processFileInternal:


MediaScanResult StagefrightMediaScanner::processFileInternal(

const char *path, const char *mimeType,

MediaScannerClient &client) {

……

sp<MediaMetadataRetriever> mRetriever(new MediaMetadataRetriever);

int fd = open(path, O_RDONLY | O_LARGEFILE);

status_t status;

//1. 通过MediaMetadataRetriever,setDataSource

if (fd < 0) {

// couldn‘t open it locally, maybe the media server can?

status = mRetriever->setDataSource(path);

} else {

status = mRetriever->setDataSource(fd, 0, 0x7ffffffffffffffL);

close(fd);

}

if (status) {

return MEDIA_SCAN_RESULT_ERROR;

}

const char *value;

if ((value = mRetriever->extractMetadata(

METADATA_KEY_MIMETYPE)) != NULL) {

status = client.setMimeType(value);

if (status) {

return MEDIA_SCAN_RESULT_ERROR;

}

}

struct KeyMap {

const char *tag;

int key;

};

static const KeyMap kKeyMap[] = {

{ "tracknumber", METADATA_KEY_CD_TRACK_NUMBER },

{ "discnumber", METADATA_KEY_DISC_NUMBER },

{ "album", METADATA_KEY_ALBUM },

{ "artist", METADATA_KEY_ARTIST },

{ "albumartist", METADATA_KEY_ALBUMARTIST },

{ "composer", METADATA_KEY_COMPOSER },

{ "genre", METADATA_KEY_GENRE },

{ "title", METADATA_KEY_TITLE },

{ "year", METADATA_KEY_YEAR },

{ "duration", METADATA_KEY_DURATION },

{ "writer", METADATA_KEY_WRITER },

{ "compilation", METADATA_KEY_COMPILATION },

{ "isdrm", METADATA_KEY_IS_DRM },

{ "width", METADATA_KEY_VIDEO_WIDTH },

{ "height", METADATA_KEY_VIDEO_HEIGHT },

};

static const size_t kNumEntries = sizeof(kKeyMap) / sizeof(kKeyMap[0]);

for (size_t i = 0; i < kNumEntries; ++i) {

const char *value;

//2. 通过extractMetadata获取相关key的值

if ((value = mRetriever->extractMetadata(kKeyMap[i].key)) != NULL) {

// 3. 存值。将获取到的值添加进在begin中new出来的buffer中

status = client.addStringTag(kKeyMap[i].tag, value);

if (status != OK) {

return MEDIA_SCAN_RESULT_ERROR;

}

}

}

return MEDIA_SCAN_RESULT_OK;

}

至此,metadata就已经保存在了native层MediaScannerClient的mNames和mValues中。

接下来我们看一下MediaScannerClient的endFile。

此endFile的主要作用有两个:

1.      将mValues中的值转成UTF8格式;

2.      将转码后的值,通过JNI保存到java层的MyMediaScannerClient中


void MediaScannerClient::endFile()

{

if (mLocaleEncoding != kEncodingNone) {

int size = mNames->size();

uint32_t encoding = kEncodingAll;

//获取mValues中各个值可能的编码格式

for (int i = 0; i < mNames->size(); i++) {

encoding &= possibleEncodings(mValues->getEntry(i));

}

// if the locale encoding matches, then assume we have a native encoding.

//转码成UTF8

if (encoding & mLocaleEncoding)

convertValues(mLocaleEncoding);

// finally, push all name/value pairs to the client

for (int i = 0; i < mNames->size(); i++) {

//将转码后的value值存放至java层的MyMediaScannerClient中。

/* handleStringTag函数是通过JNI调用到java层的。有兴趣追一下此函数的话,可以看一下android_media_MediaScanner.cpp文件。此函数最终实现的地方在MediaScaner.java的MyMediaScannerClient类中,前面已经将此函数code列出来了,此处不再做过多解释*/

status_t status = handleStringTag(mNames->getEntry(i), mValues->getEntry(i));

if (status) {

break;

}

}

}

// else addStringTag() has done all the work so we have nothing to do

delete mNames;

delete mValues;

mNames = NULL;

mValues = NULL;

}

OK,到这里,MediaScanner的所有流程就已经走完了。

我们再过来回过头整理一下scanfile的几个重要地方:

1.      Native_setup(): 在此函数中要创建出来真正干活的MediaScanner出来。

2.      setLocal():要设置正确的language

3.      processFile:Scan file

4.      native层的endFile:要将scan到的value值进行正确的转码,否则存入db中的数据就可能是乱码了。

5.      java层的endFile:此处是真正的将scan到的metadata存入了db中。

Game Over!!!

Android 之MediaScanner流程解析

时间: 2024-08-05 11:16:14

Android 之MediaScanner流程解析的相关文章

Android MediaScanner源代码解析

1. 简介 MediaScanner向上和MediaProvider.MediaScannerService交互, 响应其请求; 向下和JNI交互. MediaScanner主要工作内容: (1)接收MediaScannerService的scanDirectories和scanSingleFile请求 (2)获取各个ContentProvider 环境(Audio,Video,Image,File) initialize (3)获取需要扫描文件列表,移除已删除文件信息 prescan (4)通

Android View 绘制流程(Draw) 完全解析

前言 前几篇文章,笔者分别讲述了DecorView,measure,layout流程等,接下来将详细分析三大工作流程的最后一个流程--绘制流程.测量流程决定了View的大小,布局流程决定了View的位置,那么绘制流程将决定View的样子,一个View该显示什么由绘制流程完成.以下源码均取自Android API 21. 从performDraw说起 前面几篇文章提到,三大工作流程始于ViewRootImpl#performTraversals,在这个方法内部会分别调用performMeasure

MediaPlayer本地播放流程解析(二)

上一篇MediaPlayer本地播放流程解析(一)讲了MediaPlayer的setDataSource流程,本篇将接着讲MediaPlayer的prepare流程. Prepare前面的流程一直到AwesomePlayer,和setDataSource都基本上一样,这里直接略掉.下面将从AwesomePlayer开始. status_t AwesomePlayer::prepare() { ATRACE_CALL(); Mutex::Autolock autoLock(mLock); retu

android消息处理机制原理解析

在android开发过程中相信屌丝程序员们都用过Handler来处理一些逻辑任务,比如发送延迟消息处理业务等逻辑,我们常用的图片加载缓存库ImageLoader和Picasso等内部也是通过Handler来最终有后台加载线程切换到主线程(UI线程)来更新页面的,今天就趁着离职有点儿时间就抽空的分析了下它的一点源码,在此总结出来.闲言少叙,书归正传! 先来谈谈Looper: Looper从源码上来看就是一个普通的Java类,它在消息机制中,顾名思义,就是一个消息循环的角色.有时候读源码,我习惯性的

《Android源码设计模式解析》读书笔记——Android中你应该知道的设计模式

断断续续的,<Android源码设计模式解析>也看了一遍,书中提到了很多的设计模式,但是有部分在开发中见到的几率很小,所以掌握不了也没有太大影响. 我觉得这本书的最大价值有两点,一个是从设计模式的角度去理解Android源码,结合着日常开发中的常用类,对设计模式的理解会更加的深刻:另外一个好处就是了解常用模式,再看其他人写的代码的时候,更容易理解代码思路.下面是我的读书笔记和一些思考,设计模式只整理我认为重要的部分. 建造者模式 建造者模式最明显的标志就是Build类,而在Android中最常

Android xUtils3源码解析之数据库模块

xUtils3源码解析系列 一. Android xUtils3源码解析之网络模块 二. Android xUtils3源码解析之图片模块 三. Android xUtils3源码解析之注解模块 四. Android xUtils3源码解析之数据库模块 配置数据库 DbManager.DaoConfig daoConfig = new DbManager.DaoConfig() .setDbName("test.db") .setDbVersion(1) .setDbOpenListe

Android xUtils3源码解析之注解模块

xUtils3源码解析系列 一. Android xUtils3源码解析之网络模块 二. Android xUtils3源码解析之图片模块 三. Android xUtils3源码解析之注解模块 四. Android xUtils3源码解析之数据库模块 初始化 public class BaseActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { su

[转载]Android版本更新与JSON解析

/*  *注意,这篇文章转载自:  *http://blog.csdn.net/xjanker2/article/details/6303937  *一切权利归作者所有,这里只是转载,曾经用到过这篇文章里的方法.复制过来格式混乱,建议去原作者那里获得更好体验.  *我在百度知道的提问  */ 我们看到很多Android应用都具有自动更新功能,用户一键就可以完成软件的升级更新.得益于Android系统的软件包管理和安装机制,这一功能实现起来相当简单,下面我们就来实践一下.首先给出界面效果: 1.

【转】Android kernel启动流程

;font-family:Arial, Console, Verdana, 'Courier New';line-height:normal;white-space:normal;background-color:#FFFFFF;"> linuxandroidmakefileimagecachealignment 虽然这里的Arm Linux kernel前面加上了Android,但实际上还是和普遍Arm linux kernel启动的过程一样的,这里只是结合一下Android的Makef