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 {
    @Override
    public void onReceive(Context context, Intent intent) {
        final String action = intent.getAction();
        if (Intent.ACTION_BOOT_COMPLETED.equals(action)) {
            // Register our idle maintenance service
            IdleService.scheduleIdlePass(context);
            StableUriIdleMaintenanceService.scheduleIdlePass(context);
        } else {
            // All other operations are heavier-weight, so redirect them through
            // service to ensure they have breathing room to finish
            intent.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()
 @Override
        void 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
    @Override
    protected 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
  @Override
    public 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类是一个定义在ModernMediaScanner的内部类,它实现了Runnable接口,也实现了FileVisitor接口,如下:

private class Scan implements Runnable, FileVisitor<Path>, AutoCloseable {
	@Override
        public 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回调方法返回扫描的文件结果:

@Override
        public 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.java
 private 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中,提供给其他第三方的应用使用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值