MediaScanner分析

来源:互联网 发布:日本黑帮电影 知乎 编辑:程序博客网 时间:2024/05/20 20:44

MediaScanner分析

一 MediaScannerService

多媒体扫描是从MediaScannerService开始的。这是一个单独的package。位于

packages/providers/MediaProvider:含以下java文件

l MediaProvider.java

l MediaScannerReceiver.java

l MediaScannerService.java

l MediaThumbRequest.java

分析这个目录的Android.mk文件,发现它运行的进程名字就是android.process.media

applicationandroid:process=android.process.media

1.1 MediaScannerReceiver

这个类从BroadcastReceiver中派生,用来接收任务的。

MediaScannerReceiver extends BroadcastReceiver

在它重载的onRecieve函数内有以下几种走向:

if (action.equals(Intent.ACTION_BOOT_COMPLETED)) {

// 收到”启动完毕“广播后,扫描内部存储

scan(context, MediaProvider.INTERNAL_VOLUME);

} else {

……….

if (action.equals(Intent.ACTION_MEDIA_MOUNTED) &&

externalStoragePath.equals(path)) {

/收到MOUNT信息后,扫描外部存储

scan(context, MediaProvider.EXTERNAL_VOLUME);

}

else if (action.equals(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE)&&

path != null &&path.startsWith(externalStoragePath + "/")) {

//收到请求要求扫描某个文件,注意不会扫描内部存储上的文件

scanFile(context, path);

…………………………..

}

……下面是它调用的scan函数:

scan(Context context, String volume)

Bundle args = new Bundle();

args.putString("volume", volume);

//直接启动MediaScannerService了,

context.startService(

new Intent(context,MediaScannerService.class).putExtras(args));

总结:

MediaScannerReceiver是用来接收任务的,它收到广播后,会启动MediaService进行扫描工作。

下面看看MediaScannerService.

1.2 MediaScannerService

MSS标准的从Service中派生下来,

MediaScannerService extends Service implements Runnable

//注意:是一个Runnable…,可能有线程之类的东西存在

下面从Service的生命周期的角度来看看它的工作。

1.onCreate

public void onCreate()

PowerManager pm =(PowerManager)getSystemService(Context.POWER_SERVICE);

mWakeLock =pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);

//获得电源锁,防止在扫描过程中休眠

//单独搞一个线程去跑扫描工作,防止ANR

Thread thr = new Thread(null, this,"MediaScannerService");

thr.start();

2.onStartCommand

@Override

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

{

//注意这个handler,是在另外一个线程中创建的,往这个handler里sendMessage

//都会在那个线程里边处理

//不明白的可以去查看handler和Looper机制

//这里就是同步机制,等待mServiceHandler在另外那个线程创建完毕

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();

//把MediaScannerReceiver发出的消息传递到另外那个线程去处理。

mServiceHandler.sendMessage(msg);

………….

基本上MSR(MediaScannerReceiver)发出的请求都会传到onStartCommand中处理。如果有多个存储的话,也只能一个一个扫描了。

下面看看那个线程的主函数

3. run

public void run()

{

// reduce priority below other background threads toavoid interfering

// with other services at boot time.

Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND+

Process.THREAD_PRIORITY_LESS_FAVORABLE);

//不明白的去看看Looper和handler的实现

Looper.prepare();//把这个looper对象设置到线程本地存储

mServiceLooper = Looper.myLooper();

mServiceHandler = new ServiceHandler();//创建handler,默认会把这个looper

//的消息队列赋值给handler的消息队列,这样往handler中发送消息就是往这个线程的looper发

Looper.loop();//消息循环,内部会处理消息队列中的消息

//也就是handleMessage函数

}

上面handler中加入了一个扫描请求(假设是外部存储的),所以要分析handleMessage函数。

4.handleMessage

private final class ServiceHandler extends Handler

