本文主要是介绍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)文件扫描源码探究的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!