安卓文件小助理:我用 SwiftUI + ADB 写了个 macOS 安卓文件管理器,开源了
从零到 v4.1,经历了一万张照片卡死、管道死锁、滚动条跳回顶部……一个老派网管的 macOS 开发实战记录。
一、为什么要写这个
Mac 用户管理安卓手机文件一直是个痛点。Google 官方的 Android File Transfer 基于 MTP 协议,十年没更新——没搜索、没缩略图、连多选都费劲。想给手机里的文件排个序?对不起,做不到。
市面上也有第三方工具(如 Android Studio 自带的 Device Explorer、各种付费软件),但它们要么太重(装个 IDE 就为了看手机文件?),要么要钱。
我的需求很简单:
-
插上数据线就能用,手机不装任何 App
-
能浏览文件、复制粘贴、删除重命名
-
能看设备信息(电池、存储、系统版本)
-
能把手机里的 APK 导出来
-
最好还能看手机屏幕
满足这些条件的现成方案:零。
于是用 SwiftUI + ADB 写了一个。从第一个能显示文件列表的版本到今天 v4.1,前后迭代了四个大版本。开源在 GitHub,MIT 协议。
下载:https://github.com/wordwu/android-file-manager/releases
二、功能一览
2.1 文件管理
-
浏览:列表和图标网格两种视图,支持按名称/大小/日期/类型排序
-
复制/剪切/粘贴:多选模式,重名自动编号(
文件(1).apk) -
拖拽上传:Mac 文件直接拖进去
-
下载:快捷键 Cmd+D,下载到 ~/Downloads
-
重命名:支持单文件重命名和四种批量规则
-
删除:Delete 键直接删
2.2 搜索
递归搜索,输入即搜(300ms 防抖),搜文件名不搜内容。搜索结果点进去直接定位到文件所在目录。
2.3 设备信息
连上手机后侧边栏点设备名,一口气看:
| 信息 | 来源 |
|---|---|
| 型号 | getprop ro.product.model,内置 40+ 国产机型库自动匹配品牌 |
| Android 版本 | getprop ro.build.version.release |
| 电池电量/温度/健康状态 | dumpsys battery |
| 存储空间 | df -h /sdcard,换算 GB/MB |
| CPU / 内存 | getprop + cat /proc/meminfo |
| IMEI / 序列号 | service call iphonesubinfo |
小米、华为、OPPO、vivo、荣耀、一加等国产品牌都能自动识别。
2.4 APK 管理
-
应用列表:显示所有已安装应用(系统和用户应用分开)
-
一键导出 APK:选中应用 → 导出 →
adb shell pm path找到 apk 路径 →adb pull到桌面 -
批量备份:弹窗全选/反选,进度条显示,一次备份所有想要的 APK
-
安装 APK:拖个 apk 到窗口即可安装到手机

2.5 屏幕镜像
内嵌 scrcpy v4.0,点一下按钮直接弹窗显示手机屏幕。支持:
-
鼠标点击 = 手指触控
-
鼠标滚轮 = 页面滚动
-
键盘输入(需开启「USB 调试(安全设置)」)
-
快捷键:Option+H 主页、Option+B 返回、Option+S 最近应用、F11 全屏
scrcpy 直接打进 .app 包里,用户什么都不用装。

2.6 图片预览 + 视频播放
-
图片:内嵌预览面板,支持常见格式
-
视频/音频:双击自动 pull 到 Mac 临时目录 → 用系统默认播放器打开,支持 mp4/mkv/avi/mov/mp3/flac 等 20+ 格式
2.7 连接方式
-
USB:插上自动识别,首次需手机上授权「允许 USB 调试」
-
无线 ADB:USB 连上后点 WiFi 图标 → 开启无线调试 → 拔线,侧边栏出现无线设备

