多媒体文件管理,资源的扫描MediaScanner(二)

多媒体文件的扫描MediaScanner

主要由两部分组成,一是MediaScannerReceiver,一是MediaScannerService,扫描的执行由广播触发。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>

看下执行扫描的具体代码:

Packages/providers/mediaprovider/…/MediaScannerReceiver.java
public class MediaScannerReceiver extends BroadcastReceiver {
	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 (uri.getScheme().equals("file")) {
//获取外部存储设备的路径。
		String path = uri.getPath();
		String externalStoragePath = 
Environment.getExternalStorageDirectory().getPath();
			String legacyPath = Environment.getLegacyExternalStorageDirectory().getPath();
			path = new File(path).getCanonicalPath();
			if (path.startsWith(legacyPath)) {
				path = externalStoragePath + path.substring(legacyPath.length());
			}
}
//外部存储设备挂载的广播,
	if (Intent.ACTION_MEDIA_MOUNTED.equals(action)) {
		scan(context, MediaProvider.EXTERNAL_VOLUME);
}else if(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE.equals(action) &&
path != null && path.startsWith(externalStoragePath + "/")) {
//扫描特定的文件路径。
	scanFile(context, path);
}
}
}

不管是调用scan还是scanfile方法,都是调用MediaScannerService来接着处理。

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

接着看MediaScannerService的代码:

MediaScannerService.java

public class MediaScannerService extends Service implements Runnable {
//首先创建一个唤醒锁,级别是PARTIAL_WAKE_LOCK,确保cpu一直在运行,屏幕和键盘背光都可以被关闭,在扫描开始会通过mWakeLock.acquire();获取唤醒锁,这样扫描的过程中不会sleep,如果用户按了power键,屏幕会关掉,但是cpu会保持运行,知道调用mWakeLock.release();
	PowerManager pm = (PowerManager)getSystemService(Context.POWER_SERVICE);
	mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
//这里新启了一个线程,因为service默认是运行在主线程的,所以要在单独的线程处理扫描,不然会block主线程。
	Thread thr = new Thread(null, this, "MediaScannerService");
	thr.start();
}

顺带看下他的run()方法,这里展示启动一个独立线程的示例:

public void run() {
	Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND +
		Process.THREAD_PRIORITY_LESS_FAVORABLE);
//prepare()这个方法是一定要调用的,因为这个方法会去创建属于这个新线程的Looper,同时创建属于这个线程的消息队列,也即是Looper实例中的MessageQueue变量。
	Looper.prepare();
//获取属于这个新线程的Looper,也即是上一步prepare中创建的。
	mServiceLooper = Looper.myLooper();
	mServiceHandler = new ServiceHandler();
//调用loop()方法,让线程循环起来。
	Looper.loop();
}

我们知道service的onCreate方法之后,会调用其onStartCommand,在onStartCommand方法中会通过mServiceHandler发送消息,同时携带Intent中的扫描信息,直接看其handleMessage方法实现:

private final class ServiceHandler extends Handler {
	public void handleMessage(Message msg) {
		Bundle arguments = (Bundle) msg.obj;
		String filePath = arguments.getString("filepath");
		if (filePath != null) {
//这里处理扫描指定文件路径的请求,ACTION_MEDIA_SCANNER_SCAN_FILE。
			IBinder binder = arguments.getIBinder("listener");
			IMediaScannerListener listener =
				(binder == null ? null : IMediaScannerListener.Stub.asInterface(binder));
			uri = scanFile(filePath, arguments.getString("mimetype"));
			listener.scanCompleted(filePath, uri);
}else {
//下面是扫描存储设备,
	String volume = arguments.getString("volume");
	String[] directories = null;
	if (MediaProvider.INTERNAL_VOLUME.equals(volume)) {
//这里是扫描内部存储设备,只考虑了media目录,
		directories = new String[] {
			Environment.getRootDirectory() + "/media",
			Environment.getOemDirectory() + "/media",};
	} else if (MediaProvider.EXTERNAL_VOLUME.equals(volume)) {
//这里扫描外部存储设备,考虑了是不是多用户。
		if (getSystemService(UserManager.class).isDemoUser()) {
			directories = ArrayUtils.appendElement(String.class,
			mExternalStoragePaths,
			Environment.getDataPreloadsMediaDirectory().getAbsolutePath());
}else{
	directories = mExternalStoragePaths;
}
scan(directories, volume);
}
}
}
}

接着看下他的scan()函数:

private void scan(String[] directories, String volumeName) {
	if (volumeName.equals(MediaProvider.EXTERNAL_VOLUME)) {
		openDatabase(volumeName);
	}
//通过MediaScanner对象,进一步执行扫描操作,携带的参数是要扫描的目录。
	try (MediaScanner scanner = new MediaScanner(this, volumeName)) {
		scanner.scanDirectories(directories);
}
}

转到frameworks/base/media/java/…/MediaScanner.java。

private final MyMediaScannerClient mClient= new MyMediaScannerClient();

