当前位置: 首页 > news >正文

Android7 U盘插拔链路源码全解析(七)应用层MediaScanner与SAF

系列目录:第一篇:全景图与调用链路概览 | 第二篇:内核层—USB驱动与uevent | 第三篇:Native层—vold与NetlinkManager | 第四篇:Framework层(上)—UsbHostManager | 第五篇:Framework层(下)—MountService | 第六篇:广播分发与SystemUI响应 |第七篇:应用层—MediaScanner与SAF| 第八篇:实战调试与案例分析


一、引言

前面六篇走完了"从硬件到通知栏"的完整链路。但此时 U 盘虽然已经挂载到/mnt/media_rw/Udisk,文件系统已经可读,但用户打开一个音乐播放器或相册,仍然可能看不到 U 盘上的文件。

原因很简单:

文件系统挂载成功 ≠ 应用能访问到文件。

Android 应用需要通过MediaStore(媒体数据库)来发现媒体文件。本文聚焦应用层的两个核心机制:

  1. MediaScanner:扫描 U 盘上的媒体文件,写入 MediaStore 数据库
  2. SAF(存储访问框架):通过 DocumentsProvider 暴露 U 盘文件系统给文件管理器

Android 7 与后续版本的重要区别:Android 7(Nougat)没有分区存储(Scoped Storage),应用只要持有READ_EXTERNAL_STORAGE权限,就可以直接通过文件路径访问 U 盘上的文件。但 MediaStore 仍然是系统推荐的标准方式。


二、U 盘挂载点的权限模型

/mnt/media_rw/Udisk ← root:media_rw (0770) — 普通应用无权直接访问 ├── Music/ │ ├── song1.mp3 │ └── song2.flac ├── DCIM/ │ └── photo.jpg └── Documents/ └── manual.pdf /mnt/runtime/default/Udisk ← FUSE 挂载(sdcard 守护进程) /mnt/runtime/read/Udisk ← 所有应用可读 /mnt/runtime/write/Udisk ← 有 WRITE_EXTERNAL_STORAGE 权限的应用可写

Android 7 使用FUSE(Filesystem in Userspace)进行权限管理。sdcard守护进程(/system/bin/sdcard)将/mnt/media_rw/Udisk重新挂载为/storage/Udisk,在此过程中实施权限控制。


三、MediaScanner 全流程拆解

3.1 架构概览

ACTION_MEDIA_MOUNTED 广播 │ ▼ MediaScannerReceiver.onReceive() │ ▼ MediaScannerService (Service) │ ▼ MediaScanner.scanDirectory() ← 递归遍历所有文件 │ ▼ MediaProvider.insert() ← 写入 MediaStore 数据库

3.2 MediaScannerReceiver —— 接收广播

源码路径packages/providers/MediaProvider/src/com/android/providers/media/MediaScannerReceiver.java