三、架构设计
技术栈:SwiftUI + MVVM + ADB,纯 Swift,零外部依赖(ADB 二进制打进 .app)。
├── App │ └── AndroidFileManagerApp.swift # 入口,注入 ViewModels ├── Models │ ├── FileItem.swift # 文件模型 │ ├── Device.swift # 设备模型 │ ├── AppInfo.swift # 应用信息 │ ├── APKInfo.swift # APK 元数据 │ └── TransferTask.swift # 传输任务状态机 ├── Services │ ├── ADBService.swift # ADB 通信核心 │ ├── ADBService+Network.swift # 设备发现、WiFi 连接 │ ├── ADBService+FileOps.swift # 文件列表/推送/拉取/搜索 │ └── ADBService+AppOps.swift # 应用安装/卸载/列表 ├── Utils │ ├── Constants.swift # 全局常量 │ ├── ThumbnailCache.swift # 缩略图缓存 │ └── PreviewLoader.swift # 文件预览加载 ├── ViewModels │ ├── DeviceManager.swift # 设备发现与连接 │ ├── FileBrowser.swift # 文件导航、排序、过滤 │ ├── ClipboardManager.swift # 复制/剪切/粘贴 │ ├── SearchManager.swift # 递归搜索 │ ├── AppListViewModel.swift # 应用列表 │ └── TransferManager.swift # 传输队列 └── Views ├── ContentView.swift # 主布局 ├── ToolbarView.swift # 工具栏 ├── FileListView.swift # 文件列表/网格 ├── FileRowView.swift # 单行文件 ├── PathBarView.swift # 路径面包屑 └── Browser/ # 预览、详情面板
约 39 个源文件,34 个测试用例。
四、版本迭代
v1.0 — 能看文件就行
第一个版本:USB 连接 → 显示设备 → 点进去看文件列表。功能只有浏览、复制、删除。用 ls -la 读文件列表,没分页,超过几百个文件就开始卡。
v2.0 — 加功能
加入了应用列表、APK 导出、设备信息、无线 ADB。文件操作补齐了剪切/粘贴/重命名。界面上有了工具栏、路径面包屑、侧边栏设备列表。
v3.0 — 重构
代码量上来了,一个 FileBrowser 文件塞了太多东西。做了一次 SRP(单一职责)拆分:FileBrowser / ContentView / TransferManager 各自独立。加了搜索功能,UI 改成了 NavigationSplitView 三栏布局。支持了批量重命名。
v4.0 — 性能修复(2026-06-20)
核心改动:分页加载。 之前的版本在进 Camera 这种一万多张照片的文件夹时直接卡死。原因是 ls -la 在手机上对每个文件执行 stat(),一万个文件 = 一万次 stat,命令耗时 30 秒以上。改了 ls -1aF(只读文件名不做 stat),首屏秒出。加了缩略图 4 并发异步加载,跳过 >5MB 的大文件。
同时加了屏幕镜像(scrcpy 内嵌)、批量 APK 备份弹窗、关于页面(赞赏码+GitHub 链接)。
v4.1 — 极致性能(2026-06-22)
v4.0 解决了「能不能看」的问题,v4.1 解决了「能不能快」的问题。具体遇到的坑见下一节。
五、踩过的坑
5.1 大文件夹卡死:ls -la 的 stat 陷阱
现象:点进 DCIM/Camera(11998 张照片),loading 转圈三分钟,无任何文件显示。
排查:ADB 命令是 adb shell ls -la | head -2050。直觉上 head 应该提前截断——pipe 嘛,读端关了写端就 SIGPIPE。
实际:toybox(Android 上的 busybox 替代品)的 ls -la 在执行任何输出之前,先对所有文件调了 stat()。head 退出确实发了 SIGPIPE,但此时 ls 已经 stat 完全部 11998 个文件了。30 秒过去了。然后 ADB 超时 → 重试 → fallback 到 find+stat 又 30 秒。用户看到的就是无限转圈。
修复:改用 ls -1aF,这个命令只读目录项(dentry),不 stat。不管多少文件都是秒出。文件大小等元数据通过 refreshMetadata() 在后台异步补齐。
教训:pipe 的 SIGPIPE 机制只管输出端,不管命令内部的预处理。head 能截断输出,不能截断 ls 的 stat 循环。这个坑在 Stack Overflow 上不会有答案——正常人的文件管理器不需要管一万个文件。
5.2 管道死锁:64KB 缓冲区
现象:缓存 + sed 方案在终端里 0.03 秒出结果,打成 .app 就跑超时。
排查:我把 pageSize 从 2000 调到 5000,sed 输出从 62KB 涨到 155KB。Unix pipe 的默认内核缓冲区是 64KB。写端写到 64KB 时 pipe 满了,sed 进程被挂起等待读端消费数据。而我的 Swift 代码在 process.run() 退出后才调用 readDataToEndOfFile()。
一个完美的死锁:读端等写端退出,写端等读端消费,没人动。
修复:在 process.run() 之前启动后台 DispatchQueue 线程,用 readDataToEndOfFile() 提前消费 stdout/stderr。
// 在 process.run() 之前
var stdoutData = Data()
let stdoutQueue = DispatchQueue(label: "adb.stdout")
stdoutQueue.async {
stdoutData = pipe.fileHandleForReading.readDataToEndOfFile()
}
教训:pageSize=2000 时刚好 62KB 不触发,pageSize=5000 就炸——调试的时候如果你不知道 64KB 这个数,你会在 shell 里反复测命令、怀疑 ADB 版本、甚至怀疑宇宙射线。运维的日常:一个内核数据结构吃掉半小时的命。
5.3 滚动位置跳回顶部:SwiftUI List 的 diff 机制
现象:加载更多后,列表滚动位置跳到最上面。
经历:我前后试了 5 种方案,全失败——
| 尝试 | 方案 | 为什么失败 |
|---|---|---|
| 1 | ScrollViewReader + scrollTo | List 是 NSTableView 封装,不响应 ScrollViewReader |
| 2 | ScrollView + LazyVStack | LazyVStack 懒加载,新追加的 item 还没创建,scrollTo 找不到目标 |
| 3 | AppKit 直操作 NSScrollView | 位置恢复了,但 SwiftUI 在恢复后又重建了全部视图 |
| 4 | 删除 _sortDirty 标记 | 还是跳,List 自己有一套 diff 机制 |
最终根因:不是 List 的问题,是数据架构的问题。sortedFiles 是计算属性,新数据加入后排序结果全变,ForEach 判定所有元素顺序改变 → 全量重建 → 滚动位置丢失。
修复:把「显示用数据」和「排序用数据」拆开。加了 displayItems 数组,加载更多只做 append,不做排序。列表绑定 displayItems,追加新行不重建旧行。
// 关键:读取 displayItems,只追加不排序
@Published var displayItems: [FileItem] = []
func loadMore() async {
let newItems = await fetchItems(...)
displayItems.append(contentsOf: newItems) // 纯追加
}
改完这个之后,前面三套滚动恢复方案全变死代码,删了个干净。
教训:最好的代码是你发现不需要写的代码。5 次失败的方案不是没有价值——它们帮我排除了所有「在 UI 层修」的可能性,逼我回到数据层找根因。
5.4 Swift 6 并发限制
Swift 6 禁止在 async 上下文中使用 DispatchSemaphore.wait()。缩略图加载之前用信号量限流,编译不过。改为 OperationQueue(maxConcurrentOperationCount: 4) 解决。
5.5 patch 工具吃掉字符串插值
Hermes Agent 的 patch 工具在修改 Swift 代码时会吃掉 \(...) 插值符号,变成 \\(...)(双反斜杠)。编译不报错,运行时输出字面量 adb -s \(device)。
修复方法:Python bytes.replace 字节级修复,xxd 确认源文件字节码,strings 确认二进制生效。
这条后来写进了开发铁律:改含 \(...) 的 Swift 代码行,不能用 patch,直接用 write_file 重写整个文件。
5.6 Android 16 移除了 content query
v4.1 尝试用 content query 读通话记录,发现在 Android 16 (Baklava) 上这个子命令已被移除。content call --method query 也被 SecurityException 封杀。最后确认 Android 16 上唯一可行方案是安装配套 APK 声明 READ_CALL_LOG 权限。
六、写在最后
这个项目的初衷很简单——市面上没有一个好用的 Mac 安卓文件管理器。Google 的那个 MTP 工具十年没更新,第三方要么收费要么太复杂。
从第一个能显示文件列表的版本到今天,这个项目让我踩了 macOS 开发里能踩的大部分坑:管道缓冲区、Swift 并发限制、SwiftUI 的 diff 机制、ADB 的版本兼容性、Android 16 的 API 变更。也让我确认了一件事:老派网管的那些底子没过时——看日志、查缓冲区、盯字节码、验二进制,这些十年前在酒店机房学的技能,写 SwiftUI 一样用得上。
v4.1 在 GitHub 上了:https://github.com/wordwu/android-file-manager
MIT 开源,有建议直接提 issue。骂也行,别光骂不给 star。
作者:AltairZheng,84 年老网管,做过酒店 IT 维护,转型婚礼搭建,现在用 AI 写 macOS 程序。专栏「老派 IT 生存手册」每周更新。
GitHub:https://github.com/wordwu
更多推荐
所有评论(0)