“iOS自带播放器不堪重用,我花1.5周从头开发了个离线替代品!”

B站影视 日本电影 2025-06-05 02:26 2

摘要:在移动应用开发日益依赖云服务与框架的今天,一位独立开发者却选择回归本地、离线优先的设计初衷:用 1.5 周时间打造一款简洁的 iOS 音乐播放器。谈及为什么要自己动手开发时,其在博客上表示:“2025 年,在 iPhone 上想听自己收藏的音乐竟然变得挺麻烦的

在移动应用开发日益依赖云服务与框架的今天,一位独立开发者却选择回归本地、离线优先的设计初衷:用 1.5 周时间打造一款简洁的 iOS 音乐播放器。

谈及为什么要自己动手开发时,其在博客上表示:“2025 年,在 iPhone 上想听自己收藏的音乐竟然变得挺麻烦的。要么掏钱给苹果,要么得绕过一堆限制,挺烦的。于是我干脆从头写了一个自己的音乐播放器,能全文搜索、支持 iCloud,还优先本地播放。”

与此同时,他还将这个项目在 GitHub 上开源了出来,供更多的开发者参考:https://github.com/nexo-tech/music-app

作者 | Oleg Pustovit编译 | 苏宓

以下为译文:

为什么要自己动手开发一款播放器?

和很多人一样,我是那种会不知不觉订阅了一堆服务的人,有些是通过苹果官方渠道买的(比如 iCloud、Apple Music),还有一些是在其他平台上糊里糊涂续费的(像 Netflix,我居然都忘了还在付钱)。我其实也常用 Apple Music(之前也用过 Spotify),但后来发现流媒体听歌其实只是图个方便,真说不上是刚需。我自己整理了一个本地音乐库后,也没觉得损失什么,反倒不用被平台绑定了。

一开始我以为,取消 Apple Music 后还能继续用 iCloud 音乐库同步歌曲,结果一取消订阅,同步功能也没了——原来这功能是要钱的。虽然可以通过 iTunes Match(每年 24.99 美元)重新开通,但本质上只是把 256kbps 的 AAC 文件传上云端,原始音质的文件还是留在你设备上,除非你自己手动替换。现在的 Mac 上,这一切都要在 Music App 里操作。如果你一个会员都不买,那同步功能就彻底没戏了,只能靠连线或 Wi-Fi 手动同步。

我实在受够了这些限制,就决定自己动手。

如果我买了个计算设备(这里是 iPhone),那我为什么不能用代码把它变成我想用的样子呢?我想做的其实很简单:能加载音乐、整理它们、正常播放,顺便也提醒一下自己——iPhone 其实还是一台通用电脑,我应该有权自己决定怎么用它。

现在 Apple 和其他软件的现状

在开始写自己的 App 之前,我也先去研究了一下官方和第三方有哪些听本地音乐的方案。

Apple 自带的应用

从技术上讲,你可以在「文件」App 里直接播放 iCloud 里的音乐,但它根本不是为听歌设计的。没有播放列表、没有标签整理、没有播放队列……虽然勉强能放歌,但体验非常差,几乎没法用。

第三方 App

我去 App Store 找了一圈,虽然有不少看起来不错的 App,但很多都要订阅付费。说实话,这种只播放本地音乐的 App,搞订阅制真有点说不过去。

有一个叫 Doppler 的 App 我还挺喜欢的,试用了一下,它的界面主要围绕「专辑」来组织,搜索功能不太行,从 iCloud 导入音乐也挺慢,而且处理层级多的文件夹时特别麻烦。不过好的一点是,它是一次性买断,不搞订阅。

自己动手:技术折腾过程

于是我决定亲手做一个满足我需求的理想播放器:

可以在 iCloud 文件夹里全文搜索音乐,快速选中并导入;

至少要有跟官方音乐 App 差不多的功能:播放队列、播放列表、按专辑整理等等;

界面要顺手、看着舒服。

一开始试了 React Native

一开始我没用 Swift,因为之前用过一次,感觉不太好。当时虽然它的语法挺像 TypeScript,也有点 Rust 那种内存安全的风格,但那时候没有原生的 async/await,用起来比 Go 或 JS/TS 写并发代码麻烦多了,得写一堆模板代码,让人挺沮丧的。所以这次我就想换个熟一点的技术栈。