publicclassMediaScannerReceiverextendsBroadcastReceiver{@OverridepublicvoidonReceive(Contextcontext,Intentintent){finalStringaction=intent.getAction();finalUriuri=intent.getData();if(Intent.ACTION_BOOT_COMPLETED.equals(action)){// ★ 开机时扫描内部和外部存储scan(context,MediaProvider.INTERNAL_VOLUME);scan(context,MediaProvider.EXTERNAL_VOLUME);}elseif(uri.getScheme().equals("file")){Stringpath=uri.getPath();if(Intent.ACTION_MEDIA_MOUNTED.equals(action)){// ★ U盘挂载完成 → 启动扫描scan(context,MediaProvider.EXTERNAL_VOLUME);}elseif(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE.equals(action)){// 应用请求扫描单个文件scanFile(context,path);}}}privatevoidscan(Contextcontext,Stringvolume){Bundleargs=newBundle();args.putString("volume",volume);context.startService(newIntent(context,MediaScannerService.class).putExtras(args));}}

3.3 MediaScannerService

publicclassMediaScannerServiceextendsServiceimplementsRunnable{privatevolatileMediaScannermScanner;@OverridepublicintonStartCommand(Intentintent,intflags,intstartId){// ★ 用独立线程执行扫描(避免阻塞主线程)newThread(null,this,"MediaScannerService").start();returnService.START_REDELIVER_INTENT;}@Overridepublicvoidrun(){Looper.prepare();try{Stringvolume=mArgs.getString("volume");// ★ 创建 MediaScanner 实例mScanner=newMediaScanner(this,volume);// ★ 核心:递归扫描目录mScanner.scanDirectory(newFile(path));}catch(Exceptione){Log.e(TAG,"exception in MediaScanner.scan()",e);}stopSelf(mStartId);Looper.loop();}}

3.4 MediaScanner.scanDirectory() —— 递归扫描核心

publicvoidscanDirectory(Filedir){// 1. ★ 检查 .nomedia 文件if(hasNoMediaFile(dir)){mNoMediaPaths.put(dir.getAbsolutePath(),"");return;// 跳过整个目录}// 2. 列出所有文件和子目录File[]files=dir.listFiles();if(files==null)return;// 3. ★ 逐个处理for(Filefile:files){if(file.isDirectory()){scanDirectory(file);// 递归}else{processFile(file);// 处理单个文件}}// 4. ★ 批量提交到 MediaProvidermClient.flush();}

3.5 processFile() —— 单文件处理

privatevoidprocessFile(Filefile){Stringpath=file.getAbsolutePath();// 1. ★ 根据扩展名判断 MIME 类型StringmimeType=MediaFile.getMimeTypeForFile(path);if(mimeType==null)return;// 非媒体文件,跳过// 2. ★ 读取元数据if(mimeType.startsWith("audio/")){// 读取 ID3 标签MediaMetadataRetrieverretriever=newMediaMetadataRetriever();retriever.setDataSource(path);title=retriever.extractMetadata(METADATA_KEY_TITLE);artist=retriever.extractMetadata(METADATA_KEY_ARTIST);duration=Long.parseLong(retriever.extractMetadata(METADATA_KEY_DURATION));retriever.release();}elseif(mimeType.startsWith("image/")){// 读取图片尺寸BitmapFactory.Optionsopts=newBitmapFactory.Options();opts.inJustDecodeBounds=true;BitmapFactory.decodeFile(path,opts);width=opts.outWidth;height=opts.outHeight;}// 3. ★ 写入 MediaStoremClient.doScanFile(path,mimeType,file.lastModified(),file.length(),title,artist,album,duration,width,height);}

3.6 .nomedia 机制

.nomedia是一个零字节文件,放在目录中即可让 MediaScanner 跳过该目录:

/mnt/media_rw/Udisk/ ├── Music/ │ └── song1.mp3 ← 会被扫描 ├── Documents/ │ ├── .nomedia ← ★ 存在此文件 │ └── confidential.pdf ← 跳过,不扫描 └── Photos/ └── vacation.jpg ← 会被扫描

3.7 MediaStore 表结构

Content URI存储内容关键字段
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI音频文件TITLE, ARTIST, ALBUM, DURATION
MediaStore.Video.Media.EXTERNAL_CONTENT_URI视频文件TITLE, DURATION, WIDTH, HEIGHT
MediaStore.Images.Media.EXTERNAL_CONTENT_URI图片文件TITLE, WIDTH, HEIGHT
MediaStore.Files.getContentUri("external")所有文件MIME_TYPE, SIZE

四、拔出时的清理

// U 盘拔出后,删除该卷在 MediaStore 中的所有记录privatevoiddeleteFromMediaStore(Stringpath){mResolver.delete(mFilesUri,MediaStore.MediaColumns.DATA+" LIKE ? || '%'",newString[]{path});}

五、SAF(存储访问框架)

5.1 SAF 架构

SAF 提供统一的文件访问接口,核心是DocumentsProvider

┌──────────────────────────────────────────────┐ │ App(文件管理器) │ │ ACTION_OPEN_DOCUMENT_TREE │ │ DocumentsContract API │ ├──────────────────────────────────────────────┤ │ DocumentsUI(系统文件选择器) │ ├──────────────────────────────────────────────┤ │ ExternalStorageProvider │ │ (U盘/SD卡 的 DocumentsProvider) │ ├──────────────────────────────────────────────┤ │ 实际文件系统 │ │ /mnt/media_rw/Udisk │ └──────────────────────────────────────────────┘

5.2 ExternalStorageProvider 核心代码

publicclassExternalStorageProviderextendsDocumentsProvider{@OverridepublicCursorqueryRoots(String[]projection){MatrixCursorresult=newMatrixCursor(projection);StorageManagersm=getContext().getSystemService(StorageManager.class);for(VolumeInfovol:sm.getVolumes()){if(vol.isVisible()&&vol.isMountedReadable()){MatrixCursor.RowBuilderrow=result.newRow();row.add(Root.COLUMN_ROOT_ID,vol.getFsUuid());row.add(Root.COLUMN_TITLE,vol.getDescription());row.add(Root.COLUMN_DOCUMENT_ID,getDocIdForFile(vol.getPath()));row.add(Root.COLUMN_FLAGS,Root.FLAG_SUPPORTS_CREATE|Root.FLAG_LOCAL_ONLY);}}returnresult;}@OverridepublicParcelFileDescriptoropenDocument(StringdocId,Stringmode,CancellationSignalsignal){Filefile=getFileForDocId(docId);intaccessMode=ParcelFileDescriptor.parseMode(mode);returnParcelFileDescriptor.open(file,accessMode);}}

六、两条路径的对比

维度MediaStore 路径SAF 路径
适用文件仅媒体文件(音视频/图片)所有文件类型
访问方式ContentResolver.query()DocumentsContractAPI
用户交互不需要需要文件选择器授权
实时性依赖扫描(有延迟)直接访问(实时)
元数据自动提取(ID3/EXIF)无自动提取
典型应用相册、音乐播放器文件管理器、Office 应用

七、关键源码文件索引

packages/providers/MediaProvider/ ├── MediaScannerReceiver.java ★ 广播接收,触发扫描 ├── MediaScannerService.java ★ 扫描服务 ├── MediaProvider.java ★ ContentProvider └── DatabaseHelper.java ★ 数据库 frameworks/base/media/java/android/media/ ├── MediaScanner.java ★ 核心扫描逻辑 └── MediaFile.java ★ MIME 判断 packages/providers/ExternalStorageProvider/ └── ExternalStorageProvider.java ★ SAF Provider packages/apps/DocumentsUI/ └── RootsCache.java ★ 根目录缓存 frameworks/base/core/java/android/provider/ ├── MediaStore.java ★ Content URI 常量 └── DocumentsContract.java ★ SAF Contract

八、小结

本文拆解了 Android 7 应用层 U 盘文件访问的完整流程:

  1. MediaScanner 扫描:收到MEDIA_MOUNTED广播后,递归扫描 U 盘目录,提取媒体元数据,批量写入 MediaStore 数据库
  2. .nomedia 机制:在目录中放置.nomedia文件可阻止 MediaScanner 扫描该目录
  3. SAF 访问:通过ExternalStorageProviderDocumentsUI提供标准的文件选择器访问
  4. Android 7 特点:没有分区存储,应用持有权限后可直接通过文件路径访问 U 盘

MediaScanner 的扫描是异步的,大容量 U 盘可能需要数秒到数十秒才能完成扫描。在此之前,应用通过 MediaStore 查询不到 U 盘上的文件。下一篇是本系列的收官之作,我们将通过实战案例分析如何定位和解决 U 盘相关问题。

http://www.jsqmd.com/news/1078425/

相关文章:

  • 分布式事务一致性:从 Seata AT 模式到可靠消息最终一致的架构选型
  • MuleSoft企业级AI编排:LLM服务化、治理与合规落地实践
  • AI 存储风向标:美光指引再超预期,费半盘后全线修复
  • Python 并发模型与异步编程:从 GIL 约束到协程调度的工程实践
  • 游戏开发资源大全:一个仓库搞定所有学习资料
  • python基于框架flask模板template实现
  • react源码学习之Scheduler
  • Stable Diffusion提示词工程实战:从结构编码到动态权重调度
  • 可组合型数据团队:AI时代的数据交付新范式
  • TCN理解
  • 闲来做了一个轻量化在线计算器小项目,记录一下开发初衷
  • 5款英文降AI率平台实测推荐
  • 数据治理平台效能升级:五大厂商多智能体协同与全链路自动化水平全景扫描
  • 无监督学习实战地图:聚类、降维、异常检测工业落地指南
  • 翻译公司视频口译八强榜单:视频口译多场景覆盖全
  • 2023大模型工程落地四大拐点:推理优化、多模态对齐、开源分层与应用抽象
  • MongoDB 的 CRUD
  • 文心5.0原生全模态:统一语义空间下的多模态AI实践指南
  • B站直播开了HDR Vivid鸿蒙让手机看直播也有电视画质
  • 老年人健身应用设计:减法思维与技术适老化实践
  • LangGraph图编排原理与实战:构建可调试可扩展AI Agent系统
  • OpenAI与Broadcom联合推出专为AI推理打造的定制芯片Jalapeno
  • Activity Host 作为确定性编排与认知智能代理的桥梁
  • AI漫剧创作工具性价比适配梳理
  • Elasticsearch迁移到Qdrant实战指南:向量搜索性能优化与生产落地
  • 看完就会:盘点2026年好评如潮的的AI智能降重工具
  • gc触发crash,根因却是unsafe
  • 三位科技先驱谈如何与AI建立信任与责任机制
  • Bright Data AI Agent VS 传统爬虫开发
  • 从零搭建小红书爆文分析系统:日均处理 2500 条笔记的工程实践