Android7 U盘插拔链路源码全解析(七)应用层MediaScanner与SAF
系列目录第一篇全景图与调用链路概览 | 第二篇内核层—USB驱动与uevent | 第三篇Native层—vold与NetlinkManager | 第四篇Framework层(上)—UsbHostManager | 第五篇Framework层(下)—MountService | 第六篇广播分发与SystemUI响应 |第七篇应用层—MediaScanner与SAF| 第八篇实战调试与案例分析一、引言前面六篇走完了从硬件到通知栏的完整链路。但此时 U 盘虽然已经挂载到/mnt/media_rw/Udisk文件系统已经可读但用户打开一个音乐播放器或相册仍然可能看不到 U 盘上的文件。原因很简单文件系统挂载成功 ≠ 应用能访问到文件。Android 应用需要通过MediaStore媒体数据库来发现媒体文件。本文聚焦应用层的两个核心机制MediaScanner扫描 U 盘上的媒体文件写入 MediaStore 数据库SAF存储访问框架通过 DocumentsProvider 暴露 U 盘文件系统给文件管理器Android 7 与后续版本的重要区别Android 7Nougat没有分区存储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 使用FUSEFilesystem 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.javapublicclassMediaScannerReceiverextendsBroadcastReceiver{OverridepublicvoidonReceive(Contextcontext,Intentintent){finalStringactionintent.getAction();finalUriuriintent.getData();if(Intent.ACTION_BOOT_COMPLETED.equals(action)){// ★ 开机时扫描内部和外部存储scan(context,MediaProvider.INTERNAL_VOLUME);scan(context,MediaProvider.EXTERNAL_VOLUME);}elseif(uri.getScheme().equals(file)){Stringpathuri.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){BundleargsnewBundle();args.putString(volume,volume);context.startService(newIntent(context,MediaScannerService.class).putExtras(args));}}3.3 MediaScannerServicepublicclassMediaScannerServiceextendsServiceimplementsRunnable{privatevolatileMediaScannermScanner;OverridepublicintonStartCommand(Intentintent,intflags,intstartId){// ★ 用独立线程执行扫描避免阻塞主线程newThread(null,this,MediaScannerService).start();returnService.START_REDELIVER_INTENT;}Overridepublicvoidrun(){Looper.prepare();try{StringvolumemArgs.getString(volume);// ★ 创建 MediaScanner 实例mScannernewMediaScanner(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[]filesdir.listFiles();if(filesnull)return;// 3. ★ 逐个处理for(Filefile:files){if(file.isDirectory()){scanDirectory(file);// 递归}else{processFile(file);// 处理单个文件}}// 4. ★ 批量提交到 MediaProvidermClient.flush();}3.5 processFile() —— 单文件处理privatevoidprocessFile(Filefile){Stringpathfile.getAbsolutePath();// 1. ★ 根据扩展名判断 MIME 类型StringmimeTypeMediaFile.getMimeTypeForFile(path);if(mimeTypenull)return;// 非媒体文件跳过// 2. ★ 读取元数据if(mimeType.startsWith(audio/)){// 读取 ID3 标签MediaMetadataRetrieverretrievernewMediaMetadataRetriever();retriever.setDataSource(path);titleretriever.extractMetadata(METADATA_KEY_TITLE);artistretriever.extractMetadata(METADATA_KEY_ARTIST);durationLong.parseLong(retriever.extractMetadata(METADATA_KEY_DURATION));retriever.release();}elseif(mimeType.startsWith(image/)){// 读取图片尺寸BitmapFactory.OptionsoptsnewBitmapFactory.Options();opts.inJustDecodeBoundstrue;BitmapFactory.decodeFile(path,opts);widthopts.outWidth;heightopts.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, DURATIONMediaStore.Video.Media.EXTERNAL_CONTENT_URI视频文件TITLE, DURATION, WIDTH, HEIGHTMediaStore.Images.Media.EXTERNAL_CONTENT_URI图片文件TITLE, WIDTH, HEIGHTMediaStore.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){MatrixCursorresultnewMatrixCursor(projection);StorageManagersmgetContext().getSystemService(StorageManager.class);for(VolumeInfovol:sm.getVolumes()){if(vol.isVisible()vol.isMountedReadable()){MatrixCursor.RowBuilderrowresult.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){FilefilegetFileForDocId(docId);intaccessModeParcelFileDescriptor.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 盘文件访问的完整流程MediaScanner 扫描收到MEDIA_MOUNTED广播后递归扫描 U 盘目录提取媒体元数据批量写入 MediaStore 数据库.nomedia 机制在目录中放置.nomedia文件可阻止 MediaScanner 扫描该目录SAF 访问通过ExternalStorageProvider和DocumentsUI提供标准的文件选择器访问Android 7 特点没有分区存储应用持有权限后可直接通过文件路径访问 U 盘MediaScanner 的扫描是异步的大容量 U 盘可能需要数秒到数十秒才能完成扫描。在此之前应用通过 MediaStore 查询不到 U 盘上的文件。下一篇是本系列的收官之作我们将通过实战案例分析如何定位和解决 U 盘相关问题。

相关新闻

数值计算稳定性:后向误差原理与通用收敛算法设计

数值计算稳定性:后向误差原理与通用收敛算法设计

1. 从“算得准不准”到“算得有多准”:后向误差的引入在数值计算领域,尤其是线性代数求解中,我们常常面临一个灵魂拷问:这个解到底有多准?对于线性系统Ax b,我们通过某种算法(比如高斯消元法、…

2026/6/26 1:42:28阅读更多 →
Qwen3-Coder本地部署实战:Ollama一键启用生产级AI编程

Qwen3-Coder本地部署实战:Ollama一键启用生产级AI编程

1. 项目概述:为什么本地跑 Qwen3-Coder 不再是“极客特权”,而是一线开发者的日常工具 Qwen3-Coder 是我最近三个月在真实项目中反复验证、压测、拆解后,确认真正能扛起主力开发任务的本地代码模型。它不是那种“能跑就行”的玩具模型&#…

2026/6/26 1:42:28阅读更多 →
纳米堆栈是什么?IBM如何像建城市一样造芯片

纳米堆栈是什么?IBM如何像建城市一样造芯片

自微处理器问世以来,半导体制造商一直致力于在有限面积内集成更多晶体管,以提升机器性能。然而,随着晶体管尺寸持续缩小,芯片上可容纳的数量已趋近物理极限,行业亟需突破性解决方案。IBM最新推出的纳米堆栈&#xff08…

2026/6/26 1:42:28阅读更多 →
CTF Web安全实战:逻辑漏洞与文件包含漏洞的挖掘与防御

CTF Web安全实战:逻辑漏洞与文件包含漏洞的挖掘与防御

1. 项目概述:一次典型的Web安全实战演练最近在复盘一些经典的CTF(Capture The Flag)题目,特别是“极客大挑战”系列,发现其中有不少题目设计得非常精妙,能很好地串联起Web安全的多个知识点。今天想和大家深…

2026/6/26 2:42:32阅读更多 →
通过 npm 安装 Claude Code

通过 npm 安装 Claude Code

1️⃣ 安装 Node.js 和 npmbash# 安装 Node.js 20.x 源 curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - # 安装 nodejs (会自动包含 npm) sudo apt install -y nodejs安装完成后,验证一下:bashnode -v # 应该显示 v20.x.x npm -v…

2026/6/26 2:42:32阅读更多 →
Claude Code CLI 源码拆解:Node.js 子进程模型与 ACP 协议设计

Claude Code CLI 源码拆解:Node.js 子进程模型与 ACP 协议设计

Claude Code CLI 源码拆解:Node.js 子进程模型与 ACP 协议设计 三角对照:OpenClaw Gateway → Hermes 单循环 → Claude Code 子进程 TL;DR Claude Code 是 Anthropic 官方推出的 CLI Agent,npm 全局安装后通过 claude 命令运行。它与 OpenClaw 和 Hermes Agent 解决同一个…

2026/6/26 2:42:32阅读更多 →
从Lampiao靶场实战解析渗透测试:Drupal漏洞利用与权限提升

从Lampiao靶场实战解析渗透测试:Drupal漏洞利用与权限提升

1. 项目概述:从“Lampiao”到开源漏洞靶场最近在安全圈里,一个叫“Lampiao”的词被反复提及。如果你在VulnHub或者一些CTF(Capture The Flag)挑战平台上看到它,千万别以为这是什么新的咖啡品牌或者灯具。实际上&#x…

2026/6/26 2:42:32阅读更多 →
从0实现工业级 RAG 智能客服:架构、核心代码、部署全拆解

从0实现工业级 RAG 智能客服:架构、核心代码、部署全拆解

上周有个朋友找我吐槽:他用 LangChain 照着官方教程搭了个 RAG 客服,Dem)o 跑得挺溜,一上线就崩——用户问"我的订单怎么没发货",它给人推荐了一篇《物流行业分析报告》。 Demo 和工业级之间,隔的不是代码量…

2026/6/26 2:42:32阅读更多 →
AI Agent Skill 工程化 00:从 0 到 1 搭建一套 Skills Engineering 工程体系

AI Agent Skill 工程化 00:从 0 到 1 搭建一套 Skills Engineering 工程体系

前言:一个你可能反复经历的场景 你总结了经验,花了两个小时,给 AI 编程助手写了一份精心打磨的 Skill——代码审查规范。 第一次用,效果惊艳:AI 像资深同事一样逐条审查,输出结构化报告,你满意…

2026/6/26 2:37:32阅读更多 →
【人工智能】一文搞定到底什么是智能体

【人工智能】一文搞定到底什么是智能体

【人工智能】一文搞定到底什么是智能体 一文搞定到底什么是智能体【人工智能】一文搞定到底什么是智能体一. LM,WorkFlow,Agent分别有什么么不同二. Agent的思考过程是怎样的三. Agent的五个核心部分1)LLM2)Prompt3)Me…

