多媒体文件的扫描MediaScanner
主要由两部分组成,一是MediaScannerReceiver,一是MediaScannerService,扫描的执行由广播触发。MediaScannerReceiver接收4中类型的广播:
AndroidManifest.xml
看下执行扫描的具体代码:
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
}
//刷新剩余的插入记录。
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);
}