Android14(U)文件扫描源码探究

1.MediaReceiver

扫描的功能集中在MediaProvider中,源码位置:packages/providers/MediaProvider
其中的packages/providers/MediaProvider/AndroidManifest.xml:

<receiver android:name="com.android.providers.media.MediaReceiver"android:exported="true"><intent-filter><action android:name="android.intent.action.BOOT_COMPLETED" /></intent-filter><intent-filter><action android:name="android.intent.action.LOCALE_CHANGED" /></intent-filter><intent-filter><action android:name="android.intent.action.PACKAGE_FULLY_REMOVED" /><action android:name="android.intent.action.PACKAGE_DATA_CLEARED" /><data android:scheme="package" /></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_SCANNER_SCAN_FILE" /><data android:scheme="file" /></intent-filter></receiver>

可以看到有一个对应的MediaReceiver,它是负责接受外部的指令消息,以便发起相关盘符的扫描任务。常见的Action如android.intent.action.BOOT_COMPLETED开机启动完成、android.intent.action.MEDIA_MOUNTED磁盘挂载成功等,都会触发MediaReceiver的消息接收。代码如下:

public class MediaReceiver extends BroadcastReceiver {@Overridepublic void onReceive(Context context, Intent intent) {final String action = intent.getAction();if (Intent.ACTION_BOOT_COMPLETED.equals(action)) {// Register our idle maintenance serviceIdleService.scheduleIdlePass(context);StableUriIdleMaintenanceService.scheduleIdlePass(context);} else {// All other operations are heavier-weight, so redirect them through// service to ensure they have breathing room to finishintent.setComponent(new ComponentName(context, MediaService.class));MediaService.enqueueWork(context, intent);}}
}

2.MediaService

这里调用了MediaService的enqueueWork方法,去添加一个扫描任务,并在恰当的时候异步执行。
MeidaService继承自JobIntentService,它是Android8.0之后专门用来处理后台任务的服务。只需要执行enqueueWork方法,就可以把当前的任务Job加入到JobIntentService内部的任务队列里面,任务队列里又是使用的JobScheduler来调度任务,会在主线程任务不繁忙的时候,去调度并执行这个任务。如下:

// core/core/src/main/java/androidx/core/app/JobIntentService.java
//8.0的WorkEnqueuer.enqueueWork()@Overridevoid enqueueWork(Intent work) {if (DEBUG) Log.d(TAG, "Enqueueing work: " + work);mJobScheduler.enqueue(mJobInfo, new JobWorkItem(work));}

并且JobIntentService内部在执行任务的时候,是使用的线程池去执行的异步任务,不会阻塞主线程。针对于扫描文件这种耗时且耗性能的任务,使用此方式可以很好避免整机出现的性能瓶颈问题。关键代码如下:

// core/core/src/main/java/androidx/core/app/JobIntentService.java
@SuppressWarnings({"deprecation", "ObjectToString"}) /* AsyncTask */void ensureProcessorRunningLocked(boolean reportStarted) {if (mCurProcessor == null) {mCurProcessor = new CommandProcessor();if (mCompatWorkEnqueuer != null && reportStarted) {mCompatWorkEnqueuer.serviceProcessingStarted();}if (DEBUG) Log.d(TAG, "Starting processor: " + mCurProcessor);//使用线程池去执行异步任务mCurProcessor.executeOnExecutor(android.os.AsyncTask.THREAD_POOL_EXECUTOR);}}

同时,MediaService会实现JobIntentService的onHandleWork方法,当JobIntentService内部的JobScheduler调度器在执行到本次任务的时候,就会回调到这个方法里面,去执行具体的任务。这一点,有点类似于Handler切换线程的机制。

//packages/providers/MediaProvider/src/com/android/providers/media/MediaService.java@Overrideprotected void onHandleWork(Intent intent) {switch (intent.getAction()) {case Intent.ACTION_LOCALE_CHANGED: {onLocaleChanged();break;}......case Intent.ACTION_MEDIA_SCANNER_SCAN_FILE: {onScanFile(this, intent.getData());break;}case Intent.ACTION_MEDIA_MOUNTED: {onMediaMountedBroadcast(this, intent);break;}case ACTION_SCAN_VOLUME: {final MediaVolume volume = intent.getParcelableExtra(EXTRA_MEDIAVOLUME);int reason = intent.getIntExtra(EXTRA_SCAN_REASON, REASON_DEMAND);onScanVolume(this, volume, reason);break;}}}

可以看到针对于不同的消息类型,有不同的处理方式,我们这里以Intent.ACTION_MEDIA_MOUNTED消息类型为例,表示当磁盘挂载上之后的盘符扫描。

// packages/providers/MediaProvider/src/com/android/providers/media/MediaService.java
private static void onMediaMountedBroadcast(Context context, Intent intent)throws IOException {onScanVolume(context, mediaVolume, REASON_MOUNTED);
}public static void onScanVolume(Context context, MediaVolume volume, int reason)throws IOException {......provider.scanDirectory(volume.getPath(), reason);......
}

3.MediaProvider以及内部类ModernMediaScanner

在MediaService里面的onScanVolume会调用到MediaProvider的scanDirectory方法:

// packages/providers/MediaProvider/src/com/android/providers/media/MediaProvider.java
public void scanDirectory(@NonNull File dir, @ScanReason int reason) {mMediaScanner.scanDirectory(dir, reason);}

调用到MediaScanner的scanDirectory方法,而MediaScanner是一个接口,它的实现类是ModernMediaScanner.java,看它内部实现扫描的逻辑:

// packages/providers/MediaProvider/src/com/android/providers/media/scan/ModernMediaScanner.java@Overridepublic void scanDirectory(@NonNull File file, @ScanReason int reason) {try (Scan scan = new Scan(file, reason)) {scan.run();} catch (FileNotFoundException e) {Log.e(TAG, "Couldn't find directory to scan", e);} }

实例化了一个Scan对象,并执行了run方法。
Scan类是一个定义在MediaProvider的内部类,它实现了Runnable接口,也实现了FileVisitor接口,如下:

private class Scan implements Runnable, FileVisitor<Path>, AutoCloseable {@Overridepublic void run() {runInternal();}private void runInternal() {// First, scan everything that should be visible under requested location, tracking scanned IDs along the way// 遍历文件树walkFileTree();// Second, reconcile all items known in the database against all the items we scanned above//更新多媒体数据库,删除过时的文件信息reconcileAndClean();// Third, resolve any playlists that we scanned//更新播放列表数据resolvePlaylists();}
}

具体的扫描过程分为3个步骤,第一步是递归扫描整个文件数,遍历文件树里面的每一个文件;第二步是更新媒体数据库,删除不存在的数据项;第三步是更新播放列表的数据。

4.文件扫描----文件的遍历

文件的遍历,使用到了Java提供的API walkFileTree,它会递归遍历指定盘符下的所有文件,每扫描到一个文件,就会通过FileVisitor接口的visitFile回调方法返回扫描的文件结果:

@Overridepublic FileVisitResult visitFile(Path file, BasicFileAttributes attrs)throws IOException {// 获取文件的类型int actualMediaType = mediaTypeFromMimeType(realFile, actualMimeType, FileColumns.MEDIA_TYPE_NONE);//查询媒体库是否存在当前文件try (Cursor c = mResolver.query(mFilesUri, projection, queryArgs, mSignal)) {if (c.moveToFirst()) {// 缓存存在的文件id,在防止在后面更新媒体库的时候被删除mScannedIds.add(existingId);final boolean sameMetadata =hasSameMetadata(attrs, realFile, isPendingFromFuse, c);final boolean sameMediaType = actualMediaType == mediaType;// 文件没有改变,则继续下一个文件的遍历if (sameMetadata && sameMediaType) {if (LOGV) Log.v(TAG, "Skipping unchanged " + file);return FileVisitResult.CONTINUE;}}//如果媒体库不存在此文件,则是新加入的文件,则获取出该文件的详细信息op = scanItem(existingId, realFile, attrs, actualMimeType, actualMediaType,mVolumeName);//缓存待执行数据库插入操作的数据项addPending(op.build());//是否达到批量操作的数量maybeApplyPending();                    }

以上代码是对扫描到的文件做的逻辑处理,首先会获取文件的mediatype类型,然后再根据文件的路径去媒体数据库查找,看是否之前已经存在了该数据项。如果从MediaProvider中查找到了相同的文件信息,会去和现在扫描到的文件做比较,查看它的mime类型和mediatype类型是否一样,是一样的说明就没有变,那么就会继续执行下一个文件的扫描;如果媒体库没有查到该文件已经存在,或者查到的文件和当前文件不一样,那么就会重新获取该文件的详细信息,并且放入pending的集合中,当pending的集合达到32个之后,就会执行批量入库的操作:

 private void maybeApplyPending() {if (mPending.size() > BATCH_SIZE) {applyPending();}}private void applyPending() {//使用applyBatch来执行批量的数据操作ContentProviderResult[] results = mResolver.applyBatch(AUTHORITY, mPending);//把入库后的文件id缓存,防止后面被删除mScannedIds.add(id);mPending.clear();
}

5.文件扫描----数据的清理

完成所有文件的遍历之后,还需要对MediaProvider数据库里过时的数据做清除,如下:

 private void reconcileAndClean() {final long[] scannedIds = mScannedIds.toArray();// 获得需要删除的数据id集合addUnknownIdsAndGetMediaTypeCount(queryArgs, scannedIds);for (int i = 0; i < mUnknownIds.size(); i++) {final Uri uri = MediaStore.Files.getContentUri(mVolumeName, id).buildUpon().appendQueryParameter(MediaStore.PARAM_DELETE_DATA, "false").build();//添加批量删除的集合addPending(ContentProviderOperation.newDelete(uri).build());//执行批量操作maybeApplyPending();}
}

这里最关键一步就是得到需要删除的数据集合。首先会把MediaProvider里面的多媒体数据全部查询出来,然后对游标获取的数据做逐个对比,使用Arrays.binarySearch函数,去查看数据库中读取的id是否在本次扫描缓存的id集合中,如果不存在,那么返回的值就会小于0,就把这个数据库中读取出来的id放入mUnknownIds集合中,即这个id需要在后续从MediaProvider数据库中被删除。如下:

private int[] addUnknownIdsAndGetMediaTypeCount(Bundle queryArgs, long[] scannedIds) {//从mediaprovider中读取全部数据try (Cursor c = mResolver.query(mFilesUri,new String[]{FileColumns._ID, FileColumns.MEDIA_TYPE, FileColumns.DATE_EXPIRES,FileColumns.IS_PENDING}, queryArgs, mSignal)) {while (c.moveToNext()) {final long id = c.getLong(0);// 比较此id是否在盘符扫描结果的集合中if (Arrays.binarySearch(scannedIds, id) < 0) {//加入被删除的集合中,后续执行批量的删除操作mUnknownIds.add(id);}}}
}

6.文件扫描----播放列表更新

扫描过程的第三步就是更新媒体播放列表。首先会根据扫描的盘符去获取到对应的Uri信息,然后查询到该Uri是否在数据库中存在播放列表的数据,然后就是对该播放列表的做清除,然后重新插入新扫描的数据,如下:

//packages/providers/MediaProvider/src/com/android/providers/media/scan/ModernMediaScanner.java
private void resolvePlaylists() {final Uri playlistsUri = MediaStore.Audio.Playlists.getContentUri(mVolumeName);MediaStore.resolvePlaylistMembers(mResolver,ContentUris.withAppendedId(playlistsUri, id));
}

会调用到MediaProvider里面的callInternal方法里:

//packages/providers/MediaProvider/src/com/android/providers/media/MediaProvider.javaprivate Bundle callInternal(String method, String arg, Bundle extras) {case MediaStore.RESOLVE_PLAYLIST_MEMBERS_CALL: {return getResultForResolvePlaylistMembers(extras);}}private void resolvePlaylistMembers(@NonNull Uri playlistUri) {final DatabaseHelper helper;try {helper = getDatabaseForUri(playlistUri);} catch (VolumeNotFoundException e) {throw e.rethrowAsIllegalArgumentException();}helper.runWithTransaction((db) -> {resolvePlaylistMembersInternal(playlistUri, db);return null;});
}

这里就会调用到数据库去做删除和插入的操作:

private void resolvePlaylistMembersInternal(@NonNull Uri playlistUri,@NonNull SQLiteDatabase db) {//删除表里对应的playlist的内容db.delete("audio_playlists_map", "playlist_id=" + playlistId, null);//逐个添加播放列表的数据for (int i = 0; i < members.size(); i++) {db.insert("audio_playlists_map", null, values);}
}

至此,一个针对于特定盘符的扫描任务就完成了,新的数据也保存在了MediaProvider中,提供给其他第三方的应用使用。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/53011.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

部署Rancher2.9管理K8S1.26集群

文章目录 一、实验须知1、Rancher简介2、当前实验环境 二、部署Rancher1、服务器初始化操作2、部署Rancher3、登入Rancher平台 三、Rancher对接K8S集群四、通过Rancher仪表盘部署Nginx服务1、创建命名空间2、创建Deployment3、创建Service 一、实验须知 1、Rancher简介 中文官…

【自由能系列(中级),代码模拟】预测编码的核心:三个关键方程式的详解

预测编码的核心&#xff1a;三个关键方程式的详解 ——探索预测编码背后的数学原理与应用 核心结论&#xff1a;预测编码是一种基于贝叶斯定理的理论框架&#xff0c;它通过三个关键方程式描述了大脑如何处理和解释来自环境的信号。这些方程式分别建立了贝叶斯定理的简化形式、…

JL-02 投入式水位记录仪 采集记录一体 安装便捷

产品概述 水位记录仪是针对市场需求而研发的集成了信号采集、过程IO控制和无线数据通信于一体的高性能测控装置&#xff1b;采用低功耗技术&#xff0c;可使用太阳能、蓄电池供电&#xff0c;非常适合在野外供电条件困难的恶劣环境使用&#xff1b;安装使用方便&#xff0c;不…

9月新机首发:骁龙芯片+超大电池,游戏玩家的终极选择

随着秋风送爽的9月到来&#xff0c;智能手机和电子设备市场也迎来了新一轮的热潮。8月份的新机发布热潮刚刚退去&#xff0c;9月份的新机已经迫不及待地揭开了神秘的面纱。在众多备受期待的产品中&#xff0c;红魔品牌抢先官宣&#xff0c;两款全新的游戏平板将在9月5日正式亮相…

论文速读|通过人类远程操作的深度模仿学习框架:人型机器人的行走操纵技能

项目地址&#xff1a;Deep Imitation Learning for Humanoid Loco-manipulation through Human Teleoperation 本文详细介绍了 TRILL&#xff08;Teleoperation and Imitation Learning for Loco-manipulation&#xff09;框架&#xff0c;它是一个用于人型机器人行走操纵技能训…

LeetCode - 12 整数转罗马数字

题目来源 12. 整数转罗马数字 - 力扣&#xff08;LeetCode&#xff09; 题目描述 七个不同的符号代表罗马数字&#xff0c;其值如下&#xff1a; 符号值I1V5X10L50C100D500M1000 罗马数字是通过添加从最高到最低的小数位值的转换而形成的。将小数位值转换为罗马数字有以下规…

OpenCV绘图函数(14)图像上绘制文字的函数putText()的使用

操作系统&#xff1a;ubuntu22.04 OpenCV版本&#xff1a;OpenCV4.9 IDE:Visual Studio Code 编程语言&#xff1a;C11 算法描述 在图像上绘制指定的文本字符串。 cv::putText 函数在图像上绘制指定的文本字符串。无法使用指定字体渲染的符号会被问号&#xff08;?&#xff…

git把远程仓库的master分支合并到本地分支

假如现在我们要将远程 origin 的 master 分支合并到本地的 dev 分支&#xff0c;可以按照以下步骤进行操作&#xff1a; 切换到本地的 dev 分支&#xff1a; git checkout dev拉取远程 origin 的最新 master 分支&#xff1a; git fetch origin master将远程 origin 的 master …

9 Python函数、参数、作用域、内置函数、lambda表达式

本篇是 Python 系列教程第 9 篇&#xff0c;更多内容敬请访问我的 Python 合集 1 定义函数 在 Python 中&#xff0c;你可以使用 def 关键字来定义一个函数。函数定义的基本语法如下&#xff1a; def function_name(parameters):# 函数体# ...return valuefunction_name: 函数…

简单梯形问题

如下图&#xff0c;ABCD是一个梯形&#xff0c;E是AD的中点&#xff0c;直线CE把梯形分成甲、乙两部分&#xff0c;其面积之比为5:2&#xff0c;那么上底AB与下底CD的长度之比是&#xff08;&#xff09;。 A 2&#xff1a;5 B 3&#xff1a;5 C 3&#xff1a;4【正确答案】 D …

【ros2】 const builtin_interfaces::msg::Time timestamp解析

解析 const builtin_interfaces::msg::Time & timestamp 1. 数据类型 builtin_interfaces::msg::Time 是 ROS 2 中的一个消息类型&#xff0c;用于表示时间戳。 2. 结构 builtin_interfaces::msg::Time 包含以下字段&#xff1a; struct Time {std::uint32_t sec;std:…

LLM:推理加速相关的结构优化

对于 LLM&#xff0c;加速推理并降低显存&#xff0c;是两个至关重要的问题。本文将从 Key-Value Cache 出发&#xff0c;介绍两种相关的模型结构改进。分别是 ChatGLM 系列使用的 Multi-Query Attention&#xff08;MQA&#xff09; 和 LLama 系列使用的 Grouped-Query Attent…

C++学习, 函数返回指针

C 允许函数返回指针&#xff0c;需要声明返回指针的函数。 声明函数返回指针方式&#xff1a; type *Function() { } 程序示例&#xff1a; #include <iostream> #include <ctime> #include <cstdlib> using namespace std; int *getRandom( ) { static…

Memcached append 命令

Memcached append 命令 Memcached 是一种高性能的分布式内存对象缓存系统,常用于缓存数据库调用、API响应等,以减少服务器负载和提高访问速度。Memcached 的 append 命令用于向已存在键的值的末尾追加数据。这个功能在需要在不覆盖原有数据的情况下,对数据进行扩展时非常有…

前端与后端的身份认证

这里写目录标题 前端与后端的身份认证Web开发模式服务端渲染的Web开发模式前后端分离的Web开发模式根据场景选择开发模式 身份认证为什么需要身份认证不同开发模式下的身份认证 Session认证机制HTTP协议下的无状态性如何突破HTTP无状态的限制CookieCookie的几大特性&#xff1a…

python3.10安装

python3.10 安装 文章目录 python3.10 安装0. 我的环境1. centos7.6 安装python3需要升级openssl2. 安装python33. 查看python3版本 0. 我的环境 [rootftp ~]# cat /etc/redhat-release CentOS Linux release 7.6.1810 (Core) [rootftp ~]# openssl version OpenSSL 1.0.2k-…

VUE3+FLASK+TYPESCRIPT(实习接触,学习并自主实现)

开头 不同于笔者在学校自学简单的htmljscss的模式&#xff0c;加入了前端框架VUE3真的是一个非常方便的工具&#xff0c;而且本人主攻于c方向&#xff0c;像ts这种更严格的语法标准反而更加比原生js更能让我接受&#xff0c;由于这三个都是本人没接触的库框架和语言&#xff0c…

9 自研rgbd相机基于rk3566之qt框架开发rgb图像处理线程

qt框架开发rgb图像处理线程 rgb线程功能及处理流程1 rgb线程分为如下几个功能:2 rgb线程的处理流程如下:rgb线程qt程序详解1 接收界面启动停止信号。2 qt线程运行rgb线程程序示例1 线程头文件:pthread_33d_sc2310.h2 线程源文件:pthread_33d_sc2310.cpprgb线程功能及处理流…

ET算法【动态规划】

线性dp&#xff1a;dp[i][j] 由 dp[i - 1][j] 通过加减乘除等线性运算得到 状压dp&#xff1a;dp[i][j] 表示一个用二进制数表示的子集来反映当前状态&#xff0c;如7 &#xff08;111&#xff09;&#xff08;选了三个&#xff09; 期望dp&#xff1a;dp[i][j] 表示期望或者…

yolo8 目标检测、鉴黄

省流 看前必读 别浪费时间 &#xff1a;本文只是一个记录&#xff0c;防止自己下次被改需求时浪费时间&#xff0c;在这里就随意的写了一下文章记录整个步骤&#xff0c;但是文章想必肯定没有对应的教程讲的详细&#xff0c;该文章只适合想要快速按照步骤完成一个简单的 demo 的…