热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

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

多媒体文件的扫描MediaScanner主要由两部分组成,一是MediaScannerReceiver,一是MediaScannerService&#

多媒体文件的扫描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 //这是个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);
}





推荐阅读
  • com.hazelcast.config.MapConfig.isStatisticsEnabled()方法的使用及代码示例 ... [详细]
  • 本文详细介绍了如何在 Linux 系统上安装 JDK 1.8、MySQL 和 Redis,并提供了相应的环境配置和验证步骤。 ... [详细]
  • 本文详细介绍了在 CentOS 7 系统中配置 fstab 文件以实现开机自动挂载 NFS 共享目录的方法,并解决了常见的配置失败问题。 ... [详细]
  • 本文介绍了在 Java 编程中遇到的一个常见错误:对象无法转换为 long 类型,并提供了详细的解决方案。 ... [详细]
  • 在多线程并发环境中,普通变量的操作往往是线程不安全的。本文通过一个简单的例子,展示了如何使用 AtomicInteger 类及其核心的 CAS 无锁算法来保证线程安全。 ... [详细]
  • 微信公众号推送模板40036问题
    返回码错误码描述说明40001invalidcredential不合法的调用凭证40002invalidgrant_type不合法的grant_type40003invalidop ... [详细]
  • 原文网址:https:www.cnblogs.comysoceanp7476379.html目录1、AOP什么?2、需求3、解决办法1:使用静态代理4 ... [详细]
  • 基于Net Core 3.0与Web API的前后端分离开发:Vue.js在前端的应用
    本文介绍了如何使用Net Core 3.0和Web API进行前后端分离开发,并重点探讨了Vue.js在前端的应用。后端采用MySQL数据库和EF Core框架进行数据操作,开发环境为Windows 10和Visual Studio 2019,MySQL服务器版本为8.0.16。文章详细描述了API项目的创建过程、启动步骤以及必要的插件安装,为开发者提供了一套完整的开发指南。 ... [详细]
  • 本文介绍了一种自定义的Android圆形进度条视图,支持在进度条上显示数字,并在圆心位置展示文字内容。通过自定义绘图和组件组合的方式实现,详细展示了自定义View的开发流程和关键技术点。示例代码和效果展示将在文章末尾提供。 ... [详细]
  • 使用 ListView 浏览安卓系统中的回收站文件 ... [详细]
  • 在Linux系统中,网络配置是至关重要的任务之一。本文详细解析了Firewalld和Netfilter机制,并探讨了iptables的应用。通过使用`ip addr show`命令来查看网卡IP地址(需要安装`iproute`包),当网卡未分配IP地址或处于关闭状态时,可以通过`ip link set`命令进行配置和激活。此外,文章还介绍了如何利用Firewalld和iptables实现网络流量控制和安全策略管理,为系统管理员提供了实用的操作指南。 ... [详细]
  • C++ 开发实战:实用技巧与经验分享
    C++ 开发实战:实用技巧与经验分享 ... [详细]
  • ButterKnife 是一款用于 Android 开发的注解库,主要用于简化视图和事件绑定。本文详细介绍了 ButterKnife 的基础用法,包括如何通过注解实现字段和方法的绑定,以及在实际项目中的应用示例。此外,文章还提到了截至 2016 年 4 月 29 日,ButterKnife 的最新版本为 8.0.1,为开发者提供了最新的功能和性能优化。 ... [详细]
  • 本文探讨了 Kafka 集群的高效部署与优化策略。首先介绍了 Kafka 的下载与安装步骤,包括从官方网站获取最新版本的压缩包并进行解压。随后详细讨论了集群配置的最佳实践,涵盖节点选择、网络优化和性能调优等方面,旨在提升系统的稳定性和处理能力。此外,还提供了常见的故障排查方法和监控方案,帮助运维人员更好地管理和维护 Kafka 集群。 ... [详细]
  • 在Android 4.4系统中,通过使用 `Intent` 对象并设置动作 `ACTION_GET_CONTENT` 或 `ACTION_OPEN_DOCUMENT`,可以从相册中选择图片并获取其路径。具体实现时,需要为 `Intent` 添加相应的类别,并处理返回的 Uri 以提取图片的文件路径。此方法适用于需要从用户相册中选择图片的应用场景,能够确保兼容性和用户体验。 ... [详细]
author-avatar
挖墙找红杏000
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有