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

2024-09-03 00:04
文章标签 源码 扫描 探究 android14

本文主要是介绍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中,提供给其他第三方的应用使用。

这篇关于Android14(U)文件扫描源码探究的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1131446

相关文章

JAVA智听未来一站式有声阅读平台听书系统小程序源码

智听未来,一站式有声阅读平台听书系统 🌟&nbsp;开篇:遇见未来,从“智听”开始 在这个快节奏的时代,你是否渴望在忙碌的间隙,找到一片属于自己的宁静角落?是否梦想着能随时随地,沉浸在知识的海洋,或是故事的奇幻世界里?今天,就让我带你一起探索“智听未来”——这一站式有声阅读平台听书系统,它正悄悄改变着我们的阅读方式,让未来触手可及! 📚&nbsp;第一站:海量资源,应有尽有 走进“智听

Android平台播放RTSP流的几种方案探究(VLC VS ExoPlayer VS SmartPlayer)

技术背景 好多开发者需要遴选Android平台RTSP直播播放器的时候,不知道如何选的好,本文针对常用的方案,做个大概的说明: 1. 使用VLC for Android VLC Media Player(VLC多媒体播放器),最初命名为VideoLAN客户端,是VideoLAN品牌产品,是VideoLAN计划的多媒体播放器。它支持众多音频与视频解码器及文件格式,并支持DVD影音光盘,VCD影

Java ArrayList扩容机制 (源码解读)

结论:初始长度为10,若所需长度小于1.5倍原长度,则按照1.5倍扩容。若不够用则按照所需长度扩容。 一. 明确类内部重要变量含义         1:数组默认长度         2:这是一个共享的空数组实例,用于明确创建长度为0时的ArrayList ,比如通过 new ArrayList<>(0),ArrayList 内部的数组 elementData 会指向这个 EMPTY_EL

如何在Visual Studio中调试.NET源码

今天偶然在看别人代码时,发现在他的代码里使用了Any判断List<T>是否为空。 我一般的做法是先判断是否为null,再判断Count。 看了一下Count的源码如下: 1 [__DynamicallyInvokable]2 public int Count3 {4 [__DynamicallyInvokable]5 get

工厂ERP管理系统实现源码(JAVA)

工厂进销存管理系统是一个集采购管理、仓库管理、生产管理和销售管理于一体的综合解决方案。该系统旨在帮助企业优化流程、提高效率、降低成本,并实时掌握各环节的运营状况。 在采购管理方面,系统能够处理采购订单、供应商管理和采购入库等流程,确保采购过程的透明和高效。仓库管理方面,实现库存的精准管理,包括入库、出库、盘点等操作,确保库存数据的准确性和实时性。 生产管理模块则涵盖了生产计划制定、物料需求计划、

Codeforces Round #240 (Div. 2) E分治算法探究1

Codeforces Round #240 (Div. 2) E  http://codeforces.com/contest/415/problem/E 2^n个数,每次操作将其分成2^q份,对于每一份内部的数进行翻转(逆序),每次操作完后输出操作后新序列的逆序对数。 图一:  划分子问题。 图二: 分而治之,=>  合并 。 图三: 回溯:

Spring 源码解读:自定义实现Bean定义的注册与解析

引言 在Spring框架中,Bean的注册与解析是整个依赖注入流程的核心步骤。通过Bean定义,Spring容器知道如何创建、配置和管理每个Bean实例。本篇文章将通过实现一个简化版的Bean定义注册与解析机制,帮助你理解Spring框架背后的设计逻辑。我们还将对比Spring中的BeanDefinition和BeanDefinitionRegistry,以全面掌握Bean注册和解析的核心原理。

音视频入门基础:WAV专题(10)——FFmpeg源码中计算WAV音频文件每个packet的pts、dts的实现

一、引言 从文章《音视频入门基础:WAV专题(6)——通过FFprobe显示WAV音频文件每个数据包的信息》中我们可以知道,通过FFprobe命令可以打印WAV音频文件每个packet(也称为数据包或多媒体包)的信息,这些信息包含该packet的pts、dts: 打印出来的“pts”实际是AVPacket结构体中的成员变量pts,是以AVStream->time_base为单位的显

kubelet组件的启动流程源码分析

概述 摘要: 本文将总结kubelet的作用以及原理,在有一定基础认识的前提下,通过阅读kubelet源码,对kubelet组件的启动流程进行分析。 正文 kubelet的作用 这里对kubelet的作用做一个简单总结。 节点管理 节点的注册 节点状态更新 容器管理(pod生命周期管理) 监听apiserver的容器事件 容器的创建、删除(CRI) 容器的网络的创建与删除

red5-server源码

red5-server源码:https://github.com/Red5/red5-server