mClient对象,会作为processDirectory的参数传到native层,后续会在native层调用java层方法扫描单个文件目录,并在扫描结束时做出入数据库的处理。也就是说目录数组的扫描工作,最终还是拆分为单个文件目录,做扫描,插入数据库。
public void scanDirectories(String[] directories) @ MediaScanner.java {
// mMediaInserter对象处理数据批量插入数据库的操作,
	mMediaInserter = new MediaInserter(mMediaProvider, 500);
	for (int i = 0; i < directories.length; i++) {
//这是个jni方法,循环处理directories的目录。这里的mClient是MyMediaScannerClient类型的对象,它是MediaScanner.java的内部类。
	processDirectory(directories[i], mClient);
}
//刷新剩余的插入记录。
mMediaInserter.flushAll();
mMediaInserter = null;
}

还要继续看扫描的处理,转到jni层,

android_media_MediaScanner_processDirectory( )
@android_media_MediaScanner.cpp{
	MediaScanner *mp = getNativeScanner_l(env, thiz);
	MyMediaScannerClient myClient(env, client);
	MediaScanResult result = mp->processDirectory(pathStr, myClient);
}
上面的函数中要搞清楚两个问题,mp具体是什么对象?myClient怎么调用的java层的client实例的方法。
先看mp具体是什么对象?


//这个native_init方法,是有MediaScanner.java的static block调用的。

static void android_media_MediaScanner_native_init(JNIEnv *env){//显示找到java层"android/media/MediaScanner";这个类。jclass clazz = env->FindClass(kClassMediaScanner);//然后取出java层的变量mNativeContext,转成native层的变量fields.context,用来存取值使用。fields.context = env->GetFieldID(clazz, "mNativeContext", "J");}


//这个native_setup是在MediaScanner.java的构造函数中调用的。

static void android_media_MediaScanner_native_setup(JNIEnv *env, jobject thiz){
//把StagefrightMediaScanner实例存入到了fields.context中。
	MediaScanner *mp = new StagefrightMediaScanner;
	env->SetLongField(thiz, fields.context, (jlong)mp);
}

所以上面的processDirectory(…)方法就转到了StagefrightMediaScanner.cpp中,实质上调用的是其父类MediaScanner.cpp中的processDirectory方法。

MediaScanner.cpp

MediaScanResult MediaScanner::doProcessDirectory(
	char *path, int pathRemaining, MediaScannerClient &client, bool noMedia) {
//先做一些处理,是不是要跳过某个目录,找出所有非多媒体.nomedia,打开目录,跳过一些不存在的目录,调用doProcessDirectoryEntry。
	DIR* dir = opendir(path);
	while ((entry = readdir(dir))) {
		if (doProcessDirectoryEntry(path, pathRemaining, client, noMedia, entry, fileSpot)
			== MEDIA_SCAN_RESULT_ERROR){
			result = MEDIA_SCAN_RESULT_ERROR;
			break;
}
}	
closedir(dir);
}
MediaScanResult MediaScanner::doProcessDirectoryEntry(
	char *path, int pathRemaining, MediaScannerClient &client, bool noMedia,
	struct dirent* entry, char* fileSpot) {
	const char* name = entry->d_name;
//忽略“.”和“..”
	if (name[0] == '.' && (name[1] == 0 || (name[1] == '.' && name[2] == 0))) {…}
//获取文件类型。
	int type = entry->d_type;
//接下来如果是目录,继续调用doProcessDirectory,类似一个递归。如果是文件,调用client.scanFile方法。
	if (type == DT_DIR) {
		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);
}
}

还记得上面提到的client,是android_media_MediaScanner_processDirector()@android_media_MediaScanner.cpp中传过来的,实质是MyMediaScannerClient类型的对象。MyMediaScannerClient是android_media_MediaScanner.cp的子类,继承自MediaScannerClient.cpp

接着看下他的scanFile()方法:

class MyMediaScannerClient : public MediaScannerClient
virtual status_t scanFile(const char* path, long long lastModified,
	long long fileSize, bool isDirectory, bool noMedia){
//这个函数是调用java层的类MyMediaScannerClient(MediaScanner.java的内部类)中的方法scanFile方法。
	mEnv->CallVoidMethod(mClient, mScanFileMethodID, pathStr, lastModified,
		fileSize, isDirectory, noMedia);
}

以上面的代码为例,插播native层如何调用java层代码的:

mEnv指针变量的类型是JNIEnv,而JNIEnv又是JNINativeInterface结构类型的,代表了jni的本地接口,里面包含了很多的函数。

jobject类型,代表了调用native方法的,那个java类实例,当从java层调用native方法时,前两个参数(JNIEnv *env, jobject thiz)都是自动带入的,后面的才是函数的真正入参。

解释下mEnv->CallVoidMethod()中参数:

mClient是jobject类型的对象,代表了java层的类MyMediaScannerClient的类实例,具体是在MyMediaScannerClient(cpp)的构造函数中的初始化列表中赋值的:mClient(env->NewGlobalRef(client))。

mScanFileMethodID是jmethodID类型的变量,它的赋值语句:

mScanFileMethodID = env->GetMethodID(

mediaScannerClientInterface,"scanFile","(Ljava/lang/String;JJZZ)V");