2026/6/25 9:39:54阅读更多 →
嵌入式GUI控件实战:ROTARY、SCROLLBAR、SLIDER原理与应用

嵌入式GUI控件实战:ROTARY、SCROLLBAR、SLIDER原理与应用

1. 嵌入式GUI控件:从原理到实战的深度解析在嵌入式系统开发中,图形用户界面(GUI)的设计与实现往往是项目从“能用”到“好用”的关键一跃。不同于资源充沛的PC或移动平台,嵌入式设备的GUI需要在有限的CPU性能、内存空间…

2026/6/25 2:52:24阅读更多 →
Google AI Studio 300美元额度的真相与实战指南

Google AI Studio 300美元额度的真相与实战指南

1. 这300美金不是“送钱”,而是Google埋下的第一道技术门槛 你看到标题里那个醒目的“$300美金”时,第一反应可能是:又一个免费额度?领完就完事?我亲手试过——这300美金根本不是红包,而是一张入场券&…

2026/6/25 9:01:34阅读更多 →
HPE (慧与) 服务器专用 ESXi 9 全套官方定制资源详解 + 完整部署升级教程

HPE (慧与) 服务器专用 ESXi 9 全套官方定制资源详解 + 完整部署升级教程

一、前言:企业运维痛点与资源价值自博通收购 VMware 之后,原 VMware 公开免费下载渠道全面关闭,企业运维人员想要获取适配 HPE 慧与服务器的 ESXi 9 原厂镜像,必须注册博通账号、绑定有效授权才能下载,无授权账号无法获…

2026/6/26 0:02:15阅读更多 →
Kotlin的@JvmStatic与@JvmField:与Java互操作的注解

Kotlin的@JvmStatic与@JvmField:与Java互操作的注解

Kotlin作为一门现代编程语言,与Java的互操作性一直是其核心优势之一。为了让Kotlin代码能够无缝对接Java,Kotlin提供了多种注解来优化互操作体验,其中JvmStatic和JvmField是两个关键注解。它们分别用于解决静态成员和字段在Java中的访问问题&…

2026/6/26 0:02:15阅读更多 →
深入解析musl libc中的mmap实现源码

深入解析musl libc中的mmap实现源码

最近在阅读musl libc源码时,发现其mmap的实现非常精妙,特分享给大家。 一、代码整体结构 这段代码实现了__mmap函数,并通过weak_alias导出为mmap。这是典型的musl libc风格——提供弱符号以便用户可以重写。 weak_alias(__mmap, mmap); 二…

2026/6/26 0:02:15阅读更多 →