{

@Override

public void handleMessage(Message msg)

{

Bundle arguments = (Bundle) msg.obj;

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

try {

……… 这里不讲了

} else {

String volume = arguments.getString("volume");

String[] directories = null;

if (MediaProvider.INTERNAL_VOLUME.equals(volume)) {

//是扫描内部存储的请求?

// scan internal media storage

directories = new String[] {

Environment.getRootDirectory() + "/media",

};

}

else if (MediaProvider.EXTERNAL_VOLUME.equals(volume)) {

//是扫描外部存储的请求?获取外部存储的路径

directories = new String[] {

Environment.getExternalStorageDirectory().getPath(),

};

}

if (directories != null) {

//真正的扫描开始了,上面只不过是把存储路径取出来罢了.

scan(directories, volume);

…..

//扫描完了,就把service停止了

stopSelf(msg.arg1);

}

};

5.scan函数

private void scan(String[] directories, StringvolumeName) {

mWakeLock.acquire();

//下面这三句话很深奥…

//从 getContentResolver获得一个ContentResover,然后直接插入

//根据AIDL,这个ContentResover的另一端是MediaProvider。只要去看看它的

//insert函数就可以了

//反正这里知道获得了一个扫描URI即可。

ContentValues values = new ContentValues();

values.put(MediaStore.MEDIA_SCANNER_VOLUME, volumeName);

Uri scanUri =getContentResolver().insert(MediaStore.getMediaScannerUri(), values);

Uri uri = Uri.parse("file://" +directories[0]);

//发送广播,通知扫描开始了

sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_STARTED,uri));

try {

if (volumeName.equals(MediaProvider.EXTERNAL_VOLUME)) {

openDatabase(volumeName);

}

//创建真正的扫描器

MediaScanner scanner = createMediaScanner();

//交给扫描器去扫描文件夹 scanDirectories

scanner.scanDirectories(directories, volumeName);

} catch (Exception e) {

Log.e(TAG, "exception in MediaScanner.scan()",e);

}

//删除扫描路径

getContentResolver().delete(scanUri, null, null);

//通知扫描完毕

sendBroadcast(newIntent(Intent.ACTION_MEDIA_SCANNER_FINISHED, uri));

mWakeLock.release();

}

说说上面那个深奥的地方,在MediaProvider中重载了insert函数,insert函数会调用insertInternal函数。

如下:

private Uri insertInternal(Uri uri, ContentValuesinitialValues) {

long rowId;

int match = URI_MATCHER.match(uri);

// handle MEDIA_SCANNER before callinggetDatabaseForUri()

//刚才那个insert只会走下面这个分支,其实就是获得一个地址….

//太绕了!!!!!

if (match == MEDIA_SCANNER) {

mMediaScannerVolume =initialValues.getAsString(MediaStore.MEDIA_SCANNER_VOLUME);

return MediaStore.getMediaScannerUri();

}

……..

再看看它创建了什么样的Scanner,这就是MSS中的createMediaScanner

private MediaScanner createMediaScanner() {

//下面这个MediaScanner在framework/base/中,待会再分析

MediaScanner scanner = new MediaScanner(this);

//设置当前的区域,这个和字符编码有重大关系。

Locale locale = getResources().getConfiguration().locale;

if (locale != null) {

String language = locale.getLanguage();

String country = locale.getCountry();

String localeString = null;

if (language != null) {

if (country != null) {

//给扫描器设置当前国家和语言。

scanner.setLocale(language + "_" + country);

} else {

scanner.setLocale(language);

}

}

}

return scanner;

}

至此,MSS的任务完成了。接下来是MediaScanner的工作了。

6. 总结

MSS的工作流程如下:

l 1 单独启动一个带消息循环的工作线程。

l 2 主线程接收系统发来的任务,然后发送给工作线程去处理。

l 3 工作线程接收任务,创建一个MediaScanner去扫描。

l 4 MSS顺带广播一下扫描工作启动了,扫描工作完毕了。

二 MediaScanner

MediaScanner位置在

frameworks/base/media/下,包括jni和java文件。

先看看java实现。

这个类巨复杂,而且和MediaProvider交互频繁。在分析的时候要时刻回到MediaProvider去看看。

1. 初始化

public class MediaScanner

{

static {

//libmedia_jni.so的加载是在MediaScanner类中完成的

//这么重要的so为何放在如此不起眼的地方加载???

System.loadLibrary("media_jni");

native_init();

}

public MediaScanner(Context c) {

native_setup();//调用jni层的初始化,暂时不用看了,无非就是一些

//初始化工作,待会在再进去看看

……..

}

刚才MSS中是调用scanDirectories函数,我们看看这个。

2. scanDirectories

public void scanDirectories(String[] directories, StringvolumeName) {

try {

long start = System.currentTimeMillis();

initialize(volumeName);//初始化

prescan(null);//扫描前的预处理

long prescan = System.currentTimeMillis();

for (int i = 0; i < directories.length; i++) {

//扫描文件夹,这里有一个很重要的参数 mClient

// processDirectory是一个native函数

processDirectory(directories[i],MediaFile.sFileExtensions, mClient);

}

long scan = System.currentTimeMillis();

postscan(directories);//扫描后处理

long end = System.currentTimeMillis();

…..打印时间,异常处理…没了…

下面简单讲讲initialize ,preScan和postScan都干嘛了。

private void initialize(String volumeName) {

//打开MediaProvider,获得它的一个实例

mMediaProvider =mContext.getContentResolver().acquireProvider("media");

//得到一些uri

mAudioUri = Audio.Media.getContentUri(volumeName);

mVideoUri = Video.Media.getContentUri(volumeName);

mImagesUri = Images.Media.getContentUri(volumeName);

mThumbsUri = Images.Thumbnails.getContentUri(volumeName);

//外部存储的话,可以支持播放列表之类的东西,搞了一些个缓存池之类的

//如mGenreCache等

if (!volumeName.equals("internal")) {

// we only support playlists on external media

mProcessPlaylists = true;

mGenreCache = new HashMap<String, Uri>();

preScan,这个函数很复杂:

大概就是创建一个FileCache,用来缓存扫描文件的一些信息,例如last_modified等。这个FileCache是从MediaProvider中已有信息构建出来的,也就是历史信息。后面根据扫描得到的新信息来对应更新历史信息。

postScan,这个函数做一些清除工作,例如以前有video生成了一些缩略图,现在video文件被干掉了,则对应的缩略图也要被干掉。

另外还有一个mClient,这个是从MediaScannerClient派生下来的一个东西,里边保存了一个文件的一些信息。后续再分析。

刚才说到,具体扫描工作是在processDirectory函数中完成的。这个是一个native函数。

在frameworks/base/media/jni/android_media_MediaScanner.cpp中。

三 MediaScanner JNI层分析

MediaScanner JNI层内容比较多,单独搞一节分析吧。

先看看android_media_MediaScanner这个文件。

1.native_init函数,jni对应的函数如下

static void

android_media_MediaScanner_native_init(JNIEnv *env)

{

jclass clazz;

clazz = env->FindClass("android/media/MediaScanner");

//得都JAVA类中mNativeContext这个成员id

fields.context = env->GetFieldID(clazz,"mNativeContext", "I");

//不熟悉JNI的自己去学习下吧

}

3. native_setup函数,jni对应函数如下:

android_media_MediaScanner_native_setup(JNIEnv *env,jobject thiz)

{

//创建MediaScanner对象

MediaScanner *mp = createMediaScanner();

//太变态了,自己不保存这个对象指针.

//却把它设置到java对象的mNativeContext去保存

env->SetIntField(thiz, fields.context, (int)mp);

}

//创建MediaScanner函数

static MediaScanner *createMediaScanner() {

#if BUILD_WITH_FULL_STAGEFRIGHT

..

//使用google自己的

return new StagefrightMediaScanner;

#endif

#ifndef NO_OPENCORE

//使用opencore提供的

….

return new PVMediaScanner();

#endif

4. processDirectories函数,jni对应如下:

android_media_MediaScanner_processDirectory(JNIEnv *env,jobject thiz, jstring path, jstring extensions, jobject client)

{

MediaScanner *mp = (MediaScanner*)env->GetIntField(thiz, fields.context);

//每次都要回调到JAVA中去取这个Scanner!!

………

const char *pathStr = env->GetStringUTFChars(path,NULL);

const char *extensionsStr =env->GetStringUTFChars(extensions, NULL);

…….

//又在C++这里搞一个client,然后把java的client放到C++Client中去保存

//而且还是栈上的临时变量..

MyMediaScannerClient myClient(env, client);

//scanner扫描文件夹,用得是C++的client

mp->processDirectory(pathStr, extensionsStr, myClient,ExceptionCheck, env);

env->ReleaseStringUTFChars(path, pathStr);

env->ReleaseStringUTFChars(extensions, extensionsStr);

}

到这里似乎就没有了,那么扫描后的数据库是怎么更新的呢?为什么要传入一个client进去呢?看来必须得trace到scanner中去才知道了。

四 PVMediaScanner

这个在external/opencore/android/mediascanner.cpp中。

1. processDirectory

status_t MediaScanner::processDirectory(const char *path,const char* extensions,

MediaScannerClient& client, ExceptionCheckexceptionCheck, void* exceptionEnv)

{

InitializeForThread();

int error = 0;

status_t result = PVMFSuccess;

….

//调用client的设置区域函数

client.setLocale(mLocale);

//扫描文件夹,咋还没开始??

result = doProcessDirectory(pathBuffer, pathRemaining,extensions, client, exceptionCheck, exceptionEnv);

..

2. doProcessDirectory

status_t MediaScanner::doProcessDirectory(char *path, intpathRemaining, const char* extensions,

MediaScannerClient& client, ExceptionCheckexceptionCheck, void* exceptionEnv)

{

…终于看到点希望了

//打开这个文件夹,枚举其中的内容。

//题外话,这个时候FileManager肯定删不掉这个文件夹!!

DIR* dir = opendir(path);

while ((entry = readdir(dir))) {

const char* name = entry->d_name;

//不处理.和..文件夹

if (isDirectory) {

……..

//不处理.开头的文件夹。如果是文件夹,递归调用doProcessDirectory

//深度优先啊!

int err = doProcessDirectory(path, pathRemaining -nameLength - 1, extensions, client, exceptionCheck, exceptionEnv);

if (err) {

LOGE("Error processing '%s' - skipping/n",path);

continue;

}

} else if (fileMatchesExtension(path, extensions)) {

//是一个可以处理的文件,交给client处理

//彻底疯掉了….这是干嘛呢???

client.scanFile(path, statbuf.st_mtime, statbuf.st_size);

这里要解释下,刚才createMediaScanner中,明明创建的是PVMediaScanner,为何这里看得是MediaScanner代码呢?

l 因为PVMediaScanner从MediaScanner中派生下来的,而且没有重载processDirectory函数

l Eclaire没有PVMediaScanner这样的东西,估计是froyo又改了点啥吧。

FT,processDirctory无非是列举一个目录内容,然后又反回去调用client的scanFile处理了。为何搞这么复杂?只有回去看看C++的client干什么了。

3. MediaScannerClient---JNI层

JNI中的这个类是这样的:

class MyMediaScannerClient : public MediaScannerClient

//这是它的scanFile实现

virtual bool scanFile(const char* path, long longlastModified, long long fileSize)

{

//再次崩溃了,C++的client调用了刚才传进去的java Client的

//scanFile函数…不过这次还传进去了该文件的路径,最后修改时间和文件大小。

mEnv->CallVoidMethod(mClient, mScanFileMethodID,pathStr, lastModified, fileSize);

…..想死,,,

}

没办法了,只能再去看看MediaScanner.java传进去的那个client了。

4. MediaScannerClient----JAVA层

这个类在MediaScanner.java中实现。

private class MyMediaScannerClient implementsMediaScannerClient:

public void scanFile(String path, long lastModified, longfileSize) {

//调用doScanFile..很烦..

doScanFile(path, null, lastModified, fileSize, false);

//下面是doScanFile

public Uri doScanFile(String path, String mimeType, longlastModified, long fileSize, boolean scanAlways) {

//预处理,看看之前创建的文件缓存中有没有这个文件

FileCacheEntry entry = beginFile(path, mimeType,lastModified, fileSize);

// rescan for metadata if file was modified since lastscan

if (entry != null && (entry.mLastModifiedChanged|| scanAlways)) {

//如果事先有这个文件的信息,则需要修改一些信息,如长度,最后修改时间等

…..

//真正的扫描文件

processFile(path, mimeType, this);

//扫描完了,做最后处理

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

processFile又是jni层的。

对应android_media_MediaScanner_processFile函数

android_media_MediaScanner_processFile(JNIEnv *env,jobject thiz, jstring path, jstring mimeType, jobject client)

{

MediaScanner *mp = (MediaScanner*)env->GetIntField(thiz, fields.context);

//无语了,又搞一个 MyMediaScannerClient

MyMediaScannerClient myClient(env, client);

mp->processFile(pathStr, mimeTypeStr, myClient);

…}

第一次到PVMediaScanner中来了

status_t PVMediaScanner::processFile(const char *path,const char* mimeType, MediaScannerClient& client)

{

status_t result;

InitializeForThread();

//调用client的beginFile,估计是做一些啥预处理

client.setLocale(locale());

client.beginFile();

//LOGD("processFile %s mimeType: %s/n", path, mimeType);

const char* extension = strrchr(path, '.');

if (extension && strcasecmp(extension,".mp3") == 0) {

result = parseMP3(path, client);//client又传进去了

…根据后缀名去扫描….

}

client.endFile();

到parseMP3去看看。这个在

static PVMFStatus parseMP3(const char *filename, MediaScannerClient&client)

{//这个函数太专业了,和编解码有关,我们重点关注client在这里干什么了

//原来,解析器从文件中解析出一个信息,就调用client的addStringTag

if (!client.addStringTag("duration", buffer))

….

addStringTag在JNI的client中处理。

这个MediaScannerClient是在opencore中的那个MediaScanner.cpp中实现的,而android_media_MediaScanner.cpp中的是MyMediaScannerClient,从MediaScannerClient派生下来的

bool MediaScannerClient::addStringTag(const char* name,const char* value)

{

if (mLocaleEncoding != kEncodingNone) {

//字符串编码之类的转换。不详述了

bool nonAscii = false;

const char* chp = value;

char ch;

while ((ch = *chp++)) {

if (ch & 0x80) {

nonAscii = true;

break;

}

}

//如果不是ASCII编码的话,内部先保存一下这些个tag信息

//待会扫描完后再集中做一次字符串编码转换

if (nonAscii) {

// save the strings for later so they can be used fornative encoding detection

mNames->push_back(name);

mValues->push_back(value);

return true;

}

// else fall through

}

//调用子类的handleStringTag

return handleStringTag(name, value);

}

class MyMediaScannerClient : public MediaScannerClient{

//调用到子类的handleStringTag了

virtual bool handleStringTag(const char* name, constchar* value)

{

//又传递到JAVA层的handleStringTag来处理

//麻木了..

mEnv->CallVoidMethod(mClient,mHandleStringTagMethodID, nameStr, valueStr);

}

JAVA层

MediaScannerService中的MyMediaScannerClient类

public void handleStringTag(String name, String value) {

//下层扫描的文件tag信息,全部处理后赋值给java层这个MyScannerClient了

例如MP3的title,专辑名等等。

….

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();

到这里了,还没写到数据库呢?啥时候更新数据库?看来是在client.endFile()中了。

但是这个endClient并没有调用到JAVA层去。那在哪里结束呢?

还记得JAVA中的doScanFile函数吗,对了,这个endFile就是在那里直接由JAVA调用的。

private Uri endFile(FileCacheEntry entry, booleanringtones, boolean notifications,

boolean alarms, boolean music, boolean podcasts)

throws RemoteException {

// update database

Uri tableUri;

boolean isAudio = MediaFile.isAudioFileType(mFileType);

boolean isVideo = MediaFile.isVideoFileType(mFileType);

boolean isImage = MediaFile.isImageFileType(mFileType);

….

//来了一个新文件,直接插入数据库

result = mMediaProvider.insert(tableUri, values);

//或者更新数据库

mMediaProvider.update(result, values, null, null);

这回真算是完了。

5.流程总结

l MediaScanner(MS)调用scanDirectories中的processDirectory,进入到JNI层

l JNI调用PVMediaScanner的processDirectory

l PVMediaScanner的processDirectory为目录下的文件调用MyMediaScannerClient的scanFile

l MyMediaScannerClien

 

原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 原子贷逾期半年怎么办 51闪电购逾期怎么办 被转店中介坑钱怎么办? 转店被中介骗了怎么办? 被转店平台骗了怎么办 海信电视很暗怎么办 淘新闻账号冻结怎么办 黑号抽不中天猫购物券怎么办 退款后购物津贴怎么办 天猫优惠券过期怎么办 淘宝店复核不过怎么办 房产新人没客源怎么办 淘宝商品被监管怎么办 天猫店铺监管怎么办 普票超额了怎么办 开票金额超过限额怎么办 淘宝拉入黑名单怎么办 淘宝卖食品证件怎么办 京东额度不够怎么办 天猫盒子内存不足怎么办 10086办无限流量怎么办 天猫营业额不够怎么办 试电笔电阻坏了怎么办 父子间车辆过户怎么办 房子卖了天然气怎么办 社保这个月没扣怎么办 员工不交社保怎么办 公司没交社保怎么办 微信转账受限怎么办 支付宝违规限制怎么办 微信转账不还钱怎么办 碳放久了不爱起火怎么办 闻碳火时间久了怎么办 被代运营诈骗怎么办 被上海聚连骗了怎么办? 蓝领贷逾期半年怎么办 淘宝店倒闭售后怎么办 淘宝店铺宝贝被下架怎么办 医院被托管编制怎么办 领码分百万账号异常怎么办 拼多多帐号异常怎么办