尧图网站建设 尧图网络
  • 首页
  • 关于我们
  • 服务项目
  • 案例展示
  • 建站流程
  • 资讯中心
  • 联系我们
首页/资讯中心/详情

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

Android7 U盘插拔链路源码全解析(七)应用层MediaScanner与SAF
📅 发布时间:2026/6/26 1:52:52

系列目录:第一篇:全景图与调用链路概览 | 第二篇:内核层—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 访问:通过ExternalStorageProvider和DocumentsUI提供标准的文件选择器访问
  4. Android 7 特点:没有分区存储,应用持有权限后可直接通过文件路径访问 U 盘

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

相关新闻

  • Kimi API合规接入指南:从认证到生产部署
  • 分布式事务一致性:从 Seata AT 模式到可靠消息最终一致的架构选型
  • 游戏开发资源大全:一个仓库搞定所有学习资料

最新新闻

  • 3分钟开启记忆守护:微信聊天记录永久保存的智能方案
  • 数字取证实战:从系统日志到内存分析,还原安全事件真相
  • 智码 AICoder · 桌面桌宠(Desktop Pet)功能完全介绍
  • SpringSecurity 静态资源放行深度详解(解决401认证失败、文件无法访问、URL拦截问题)
  • 如何在10分钟内搭建AI驱动的无代码测试平台:Testsigma完整实战指南
  • 服务定位器中的依赖查找与实例管理

日新闻

  • Qwen2.5-Turbo百万上下文实战指南:百炼平台长文本处理全解析
  • 怎么监控对标账号更新,2026年作者监控工作流,5款深度对比
  • EdgeRemover:专业级Windows Edge浏览器管理工具,彻底解决顽固软件卸载难题

周新闻

  • Visual C++运行库修复终极指南:5分钟快速解决Windows软件启动错误
  • 手把手教你构建统计局地区经济数据爬虫:从环境搭建到数据持久化全指南
  • 2026多Agent深度解析:用AI团队替代单一模型,四种架构实战落地

月新闻

  • 【总结】入门篇:50句话让你记住架构核心概念
  • WeChatMsg技术方案解析:实现Mac微信数据自主管理的完整解决方案
  • WeChatMsg:革新性微信数据备份方案,打造你的专属数字记忆库

关于尧图

  • 公司简介
  • 团队介绍
  • 企业文化
  • 荣誉资质

服务项目

  • 定制开发
  • 电商建站
  • UI 设计
  • 运维服务

快速链接

  • 案例展示
  • 建站流程
  • 常见问题
  • 资讯中心

联系方式

  • 📍北京市朝阳区互联网产业园 A 座 10 层
  • 📞400-888-8888
  • ✉️contact@rkmt.cn
  • 🕐周一至周日 9:00-21:00

© 2024 北京尧图网络科技有限公司 版权所有 | 京 ICP 备 XXXXXXXX 号