我先用 React Native 或 Expo 来试试,想着可以复用以前做网页的经验,还能套用现成的 UI 模板。播放界面做起来挺顺利的,网上有很多开源项目和教学视频。

我选了 Gionatha Sturba 做的一个模板项目(https://github.com/CodeWithGionatha-Labs/music-player),它几乎具备我需要的所有功能。

我本来想通过现成的工具处理文件访问和 iCloud 同步,但很快就遇到了大问题。像 expo-filesystem 这样(https://docs.expo.dev/versions/latest/sdk/filesystem/)的库,虽然可以用来选文件,但要递归扫描 iCloud 里层级很深的文件夹,经常失败,甚至直接把 App 弄崩了。这时候我意识到:JavaScript 的方案反而把事情搞复杂了,还不如直接用 Apple 的原生 API,虽然学起来难度大点,但更靠谱。

iOS 系统的沙盒机制限制很严,App 要访问用户的文件必须获得明确授权。React Native 在这方面很不稳定,想访问 iCloud 里的文件夹不太现实。于是我决定转向 Swift 开发,这样对文件访问和权限控制能掌握得更细致。

转向 SwiftUI

我选了 SwiftUI 而不是 UIKit 或 storyboard,因为它的语法更现代、声明式风格更清爽,不会让 UI 代码干扰到我主要关注的逻辑处理和数据同步。Swift 的 async/await 和 actor 模型也很好用,让我在处理并发和数据流时省了不少力。SwiftUI 还能把 App 分成更清晰的 ViewModel 结构,这对我用 LLM(比如 OpenAI o1 或 DeepSeek)写 UI 代码也有帮助——模型可以直接输出干净的界面或绑定代码,不会出现各种乱七八糟的依赖。

应用架构与数据模型

整个 App 的架构我参考了写后端服务的方式来做:用 SQLite 存储数据,设计成一个简单但清晰的逻辑系统。我没有用 Apple 的 CoreData,因为我需要更高的自由度,比如自己定义数据库结构、写原生 SQL、做全文搜索等等。而 SQLite 原生支持 FTS5(全文搜索引擎),让我能非常高效地做模糊查找,不用再额外集成 Elasticsearch 或自己造轮子。

三个主要界面

这个 App 一共分三个核心页面:

导入音乐库:用户选择 iCloud 文件夹后,App 会扫描所有子目录,找出音频文件,并把它们的路径存到 SQLite 数据库中。这样你就可以自由地搜索、添加文件夹和子文件夹了。Apple 自带的文件选择器非常不好用,不能一次性选中多个目录或按关键词筛选一批文件,这就是它的硬伤。

音乐管理界面:这里可以浏览和管理已导入的歌曲,整理播放列表。我大致照搬了 Apple Music 的操作逻辑,对我来说已经够用了。

播放器:负责播放、暂停、切歌、重复、随机播放、排队等功能。

一个简单的用户使用流程图如下:

第一次打开 App 时,如果还没有导入音乐,会先进入 “同步” 页面,中间有个很大的「添加 iCloud 文件夹」按钮;

选好一个文件夹后,App 会开始扫描文件,进度条会显示处理进度;

扫描完后,会自动跳转到 “音乐库” 页面,展示播放列表 / 艺人 / 专辑 / 歌曲;

点进任意一项,播放条会出现在底部;点播放条可以展开全屏播放器,里面有随机播放、重复、队列排序、音量控制等功能;

你可以随时返回音乐库,音乐继续播放不受影响;

想添加更多音乐,只需返回同步页,点右上角的「+」,再选新的文件夹,系统会在后台自动合并进当前音乐库,无需重启。

像做后端一样设计逻辑层

我之前写后端服务比较多,干脆把 App 的逻辑层也按后端思路来做。整个领域/逻辑层(处理同步、搜索、队列等)完全跟 UI 分离,数据存取也用 SQLite 操作得很精细。

简单说下架构是这样分层的:

最底层是 SQLite,存原始歌曲数据和全文搜索索引;

上面一层是 Repository,封装数据库访问逻辑,提供异步 API;

然后是 Domain Actor(Swift actor),负责业务逻辑,比如导入、搜索、排队等;

ViewModel 再订阅这些 Actor,把数据转换成适合 UI 展示的格式;

最上层的 SwiftUI 只负责“展示”数据,所有状态和逻辑处理都已经在底层搞定,彼此之间不会耦合。

这样一来,iCloud 同步、播放功能和界面展示都能分工明确、互不干扰。

在 SQLite 中实现全文搜索

我前面提到,iOS 从大概 iOS 11 开始,就原生集成了带 FTS 功能的 SQLite。这太好了,我可以不依赖第三方搜索引擎,就实现模糊搜索。

我用 SQLite.swift 这个库来写一般的数据库查询(它有点类似于安全型 SQL 构造器),但 FTS 搜索还是得用原生 SQL 语句来写。

SQLite 的 FTS5 功能对我来说非常关键,它能快速搜索歌曲名、艺人、专辑这些字段,而且不需要额外的索引系统。

创建全文搜索索引表

我建了两个 FTS 表:一个用于索引歌曲(艺术家/标题/专辑),另一个用于文件夹导入期间的文件路径。两个表都放在普通的 B-tree 表(songs、source_paths)旁边。FTS 表在 UI 层是只读的,所有写入都通过 Repository 完成,保证不会漏掉任何数据。

创建搜索索引

try db.execute("""CREATE VIRTUAL TABLE IF NOT EXISTS songs_fts USING fts5( songId UNINDEXED, artist, title, album, albumArtist, tokenize='unicode61');""")

这里用了 unicode61 分词器,可以支持更多不同语言和字符类型。而像 songId 这样的字段我标记为 UNINDEXED,防止它们占用太多索引空间。

数据可靠更新

为了保证简单又安全,我把所有的更新和插入操作都包裹在事务中。这样一来,即便应用崩溃或中断,搜索索引也不会不同步。

funcupsertSong(_song: Song) asyncthrows { db.transaction {// 插入或更新主歌曲数据// 插入或更新搜索索引数据 }}

模糊搜索查询

为了让搜索体验更友好,我自动添加了通配符支持。比如你输入“lumine”,系统内部会搜索“lumine*”,即便是部分匹配也能立刻返回结果。

我还利用了 SQLite 内置的智能排序算法(bm25),能在不增加额外复杂度的前提下返回更相关的结果:

SELECT s.*FROM songs s JOIN songs_fts fts ON s.id = fts.songIdWHERE songs_fts MATCH ?ORDERBY bm25(songs_fts)LIMIT ? OFFSET ?;

总的来说,使用原生 SQLite 提供了我所需的灵活性:可预期的 schema、本地优先的访问方式、强大的全文搜索功能,而且无需依赖网络或外部服务。这种方式非常适合一个注重隐私、强调离线使用的应用。

与 iOS 文件系统和书签交互

在 iOS 上,应用可以存储对文件位置的持久书签(bookmarks),但所谓的“安全作用域书签”(security-scoped bookmarks),即允许访问沙盒外部文件的权限机制,仅在 macOS 上可用。iOS 应用只能使用普通书签记录路径,之后需通过文档选择器再次请求访问权限,而且这类访问不能悄无声息地持续生效。

为了缓解这个问题,我实现了一种回退机制:将文件复制到应用自身的沙盒目录中。这样可以规避安全作用域书签生命周期脆弱的问题——比如 iOS 重置权限后可能导致访问失败。我选择在后台主动复制文件,只要书签仍然有效,就能确保不会访问到无效音频路径。

这种方式还能提升索引速度。我可以在访问权限仍然有效时一次性遍历整个目录结构,只导入相关音频文件,并可靠地深入嵌套目录。但要在设备重启后仍能稳定播放这些外部音频文件,目前我还没找到解决方案。这也说明,即便对原生开发者来说,iOS 文件访问的这一用例仍然缺乏支持,处理起来也依然复杂。

构建播放功能和用户界面

元数据解析

为了从音频文件中解析元数据,我使用了 Apple 的 AVFoundation 框架,尤其是 AVURLAsset 类,可以用来检查媒体文件的标题、专辑艺术家等元信息。

虽然大部分元数据由原生 SDK 处理,但比如曲目编号等字段仍需手动从 ID3 标签中提取。我通过 GitHub 搜索(https://github.com/TastemakerDesign/Warper/blob/2af8c07ad8422f4dc3a539177d3a76ee8502e632/plugins/flutter_media_metadata/ios/Classes/Id3MetadataRetriever.swift)找到了一些处理边缘情况的代码示例,因为官方文档在这方面覆盖非常有限。

音频播放功能

当音乐库完成索引后,构建一个播放器其实很简单:初始化一个 AVAudioPlayer 实例播放音频即可。为了支持系统控制中心播放功能,我实现了 AVAudioPlayerDelegate 协议,并接入了 Apple 的 MPRemoteCommandCenter,从而可以响应系统级的播放控制事件。

一些反思

不足之处

Xcode 的局限性仍然令人沮丧。SwiftUI 的实时预览确实是进步,但整体开发体验依旧无法与五年前的 Flutter 相提并论——Flutter 拥有紧密的 VSCode 集成、实时模拟器热重载和熟悉的调试工具。

编辑器灵活性差。想要在 Neovim 或 VSCode 中配置 Swift 的 Language Server Protocol(LSP)支持,你还得安装像 xcode-build-server 这样的工具,效果依旧赶不上 web 开发那种轻快的体验。

Apple 的 SDK 有些部分还停留在 Objective-C 时代。比如 Spotlight 文件搜索功能只能通过 NSMetadataQuery 使用,采用 KVO 和字符串键,没有 Swift 友好的封装。加上文档稀缺,学习成本也就更高。

SwiftUI 的声明式 UI 很棒,但调试 iCloud 相关功能仍需手动 mock。因为 SwiftUI 的预览无法模拟涉及 iCloud 权限的完整行为,必须自行模拟云端交互,虽然只是个小问题,但确实麻烦。

优点之处

async/await 真是福音。终于可以像写同步代码一样写并发逻辑,告别烦人的回调地狱。甚至可以在 Actor 里写 I/O 密集的逻辑,像 JavaScript 那样直接调用,非常丝滑。

丰富的原生库支持。在 React Native/Flutter 等生态里你可能会受限于开源绑定质量,但在 iOS 原生开发中你可以更自由地做“严肃一点”的应用。Apple 提供的许多 API 都附带示例代码,入门门槛反而降低了。

SwiftUI 本身就很棒。React 风格的 UI 构建方式让开发效率更高、更容易试验和构思。Apple 采纳它实在是明智之举。

总结:构建应用本应更简单

折腾了 1.5 周之后,我做出了一个完全满足个人需求的软件 —— 一个可以从云端导入音频文件的本地/离线音乐播放器。

但很快你会意识到,如今的开发者很难自由地把自己写的 App 装到设备上长期使用。没有开发者证书,应用只能运行 7 天,之后你必须重新构建。除非你每年向 Apple 支付 99 美元,注册开发者计划。

即便在欧盟《数字市场法》(DMA)生效后,sideloading(侧载)也仍非完全开放。虽然 EU 用户现在可以从第三方网站直接安装 App,但前提是开发者已经加入 Apple 的 $99/年开发者计划,并接受 Apple 的“替代条款”。对于纯粹的个人或爱好者来说,这并未真正解除“7 天使用期”的限制。

这根本说不通。一家声称推动技术创新的公司,反而在人为设置障碍,阻碍开发者自由创作。即便是渐进式 Web 应用(PWA)在 iOS 上也存在明显限制:即使在 iOS 16~18.x 中,PWA 仍运行在 Safari 的沙盒里。它们获得了 WebGL2 和 Web 推送,但仍然缺乏 Web 蓝牙/USB/NFC、后台同步,甚至连超过约 50MB 的稳定存储也不支持。WebGL 还通过 Metal 的中间层运行,实际帧率远低于原生 Metal 应用——对 UI 来说勉强够用,但无法支撑真正的 3A 级 3D 游戏。

现在 AI 已经显著降低了现代软件开发的门槛,让任何人都能快速上手未知技术、构建自己的工具。我们也看到 Web 开发因其开放性吸引了大量非技术背景的创作者,他们无需掌握一堆技术就能实现自己的想法。但在移动开发领域,你还是得遵守一整套人为的规则。即使是你自己为自己做的 App,Apple 仍然握有决定权,限制你运行超过 7 天。

这家公司曾经点燃了独立开发者的梦想,如今却亲手关上了那扇自由的大门。在 AI 让一切变得更简单的时代里,唯有 iOS 开发仍被死死锁住。

来源:JAX的科技小讯

相关推荐