在这条赋值语句中mediaScannerClientInterface是jclass 类型的java层类(MediaScannerClient.java)实例(注意不是对象实例):

static const char* constkClassMediaScannerClient ="android/media/MediaScannerClient";

jclass mediaScannerClientInterface = env->FindClass(kClassMediaScannerClient);

"scanFile"是java层类MediaScannerClient.java中方法名。

"(Ljava/lang/String;JJZZ)V"是scanFile这个方法的类型签名,括号中是参数,最后是返回值。

接着说mEnv->CallVoidMethod()这个函数中的参数,除了前两个mClient, mScanFileMethodID,后面的值是实际传给调用函数的参数值,是真正的入参。


JNI对所有的数据类型做了类型定义:

JNI基础类型对照表

Java类型

本地类型

占位说明

boolean

jboolean

Unsigned 8 bits

byte

jbyte

Signed 8 bits

char

jchar

Unsigned 16 bits

short

jshort

Signed 16 bits

int

jint

Signed 32 bits

long

jlong

Signed 64 bits

float

jfloat

32bits

double

jdouble

64bits

void

void

void

JNI引用数据类型对照表

Java类型

本地类型

Java类型

本地类型

All objects

jobject

Char[]

jcharArray

Java.lang.Class  instances

jclass

Short[]

jshortArray

Java.lang.String  instances

jstring

Int[]

jintArray

arrays

jarray

Long[]

jlongArray

Object[]

jobjectArray

Float[]

jfloatArray

Boolean []

JbooleanArray

Double[]

jdoubleArray

Byte[]

jbyteArray

Java.lang.Throwable  objects    

jthrowable

JNI中类型签名,注意boolean,long

Java type

Type Sinature

Java type

Type signature

boolean

Z

float

F

byte

B

Double

D

char

C

Full-qualified-class

(如:java/lang/String)

L full-qualified-class(全限定类

如:Ljava/lang/String)

short

S

Type[] (数组,如:int[])

[type   (如:[I)

int

I

Method type (方法类型)

(arg-types)ret-type

(参数类型)返回值类型

long

J

 

 



接着前面扫描流程分析:

scanFile又直接调用了doScanFile方法:

MediaScanner.java

public Uri doScanFile(String path, String mimeType, long lastModified, long fileSize,
	boolean isDirectory, boolean scanAlways, boolean noMedia)@ MyMediaScannerClient {
//获取文件类型,生成FileEntry对象。
	FileEntry entry = beginFile(path, mimeType, lastModified,
		fileSize, isDirectory, noMedia);
//接下来区分多媒体文件,非多媒体文件。
	if (noMedia) {
//非多媒体文件,直接调用endFile。
		result = endFile(entry, false, false, false, false, false);
}else{
//根据beginFile中获取的文件类型mFileType,判断是音频,视频,图片。
	boolean isaudio = MediaFile.isAudioFileType(mFileType);
	boolean isvideo = MediaFile.isVideoFileType(mFileType);
	boolean isimage = MediaFile.isImageFileType(mFileType);
//做路径转化,这里的模拟外部存储设备,应该是指手机内存中除PRIVATE外的部分?
	if (isaudio || isvideo || isimage) {
		path = Environment.maybeTranslateEmulatedPathToInternal(new File(path))
			.getAbsolutePath();
}
//如果是音视频文件,只提取元数据,ID3信息,比如:宽高,日期,专辑名,艺术家等。
if (isaudio || isvideo) {
	processFile(path, mimeType, this);
}
//如果是图片,执行解码。
if (isimage) {
	processImageFile(path);
}
// endFile,做的一个主要工作就是往数据库插入数据。
result = endFile(entry, ringtones, notifications, alarms, music, podcasts);
}
}

private Uri endFile(FileEntry entry, boolean ringtones, boolean notifications,
	boolean alarms, boolean music, boolean podcasts){
//生成一个存入数据库的数据对象
	ContentValues values = toValues();
//对jpg的对象,处理下其ExifInterface信息,主要角度调整。
	if((mFileType == MediaFile.FILE_TYPE_JPEG
                    || MediaFile.isRawImageFileType(mFileType)) && !mNoMedia){
		ExifInterface exif = new ExifInterface(entry.mPath);
		int orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, -1);
		if (orientation != -1) {
			int degree;
			switch(orientation) {
				case ExifInterface.ORIENTATION_ROTATE_90:
				degree = 90;
}
values.put(Images.Media.ORIENTATION, degree);
}
}
//处理数据插入的对象,
MediaInserter inserter = mMediaInserter;
//根据不同的类型,设置相应的uri,这些uri来自MediaStore.java。
if (MediaFile.isVideoFileType(mFileType)) {
	tableUri = mVideoUri;
} else if (MediaFile.isImageFileType(mFileType)) {
	tableUri = mImagesUri;
}
//有了uri,有了要插入的数据values,调用MediaInserter插入数据库。一个新的文件插入时,要先插入其目录,然后在插入文件,记得调用flushAll,或者flush清空缓冲区。
inserter.insert(tableUri, values);
}



  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值