摘要:对于独立开发者而言,macOS 应用的分发一直是个痛点。想用业内标准的 Sparkle 框架做自动更新?先交 $99/年的 Apple 开发者保护费买证书,否则免谈。本文介绍一种在开发 Flow 时实践的“野路子”方案:利用 GitHub Release + Shell 脚本,绕过代码签名,实现 $0 成本的丝滑自动更新。
😫 痛点:独立开发的“最后一公里”
如果你写过 macOS 工具类应用,你一定遇到过这个死循环:
- 你开发了一个超好用的小工具(比如番茄钟、状态栏助手)。
- 你想让用户能自动获取新功能和 Bug 修复。
- 你去调研 macOS 的更新方案,发现 Sparkle 是唯一真神。
- 但是,Sparkle 强制要求应用必须有 Apple 代码签名(Code Signing)。
- 没有签名?Sparkle 下载更新后会直接报错:“签名验证失败”,更新无法安装。
对于学生党或刚起步的独立开发者,$99/年(约 700 人民币)的开发者账号是一笔不小的开支。难道没有证书,用户就只能手动去 GitHub 下载新包覆盖吗?
当然不。我们可以自己造轮子。
💡 核心思路:暴力美学
抛开复杂的签名验证,自动更新的本质只有三步:
- 下载新文件。
- 删除旧文件。
- 打开新文件。
macOS 的文件系统允许我们对 /Applications/ 目录进行操作(只要用户授权)。我们可以利用 Swift 调用系统的 Shell 环境,直接执行一套组合拳,完成“热替换”。
流程图如下:
graph LR
A[检测更新] --> B{有新版本?}
B -- Yes --> C[弹窗提示]
C --> D[用户点击更新]
D --> E[后台下载 ZIP]
E --> F[解压并覆盖 App]
F --> G[重启应用]
🛠️ 实现步骤
1. 服务端:一个简单的 XML
我们需要一个“公告栏”告诉 App 最新版本是多少。在 GitHub 代码仓库里放一个 appcast.xml 即可。
每次发版时,发布脚本会自动更新这个文件:
-
Version 1.3.8
23
1.3.8
2. 客户端:Swift 检测逻辑
在 App 启动时,拉取这个 XML,对比本地 Bundle.main 的 CFBundleVersion。
// UpdateManager.swift
func checkForUpdates() {
let appcastURL = URL(string: "https://raw.githubusercontent.com/.../appcast.xml")!
URLSession.shared.dataTask(with: appcastURL) { data, _, _ in
// 解析 XML 获取远端版本号 (这里省略 XML 解析代码)
let latestVersion = 23
let currentVersion = Int(Bundle.main.infoDictionary?["CFBundleVersion"] as! String)!
if latestVersion > currentVersion {
DispatchQueue.main.async {
showUpdateAlert() // 弹窗提示用户
}
}
}.resume()
}
3. 核心 Hack:Shell 脚本热替换
这是整个方案最骚操作的地方。当用户点击“更新”按钮时,我们不使用任何高级 API,而是直接创建一个 Process 运行 Shell 脚本。
// 也就是用户点击 Alert 上的“更新”按钮后执行
func performUpdate() {
let task = Process()
task.executableURL = URL(fileURLWithPath: "/bin/bash")
// 脚本逻辑:下载 -> 解压 -> 删旧 -> 移新 -> 提权 -> 重启
let script = """
# 1. 下载
curl -sL "https://github.com/.../Flow.app.zip" -o /tmp/Flow.zip
# 2. 解压
unzip -oq /tmp/Flow.zip -d /tmp
# 3. 替换 (危险操作,慎用)
rm -rf /Applications/Flow.app
mv /tmp/Flow.app /Applications/
# 4. 关键:移除隔离属性 (绕过 "App已损坏" 提示)
xattr -rd com.apple.quarantine /Applications/Flow.app
# 5. 重启
open /Applications/Flow.app
"""
task.arguments = ["-c", script]
task.run()
// 退出当前旧进程,把舞台交给新进程
NSApplication.shared.terminate(nil)
}
🧐 为什么它能工作?(技术原理解析)
你可能会问:为什么 Sparkle 不行,这样暴力操作反而行?
-
绕过 Gatekeeper 验证:
标准 App 下载后会有com.apple.quarantine属性。如果你没有签名,系统会直接拦截并在打开时提示“无法打开”或“移到废纸篓”。
但在我们的脚本中,利用xattr -rd命令主动移除了这个属性。相当于告诉系统:“我是用户自己下载并解压的,我信任它。” -
Unix 的文件特性:
在 macOS (Unix) 中,删除一个正在运行的程序的可执行文件(rm -rf)通常不会导致进程立即崩溃,因为代码段已经被加载到内存中了。这给了我们几毫秒的时间差来完成mv操作并执行重启。 -
避开了 API 限制:
Sparkle 使用的是 macOS 官方推荐的更新 API,这套 API 强制绑定了安全校验体系。而我们使用的是底层的 Shell 命令,属于“降维打击”。
⚠️ 优缺点权衡
✅ 优点
- 省钱:$0 成本,不需要开发者证书。
- 自由:完全可控,由于使用了 GitHub Release,带宽也是免费的。
- 极客范:代码量极少,逻辑清晰。
❌ 缺点与风险
- 权限问题:脚本假设 App 安装在
/Applications文件夹。如果用户安装在其他地方,脚本需要做路径适配。 - 首次安装体验:用户第一次安装你的 App 时,因为没有证书,依然需要去“系统设置 -> 隐私与安全性”里手动点一次“仍要打开”。但之后的更新就是全自动的了。
- 安全性:由于没有签名校验,理论上如果你的 GitHub 账号被黑,分发的包被篡改,客户端是无法感知的(可以通过加 SHA256 校验来改进)。
📝 总结
对于 Flow 这样的个人独立项目,这套方案完美平衡了成本与体验。它不需要复杂的服务器支持,也不需要向 Apple 缴纳昂贵的年费,却能给用户提供近乎原生的“一键更新”体验。
如果你也在做 Side Project,不妨试试这个方案,把那 $99 省下来喝咖啡吧!☕️
代码细节可以查看我的GitHub仓库:https://github.com/MuQY1818/Flow
- 1本网站名称:MuQYY
- 2本站永久网址:www.muqyy.top
- 3本网站的文章部分内容可能来源于网络,仅供大家学习与参考,如有侵权,请联系站长 微信:bwj-1215 进行删除处理。
- 4本站一切资源不代表本站立场,并不代表本站赞同其观点和对其真实性负责。
- 5本站一律禁止以任何方式发布或转载任何违法的相关信息,访客发现请向站长举报
- 6本站资源大多存储在云盘,如发现链接失效,请联系我们我们会在第一时间更新。






暂无评论内容