我们手机经常会存放一些多媒体文件,但是我们使用的时候并不需要到存储这些多媒体文件的目录中去查找,点开某个应用可能就直接使用了;这个当然不应该由用户来操作了;要是真的这样,Android早就被淘汰了;但是对这些多媒体文件的检索扫描的工作是怎么实现的呢?我们今天就正对这个问题,就源码流程方面进行梳理.
首先,还是看看时序图
和结构图
对整体的流程能有个全局的了解;然后我们就直接进入代码:
时序图:
结构图:
这个MediaScanner(媒体扫描器)是一个生存在后台的服务,凡事都有个初始;我们需要知道这个MediaScanner是怎么开始启动,然后又是怎么么运行的?
那我们就从这个MediaScanner的启动开始,然后进一步顺着代码梳理流程:
MediaScanner
一般会在系统启动的时候就会进入扫描,这是MediaScanner会接收到开机广播
,此时MediaScanner就正式启动并进行工作了.我们看代码,MediaScannerReceiver继承了BroadcastReceiver,所以MediaScannerReceiver就是一个广播接收器.就是它接受启动MediaScanner开始扫描的广播.这就是我们分析MediaScanner的入口.
首先我们看看MediaScannerReceiver会接接收哪些类型的广播:
<receiver android:name="MediaScannerReceiver">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.LOCALE_CHANGED" />
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>
说明对下列消息会相应
:
然后回到代码中看看它的实现,如下所示:
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( Displayed raw string for a moment before preview.
);
// 开机广播"Intent.ACTION_BOOT_COMPLETED"
if (Intent.ACTION_BOOT_COMPLETED.equals(action)) {
// Scan internal only.
scan(context, MediaProvider.INTERNAL_VOLUME);
// 当前区已设置改变的广播"Intent.ACTION_LOCALE_CHANGED"
} else if (Intent.ACTION_LOCALE_CHANGED.equals(action)) {
scanTranslatable(context);
} else {
// 当接收到的uri 的scheme为"files"
if (uri.getScheme().equals("file")) {
// handle intents related to external storage
String path = uri.getPath();
String externalStoragePath = Environment.getExternalStorageDirectory().getPath();
String legacyPath = Environment.getLegacyExternalStorageDirectory().getPath();
try {
path = new File(path).getCanonicalPath();
} catch (IOException e) {
Log.e(TAG, "couldn't canonicalize " + path);
return;java
}
if (path.startsWith(legacyPath)) {
path = externalStoragePath + path.substring(legacyPath.length());
}
Log.d(TAG, "action: " + action + " path: " + path);
// 外部媒体插入成功mount是发动的广播
if (Intent.ACTION_MEDIA_MOUNTED.equals(action)) {
// scan whenever any volume is mounted
scan(context, MediaProvider.EXTERNAL_VOLUME);
// 请求mediaScanner扫描文件和添加到数据库的广播
} 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));
}
private void scanTranslatable(Context context) {
final Bundle args = new Bundle();
args.putBoolean(MediaStore.RETRANSLATE_CALL, true);
context.startService(new Intent(context, MediaScannerService.class).putExtras(args));
}
}
入上所示,我们可以发现,这个MediaScannerReceiver在接收到相应的广播后,根据各自广播的不同要求进行相应的处理,即分别调用scan
,scanFile
,scantranslatable
,我们就先总体看看这三个方法的实现;我们可以发现,这三个方法其实最终都是在启动了一个叫做MediaScannerService的服务,到此MediaScannerReceiver的任务就完成了.
我们就下来就需要去MediaScannerReceiver最后启动的MediaScannerService服务来继续探索;
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();
// Start up the thread running the service. Note that we create a
// separate thread because the service normally runs in the process's
// main thread, which we don't want to block.
Thread thr = new Thread(null, this, "MediaScannerService");
thr.start();
}
@Override
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.sendMessage(msg);
// Try again later if we are killed before we can finish scanning.
return Service.START_REDELIVER_INTENT;
}
...
@Override
public void run() {
// reduce priority below other background threads to avoid interfering
// with other services at boot time.
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND +
Process.THREAD_PRIORITY_LESS_FAVORABLE);
Looper.prepare();
mServiceLooper = Looper.myLooper();onStartCommand
mServiceHandler = new ServiceHandler();
Looper.loop();
}
...
}
在MediaScannerService的定义中我们可以看到,它继承了Service和实现Runnable接口,那我就需关心一下重写的一些接口方法了;
onCreate()
方法中,首先获取了PowerManager的weaklock
,这是一个电源锁,主要是防止在MediaScanner工作的时候进入休眠,然后创建了一个新的工作线程,因为扫描是个耗时的任务,我们不能让它阻塞了主线程,那些很有可能造成ANR或者其他更危险的后果;onStartCommand
方法,Service的onCreate方法智只会运行一次,我们每次startService后,都会调用的是onStartCommand方法,所以就会在onStartCommand方法中对intent进行解析,将工作任务通过handle发送到工作线程中去处理
;run()
中我们就会看到,工作线程首先降低自身的优先级
,这是为了防止扫描任务影响开机启动;紧接着创建ServiceHandler,工作线程在oncreate
之后就进入loop状态,等待接受扫描任务;以上都是MediaScannerService的启动和任务传递的过程,我们需要进一步看看,工作线程中是怎么处理搜买哦任务的:
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) {
...
uri = scanFile(filePath, arguments.getString("mimetype"));
...
} else {
...
scan(directories, volume);
...
}
stopSelf(msg.arg1);
}
}
以上就是ServiceHandler的实现,主要的流程就是,在接收到扫描任务的时候,对消息进行解析,然后调用scanFile
或者scan
方法来执行扫描任务.最后调用stopSelf()
来结束任务.
我们就看看scanFile
方法和scan
方法,看看其内部有事怎么样子的:
private void scan(String[] directories, String volumeName) {
Uri uri = Uri.parse("file://" + directories[0]);
// don't sleep while scanning
mWakeLock.acquire();
...
try (MediaScanner scanner = new MediaScanner(this, volumeName)) {
scanner.scanDirectories(directories);
}
...
}
...
private Uri scanFile(String path, String mimeType) {
String volumeName = MediaProvider.EXTERNAL_VOLUME;
try (MediaScanner scanner = new MediaScanner(this, volumeName)) {
// make sure the file path is in canonical form
String canonicalPath = new File(path).getCanonicalPath();
return scanner.scanSingleFile(canonicalPath, mimeType);
...
}
我们可以发现,不管是scan还是scanFile方法,最终都会创建MediaScanner
实例,然后调用MediaScanner的成员方法,也就是说扫描分为两种类型:
scanDirectories
【扫描目录】scanSingleFile
【扫描单个文件】。经过以上对MediaScannerService简单的梳理,我们会发现,MediaScannerService主要还是一个承前启后的作用,
接下来,我们就要去看看MediaScanner是怎么样子的:
在MediaScanner的类文件中,注解中说明了MediaScanner代码调用原理;简单地总结就是:
MediaScannerService->创建MediaScanner对象,
->(java)MeidaScanner::scanDirectory
->(java)MediaScanner::processDirectory(native)
->(native)MediaScanner::processDirectory
->(native)MyMediaScannerClient::scanFile
->(java)MediaScannerClient::scanFile
->(java)MediaScannerClient::doScanFile
->(java)MediaScanner::processFile(native)
->(native)MediaScanner::processFile
->(native)MyMediaScanner::handleStringTag
一旦MediaScanner processFile返回,就会在database中插入一个entry
public void scanDirectories(String[] directories) {
long start = System.currentTimeMillis();
prescan(null, true);
long prescan = System.currentTimeMillis();
if (ENABLE_BULK_INSERTS) {
// create MediaInserter for bulk inserts
mMediaInserter = new MediaInserter(mMediaProvider, 500);
}
for (int i = 0; i < directories.length; i++) {
processDirectory(directories[i], mClient);
}
...
postscan(directories);
}
这里主要分为三大部:
①: 扫描预处理
prescan(null, true);
②: 真是处理扫描任务
for (int i = 0; i < directories.length; i++) {
processDirectory(directories[i], mClient);
}
③: 扫面后续收尾工作
postscan(directories);
如上所示:在MediaScanner的scanDirectories()方法中主要做了三件事:
prescan()方法主要作用就是在扫描之前将数据库中和文件相关的信息取出来并保存起来.这些信息主要是媒体文件的路径,所属表的Uri,
processDirectory()是一个native函数,调用它来对目标文件夹进行扫描;既然是native函数,那就需要进入到native层一探究竟了:
jni: frameworks/base/media/jni/android_media_MediaScanner.cpp
static void
android_media_MediaScanner_processDirectory(
JNIEnv *env, jobject thiz, jstring path, jobject client)
{
ALOGV("processDirectory");
// 获取native层的MeidaScanner对象
MediaScanner *mp = getNativeScanner_l(env, thiz);
...
// 通过Java层传入的MyMediaScannerClient来获取native层的MyMediaScannerClient对象
MyMediaScannerClient myClient(env, client);
// 调用native层的processDirectory()方法,传入①文件路径,②之前获取到的native层的MyMediaScannerClient对象
MediaScanResult result = mp->processDirectory(pathStr, myClient);
if (result == MEDIA_SCAN_RESULT_ERROR) {
ALOGE("An error occurred while scanning directory '%s'.", pathStr);
}
env->ReleaseStringUTFChars(path, pathStr);
}
我们看到,在jni层对应android_media_MediaScanner_processDirectory
的Java层的processDirectory
方法,在android_media_MediaScanner_processDirectory()函数中,主要做一下事情:
native:/frameworks/av/media/libmedia/MediaScanner.cpp
MediaScanResult MediaScanner::processDirectory(
const char *path, MediaScannerClient &client) {
...
client.setLocale(locale());
MediaScanResult result = doProcessDirectory(pathBuffer, pathRemaining, client, false);
free(pathBuffer);
return result;
}
进入到native层的MediaScanner中,在成员函数processDirectory()函数中主要就是调用doProcessDirectory() 来实现,继续去doProcessDirectory()去看看实现:
MediaScanResult MediaScanner::doProcessDirectory(
char *path, int pathRemaining, MediaScannerClient &client, bool noMedia) {
// place to copy file or directory name
char* fileSpot = path + strlen(path);
struct dirent* entry;
// 检查当前传入的路径是不是需要跳过不扫描
if (shouldSkipDirectory(path)) {
ALOGD("Skipping: %s", path);
return MEDIA_SCAN_RESULT_OK;
}
...
DIR* dir = opendir(path);
if (!dir) {
ALOGW("Error opening directory '%s', skipping: %s.", path, strerror(errno));
return MEDIA_SCAN_RESULT_SKIPPED;
}
MediaScanResult result = MEDIA_SCAN_RESULT_OK;
while ((entry = readdir(dir))) {
if (doProcessDirectoryEntry(path, pathRemaining, client, noMedia, entry, fileSpot)
== MEDIA_SCAN_RESULT_ERROR) {
result = MEDIA_SCAN_RESULT_ERROR;
break;
}
}
closedir(dir);
return result;
}
上面的函数调用逻辑也很简单,在doProcessDirectory()函数中主要是调用doProcessDirectoryEntry()函数
MediaScanResult MediaScanner::doProcessDirectoryEntry(
char *path, int pathRemaining, MediaScannerClient &client, bool noMedia,
struct dirent* entry, char* fileSpot) {
...
// 如果当前是目录
if (type == DT_DIR) {
...
// report the directory to the client
if (stat(path, &statbuf) == 0) {
status_t status = client.scanFile(path, statbuf.st_mtime, 0,
true /*isDirectory*/, childNoMedia);
...
}
// and now process its contents
strcat(fileSpot, "/");
MediaScanResult result = doProcessDirectory(path, pathRemaining - nameLength - 1,
client, childNoMedia);
...
}
//如果是文件
} else if (type == DT_REG) {
stat(path, &statbuf);
status_t status = client.scanFile(path, statbuf.st_mtime, statbuf.st_size,
false /*isDirectory*/, noMedia);
...
}
return MEDIA_SCAN_RESULT_OK;
}
这里,首先从传进来的是目录说起【因为对目录的处理最后会得到文件,是包含对文件的处理的这段过程的
】;如代码所示.在doProcessDirectoryEntry()函数中检查出当前传入的是目录,就会调用native
层的MediaScanerClient的scanFile()函数,
我们这里就先去MediaScannClient中去看看:
/frameworks/base/media/jni/android_media_MediaScanner.cpp
virtual status_t scanFile(const char* path, long long lastModified,
long long fileSize, bool isDirectory, bool noMedia)
{
...
//调用java层的scanFile()方法
mEnv->CallVoidMethod(mClient, mScanFileMethodID, pathStr, lastModified,
fileSize, isDirectory, noMedia);
mEnv->DeleteLocalRef(pathStr);
return checkAndClearExceptionFromCallback(mEnv, "scanFile");
}
呃呃呃,最后又调用到了java层的scanFile()方法
,我们害得回到Java层去一探究竟:
/frameworks/base/media/java/android/media/MediaScanner.java
// 在Java层的scanFile()方法中也直接了当的说了是会被native层调用的.然后在Java层的scanFile()方法内部并没有其他什么附加操作,而是调用了doScanFile()
// 可以猜得到,大头还是在doScanFile()方法中:
@Override
public void scanFile(String path, long lastModified, long fileSize,
boolean isDirectory, boolean noMedia) {
// This is the callback funtion from native codes.
// Log.v(TAG, "scanFile: "+path);
doScanFile(path, null, lastModified, fileSize, isDirectory, false, noMedia);
}
public Uri doScanFile(String path, String mimeType, long lastModified,
long fileSize, boolean isDirectory, boolean scanAlways, boolean noMedia) {
Uri result = null;
...
FileEntry entry = beginFile(path, mimeType, lastModified,
fileSize, isDirectory, noMedia);
...
// we only extract metadata for audio and video files
if (isaudio || isvideo) {
mScanSuccess = processFile(path, mimeType, this);
}
if (isimage) {
mScanSuccess = processImageFile(path);
}
...
result = endFile(entry, ringtones, notifications, alarms, music, podcasts);
return result;
}
在都ScanFile()方法中,代码比较多,我们还是只针对其主要的逻辑代码进行分析,看看他做了什么事情:
FileEntry
这个是一个内部class。封装了文件路径,record ID,格式,修改时间等等属性。用来辅助判断文件是否被修改过。如果是audio/video/image则需要解析metadata属性。]native的方法
这里我们就顺着扫描的思路,从processFile()方法继续看下去[前面我们就是从Java层的processDirectory()走下去的,看来底层扫面的对象主要还是针对文件,因为如果是目录,还是会返回到Java层进行一些封装处理,然后在回到底层]:
JNI:frameworks/base/media/jni/android_media_MediaScanner.cpp
static jboolean android_media_MediaScanner_processFile(
JNIEnv *env, jobject thiz, jstring path,
jstring mimeType, jobject client)
{
ALOGV("processFile");
// Lock already hold by processDirectory
MediaScanner *mp = getNativeScanner_l(env, thiz);
...
MyMediaScannerClient myClient(env, client);
MediaScanResult result = mp->processFile(pathStr, mimeTypeStr, myClient);
...
return result != MEDIA_SCAN_RESULT_ERROR;
}
类似于前面的android_media_MediaScanner_processDirectory
函数,android_media_MediaScanner_processFile首先也是获取native层的MeidaScanner对象和native层的MyMediaScannerClient对象,调用native层的processFile
(还是熟悉的套路)
那么我们这就去看看这个(native)processFile()函数的实现:
frameworks/av/media/libstagefright/StagefrightMediaScanner.cpp
MediaScanResult StagefrightMediaScanner::processFile(
const char *path, const char *mimeType,
MediaScannerClient &client) {
...
client.beginFile();
MediaScanResult result = processFileInternal(path, mimeType, client);
...
client.endFile();
return result;
}
/frameworks/base/media/jni/android_media_MediaScanner.cpp
//----------这是StagefrightMediaScanner的初始化的地方-
static void
android_media_MediaScanner_native_setup(JNIEnv *env, jobject thiz)
{
ALOGV("native_setup");
MediaScanner *mp = new StagefrightMediaScanner;
...
env->SetLongField(thiz, fields.context, (jlong)mp);
}
这里我们其实是调用StagefrightMediaScanner的processFile()函数,这个StagefrightMediaScanner是继承native层的MediaScanner
,他的初始化是在Java层调用了native_setup()
后初始化的;到此我们也就明白了调用StagefrightMediaScanner的processFile()函数了;我们根据代码看到,在(native)processFile()中主要是:
native层的endFile,endFile是空实现,所以我们只需要关注processFileInternal
:frameworks/av/media/libstagefright/StagefrightMediaScanner.cpp
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;
...
//将文件转装载到mRetriever中
status = mRetriever->setDataSource(fd, 0, 0x7ffffffffffffffL);
...
const char *value;
if ((value = mRetriever->extractMetadata(
METADATA_KEY_MIMETYPE)) != NULL) {
// MyMediaScannerClient设置文件MIME类型信息
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 },setDataSource
{ "date", METADATA_KEY_DATE },
{ "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;
if ((value = mRetriever->extractMetadata(kKeyMap[i].key)) != NULL) {
status = client.addStringTag(kKeyMap[i].tag, value);
if (status != OK) {
return MEDIA_SCAN_RESULT_ERROR;
}
}
}
...
}
processFileInternal()函数最后在对当前文件经过层层筛选后,就会对文件进行解析:
首先获取一个MediaMetadataRetriever对象mRetriever[这个MediaMetadataRetriever类是什么呢?这个类提供了用于从输入媒体文件检索帧和元数据的统一接口.由android提供,可以获取本地和网络media相关文件的信息
];然后打开文件,将文件装载到mRetriever中,然后MyMediaScannerClient设置MIME类型,添加标签等动作;到这里,扫描的底层工作就完成的差不多了;
然后在Java层的processFile()方法中返回;调用endFile()
将扫描的结果插入到MediaProvider中,
最后返回到MediaScanner中的scanDirectories()方法中,最后调用postScan()中,进行扫描的收尾工作;
private void postscan(final String[] directories) throws RemoteException {
// handle playlists last, after we know what media files are on the storage.
if (mProcessPlaylists) {
processPlayLists();
}
// allow GC to clean up
mPlayLists.clear();
}
postscan()的作用是把不存在的文件信息从数据库中删除,而使得数据库得到更新.
MediaScanner执行流程:
begin:
(java)MediaScanner::scanDirectories
-|->(java)MediaScanner::(jni)processDirectory
--|->(jni)android_media_MediaScanner::android_media_MediaScanner_processDirectory
---|->(native)MediaScanner::processDirectory
----|->(native)MediaScanner::doProcessDirectory
-----|->(native)MediaScanner::doProcessDirectoryEntry
------|->(native)MyMediaScannerClient::scanFile
-------|->(java)MyMediaScannerClient::scanFile
--------|->(java)MyMediaScannerClient::doScanFile
---------|->(java)MediaScanner::(jni)processFile
----------|->(jni)android_media_MediaScanner::android_media_MediaScanner_processFile
-----------|->(native)MediaScanner::processFile
-----------|->(native)StagefrightMediaScanner::processFile
------------|->(native)StagefrightMediaScanner::processFileInternal
-------------|->(native)MyMediaScannerClient::setMimeType
-------------|...
-------------|->(native)MyMediaScannerClient::addStringTag
--------------|->(java)MediaScanner::endFile
---------------|->(java)mMediaProvider.update
----------------|->(java)MediaScanner::postscan
end