为什么我的产品尽量不用「外置」动态链接库
先说在前面,给现在或未来的用户吃颗定心丸:
文中说到的那些「因为动态库导致无法启动」的问题,
都是我在早期开发阶段踩过的坑,已经在后续版本里彻底修掉了 。
现在每次发版前,我都会跑一套自动脚本去检查依赖,
目的是尽可能避免类似问题再次发生。
这篇文章,更像是一个开发者自述:
我为什么在 macOS 应用里,对「外置动态链接库」这件事格外小心。
背景:一款在 Mac 上做远程的工具
我是一个 macOS 独立开发者,做了一款在 Mac 上远程连接服务器的客户端,支持:
- • SSH
- • RDP(远程桌面)
- • VNC
上层界面是用苹果官方的 Swift 写的,
但底层协议实现,离不开很多 C / C++ 的第三方库。比如:
- • RDP 协议 :使用的是业界常用、非常成熟的 FreeRDP
除了微软官方客户端之外,现在很多 RDP 工具都是在它的基础上做的,
我选择它的原因也很简单:稳定、成熟、被验证过。
对用户来说,只要「点一下就能远程上去」,
背后是 Swift + 各种第三方库协同工作。
集成第三方库,一般有两条路
把这些 C / C++ 库塞进一个 macOS 应用,常见有两种方式:
方式一:直接把源码拉进工程一起编译
优点:
- • 想改源码就改,立刻生效
- • 所有依赖都在工程里,比较一体化
缺点:
- • 编译时间会变得很长
- • 工程越来越「重」,每次改动都要等很久
方式二:先编译成静态库 / 动态库,再集成
也就是先把 C / C++ 代码编译成 .a / .dylib,
主应用只负责链接这些二进制文件。
优点:
- • 编译主应用的时间会快很多
- • 底层库的版本比较稳定,不会经常被改动
缺点:
- • 这些库本身可能再依赖其他库,如果不检查清楚,容易出现「隐形依赖」
- • 一旦出了问题,排查会比有源码时麻烦一点
目前我的做法是偏向 第 2 种 :
把复杂的底层库整理好,再集成到主工程里。
也正是在这个过程中,我踩到了一个跟「动态库」有关的大坑。
问题出在哪:那些悄悄被依赖的动态库
预编译好的库,往往不止自己一个文件:
- • 有的依赖静态库
- • 有的依赖系统自带的动态库
- • 还有的会依赖你本机通过 Homebrew 等方式安装的动态库
真正危险的是这类路径:
- •
/usr/local/lib/xxxx.dylib - •
/opt/homebrew/lib/xxxx.dylib - • 以及类似的「只在开发机上存在」的路径
在我的开发环境里,这些库都是存在的,所以:
- • 编译顺利
- • 运行正常
- • 用起来一切看似「风平浪静」
但换到用户电脑上,就不一定了:
- • 用户通常不会在
/usr/local/lib里装这些开发相关的动态库 - • 应用一旦在启动时找不到依赖,就会出现 「应用无法正常启动」 的情况
换句话说:
在我这里跑得很好的版本,一旦发出去,有可能在部分用户环境下根本起不来。
这类问题,在传统桌面软件里其实不算罕见,
但在 App Store 这种面向普通用户的环境里,一旦发生就是非常糟糕的体验。
不能把希望寄托在苹果审核上
有些人可能会想:
这种问题,苹果审核应该会帮你挡住吧?
审核确实会做一些自动检测和基本验证,
但它主要是为了:
- • 安全性
- • 是否使用了禁止的 API
- • 是否符合审核规则
它不会也不可能帮你覆盖所有「用户机器的环境差异」。
尤其是当你的应用已经多次通过审核之后,有时候流程会比较快,
不一定会在各种环境下完整跑一遍。
所以,对我来说结论很明确:
审核是「最后一层兜底」,
而不是替你做完所有环境测试的质量部门。
真正能放心的方式,还是要在发版前,自己多加几道保险。
我的解决方案:写脚本做一次「全面体检」
从那次之后,我给自己定了一个硬性要求:
每次发版前,都必须对打包好的
.app做一遍「依赖体检」。
做法其实不复杂,大致分三步:
-
- 找到
.app里所有可能会被加载的可执行文件 / 动态库
- 找到
-
- 用工具(比如
otool -L)列出它们依赖了哪些动态库
- 用工具(比如
-
- 把这些依赖路径和一份「允许列表」做对比,
只要发现类似/usr/local/lib、/opt/homebrew/lib等路径,就直接标红
- 把这些依赖路径和一份「允许列表」做对比,
跑完以后,你可以一眼看到:
- • 哪些是系统库,没问题
- • 哪些是你自己打包进 App 的库,也没问题
- • 哪些是从本地环境「顺带带进来的」,必须处理
如果你自己也在做 macOS 开发,可以让 AI 帮你写一个类似的脚本,
关键词大概是:遍历 .app、otool -L、过滤非系统路径 等。
这跟普通用户有什么关系?
从用户角度看,这些技术细节听起来可能有点遥远。
但最终落在体验上,其实就是两件事:
-
- 应用更稳定了
- • 类似「因为环境差异导致无法启动」的问题,
在发版前就会被我自己提前拦截掉。
-
- 问题越早被发现,修复越可控
- • 比起等用户遇到问题再修,
我更希望在你看到更新之前,就已经把这些隐患清理干净。
所以,我才会在开发流程里,多加一道这样的脚本检查。
它不会出现在界面上,但会长期影响你使用时的感觉。
为什么我对「外置动态库」格外谨慎?
总结一下我的个人原则:
- • 能用系统自带的库,就不用要求用户自己额外安装
- • 能静态集成进 App Bundle 的,就不要依赖用户机器上的某个路径
- • 必须用动态库时,也要尽量做到:
- • 要么是系统一定包含的
- • 要么是我明确打包在 App 里的
这也是为什么我会说:
我的产品尽量不用「外置」动态链接库,
能自己带的,就尽量自己带着走。
对我来说,这不是技术洁癖,
而是「减少你遇到意外的几率」的一种选择。
写在最后
这篇文章更多是给对技术实现好奇的朋友看的一个幕后故事:
- • 我在集成 RDP 等底层能力时,踩过哪些坑
- • 为什么在动态库这件事上会格外慎重
- • 以及我后来是怎么把它变成一个固定的「质量检查环节」
作为用户,你不需要记住这些工具名,也不需要会写脚本。
你只要知道:
当你看到一个小版本更新时,
很多看不到的地方,其实都在一点点变得更稳、更可控。
这就是我现在开发和发版时,一直坚持的一件小事。
