johnno1962/InjectionIII
GitHub: johnno1962/InjectionIII
面向 Xcode 的 Swift 代码热重载工具,通过动态库注入和链接器 interposing 实现运行时代码更新,大幅提升 iOS/macOS 开发迭代效率。
Stars: 4591 | Forks: 348
# InjectionIII.app 项目
## 是的,Swift 的热重载 (HotReloading)
中文版 README:[中文集成指南](https://github.com/johnno1962/InjectionIII/blob/main/README_Chinese.md)

代码注入允许你在 iOS 模拟器中以增量的方式更新函数以及类、结构体或枚举的任何方法的实现,而无需执行完整的重新构建或重启你的应用程序。这为开发者节省了大量调整代码或迭代设计的时间。实际上,它将 Xcode 从一个“源代码编辑器”变成了一个_“程序编辑器”_,在这里,源代码的修改不仅会保存到磁盘中,还会直接保存到你正在运行的程序中。
### 紧急通知:Injection 和 Xcode 16.3
InjectionIII 的工作原理是将编辑过的源文件重新编译为动态库,然后将其加载到你的应用程序中。它通过搜索最近的 Xcode 构建日志来查找 `swift-frontend` 编译器调用,以此确定如何重新编译文件。不幸的是,在这一功能已经正常运行了 10 年之后,Xcode 16.3 不再默认记录这些信息,除非你使用“Editor/Add Build Setting/Add User-Defined Setting”为你的项目的 `Debug` 构建设置添加一个 `EMIT_FRONTEND_COMMAND_LINES` 的值(设置为 "YES"),这样 InjectionIII 就可以像以前一样继续工作了。
### InjectionNext
InjectionIII 现在有一个重新启动的继任者,即非常类似的 [InjectionNext](https://github.com/johnno1962/InjectionNext) 项目。如果你遇到 InjectionIII 的限制,建议尝试一下 InjectionNext,看看问题是否在那里得到了解决。
### 如何使用
设置你的项目以使用注入现在非常简单,只需下载该应用程序的 [github
releases](https://github.com/johnno1962/InjectionIII/releases) 版本之一,或者从 [Mac App Store](https://itunes.apple.com/app/injectioniii/id1380446739?mt=12) 下载,并在你的应用程序中添加以下代码以便在启动时执行(不再需要实际运行本应用程序本身)。
```
#if DEBUG
Bundle(path: "/Applications/InjectionIII.app/Contents/Resources/iOSInjection.bundle")?.load()
//for tvOS:
Bundle(path: "/Applications/InjectionIII.app/Contents/Resources/tvOSInjection.bundle")?.load()
//Or for macOS:
Bundle(path: "/Applications/InjectionIII.app/Contents/Resources/macOSInjection.bundle")?.load()
#endif
```
同样重要的是,将选项 `-Xlinker` 和 `-interposable`(不带双引号且分行填写)添加到你项目中 target 的 "Other Linker Flags" 中,以启用“interposing”(参见下文的解释)。这必须仅针对 `Debug` 配置进行设置,否则你可能会在 TestFlight 中遇到问题。

之后,当你在模拟器中运行你的应用程序时,你应该会看到一条消息,说明已为你的主目录启动了文件监视器,并且每当你在当前项目中保存源文件时,它应该会报告该文件已被注入。这意味着以前调用旧实现的所有地方都将被更新为调用最新版本的代码。
事情并不像看起来那么简单,因为要在屏幕上立即看到结果,新代码需要被实际调用。例如,如果你注入了一个视图控制器,它需要强制重新显示。为了解决这个问题,类可以实现一个 `@objc func injected()` 方法,该方法将在类被注入后被调用,以执行对显示的任何更新。你可以使用的一种技术是在你的程序中的某个地方包含以下代码:
```
#if DEBUG
extension UIViewController {
@objc func injected() {
viewDidLoad()
}
}
#endif
```
解决此问题的另一个方案是使用由这篇
[博客文章](https://merowing.info/2022/04/hot-reloading-in-swift/)
引入的
[Inject](https://github.com/krzysztofzablocki/Inject)
Swift Package 进行“托管”。
### 注入不能做什么
你不能注入关于数据在内存中如何布局的更改,即你不能添加、删除或重新排列具有存储功能的属性。对于非最终(non-final)类,这也适用于添加或删除方法,因为用于调度的 `vtable` 本身就是一个不能在注入过程中发生改变的数据结构。
如上所述,注入也无法确定需要重新执行哪些代码片段才能更新显示。
此外,不要滥用访问控制。`private` 属性和方法不能直接注入,特别是在扩展(extensions)中,因为它们不是 `global` 可插入的符号。它们通常是间接注入的,因为它们只能在正在注入的文件内部被访问,但这可能会引起混淆。
最后,Injection 在注入过程中无法很好地处理源文件被添加/重命名/删除的情况。你可能需要构建并重新启动你的应用程序,甚至关闭并重新打开你的项目,以清除旧的 Xcode 构建日志。
### SwiftUI 的注入
如果说有什么不同的话,SwiftUI 比 UIKit 更适合注入,因为它有特定的机制来更新显示,但是你需要对每个你想要注入的 `View` 结构体做几处修改。
要强制重绘,最简单的方法是添加一个属性来观察何时发生了注入:
```
@ObserveInjection var forceRedraw
```
这个属性包装器可以在
[HotSwiftUI](https://github.com/johnno1962/HotSwiftUI) 或
[Inject](https://github.com/krzysztofzablocki/Inject)
Swift Package 中找到。它本质上包含了一个你的视图可以观察的 `@Published` 整数,该整数在每次注入时都会递增。你可以使用以下代码之一来使这些包中的一个在整个项目中可用:
```
@_exported import HotSwiftUI
or
@_exported import Inject
```
为了实现可靠的 SwiftUI 注入,你需要做的第二处修改是使用这些包中扩展 `View` 的 `.enableInjection()` 方法将 body 属性包装在 `AnyView` 中,从而“擦除返回类型”。这是因为,当你添加或删除 SwiftUI 元素时,可能会改变 body 属性的具体返回类型,这相当于改变了内存布局,可能会导致崩溃。总而言之,每个 body 的末尾应该总是看起来像这样:
```
var body: some View {
VStack or whatever {
// Your SwiftUI code...
}
.enableInjection()
}
@ObserveInjection var redraw
```
你可以将这些修改保留在你的生产代码中,因为在 `Release` 构建中,它们会被优化为空操作。
### 在 iOS、tvOS 或 visionOS 设备上的注入
这是可行的,但你需要实际运行 InjectionIII.app 的一个 [github
4.8.0+ releases](https://github.com/johnno1962/InjectionIII/releases)
版本,设置一个用户默认值以选择加入,并重新启动应用程序。
```
$ defaults write com.johnholdsworth.InjectionIII deviceUnlock any
```
然后,不要加载注入包,而是在 "Build Phase" 中运行此脚本:
(你还需要关闭项目构建设置中的 "User Script Sandboxing")
```
RESOURCES=/Applications/InjectionIII.app/Contents/Resources
if [ -f "$RESOURCES/copy_bundle.sh" ]; then
"$RESOURCES/copy_bundle.sh"
fi
```
并且,在你的应用程序中,在启动时执行以下代码:
```
#if DEBUG
if let path = Bundle.main.path(forResource:
"iOSInjection", ofType: "bundle") ??
Bundle.main.path(forResource:
"macOSInjection", ofType: "bundle") {
Bundle(path: path)!.load()
}
#endif
```
一旦你切换到这种配置,在使用模拟器时它也会起作用。请参阅
[HotReloading project](https://github.com/johnno1962/HotReloading)
的 README,获取关于如何调试你的程序通过 Wi-Fi 连接到 InjectionIII.app 的详细信息。你还需要从下拉菜单中手动选择项目目录以供文件监视器使用。
### 在 macOS 上的注入
它可以工作,但在开发期间你需要暂时关闭“ hardened runtime”下的“app sandbox”和“library validation”,以便它可以动态加载代码。为了避免代码签名问题,请使用上面关于真实设备注入说明中详细描述的新的 `copy_bundle.sh` 脚本。
### 工作原理
多年来,注入的工作方式多种多样,最初使用 Objective-C 的“Swizzling” API,但现在主要围绕 Apple 链接器的一项称为“interposing”的功能构建,它为任何类型的任何 Swift 方法或计算属性提供了解决方案。
当你的代码在 Swift 中调用一个函数时,它通常是“静态分派”的,即使用被调用函数的“mangled symbol”进行链接。然而,每当你使用 "-interposable" 选项链接你的应用程序时,就会增加一个额外的间接层,它通过一段可写内存来查找所有被调用函数的地址。利用操作系统加载可执行代码的能力以及 [fishhook](https://github.com/facebook/fishhook) 库来“重绑定”调用,因此可以“插入”任何函数的新实现,并在运行时有效地将它们缝合到程序的其余部分中。从那时起,它的表现就如同新代码已经内置到程序中一样。
注入使用 `FSEventSteam` API 来监视源文件何时被更改,并扫描最后一次的 Xcode 构建日志以了解如何重新编译它,然后链接一个可以加载到你程序中的动态库。注入的运行时支持随后加载该动态库,并扫描其中包含的函数定义,然后将其“插入”到程序的其余部分中。这并不是全部情况,因为非最终类方法的调度使用了一个也必须被更新的“vtable”(类似于 C++ 的虚方法),而且项目还会处理它以及任何遗留的 Objective-C “swizzling”。
如果你想了解更多关于注入是如何工作的,最好的资料来源是我的书 [Swift Secrets](http://books.apple.com/us/book/id1551005489) 或者是在 [InjectionLite](https://github.com/johnno1962/InjectionLite)
Swift Package 中全新重写的参考实现。有关“interposing”的更多信息,请查阅[这篇博客文章](https://www.mikeash.com/pyblog/friday-qa-2012-11-09-dyld-dynamic-linking-on-os-x.html)或 [fishhook 项目](https://github.com/facebook/fishhook)的 README。有关应用程序本身组织的更多信息,请查阅 [ROADMAP.md](https://github.com/johnno1962/InjectionIII/blob/main/ROADMAP.md)。
### 一些术语
让注入发挥作用包含三个组件。一个是文件监视器,一个是重新编译任何更改过的文件并构建可加载的动态库的代码,另一个是注入代码本身,它在你运行应用时将新版本的代码缝合到应用程序中。这三个组件的组合方式衍生出了使用注入的多种方式。
“经典注入”是指你从 github 下载一个[二进制发布版](https://github.com/johnno1962/InjectionIII/releases)并运行 InjectionIII.app。然后,如上所示在模拟器中将该应用程序内的某个 bundle 加载到你的程序中。在这种配置中,文件监视和源代码重新编译在应用程序内部完成,并且 bundle 使用套接字连接到应用程序,以了解何时有新的动态库准备好被加载。
“App Store 注入” 此版本的应用程序是沙盒化的,虽然文件监视器仍在应用程序内部运行,但重新编译和加载被委托给模拟器内部执行。这可能会引发 C 头文件的问题,因为模拟器使用区分大小写的文件系统,以忠实模拟真实设备。
“HotReloading 注入”是指你在设备上运行你的应用程序,并且因为你无法从 Mac 的文件系统在真实的手机上加载 bundle,所以你将 [HotReloading Swift Package](https://github.com/johnno1962/HotReloading)
添加到你的项目中(仅在开发期间!),它包含通常存在于 bundle 中用于执行动态加载的所有代码。这要求你使用未沙盒化的二进制发布版。它也已被上面描述的 `copy_bundle.sh` 脚本所取代。
“独立注入”。这是该项目最新的演进形式,你不再需要运行应用程序本身,只需加载其中一个注入 bundle,并且文件监视、重新编译和注入都在模拟器内部执行。默认情况下,它会监视你的主目录中的任何 Swift 文件的更改,不过你可以使用环境变量 `INJECTION_DIRECTORIES` 来更改此设置。
[InjectionLite](https://github.com/johnno1962/InjectionLite) 是一个重头开始编写的、极简的独立注入参考实现。只需添加这个 Swift Package,你就应该能够在模拟器中进行注入了。
[InjectionNext](https://github.com/johnno1962/InjectionNext)
是一个全新重头开始编写的 Injection 版本,对于大型项目来说应该更快、更可靠。它集成了 Xcode 的一个调试标志来找出如何重新编译文件,从而避免了构建日志的解析,并复用了 `InjectionLite` 中的注入客户端实现。为了与诸如 `Cursor` 之类的外部编辑器配合使用,InjectionNext 也可以使用文件监视器来检测编辑操作,并回退到构建日志解析代码。
所有这些变体都要求你为 Debug 构建添加 "-Xlinker -interposable" 链接器标志,否则你将只能注入类的非最终方法,并且所有这些变体都可以与更高层的 [Inject](https://github.com/krzysztofzablocki/Inject) 或
[HotSwiftUI](https://github.com/johnno1962/HotSwiftUI) 结合使用。
### 更多信息
请查阅[旧版 README](https://github.com/johnno1962/InjectionIII/blob/main/OLDME.md),如果说它有什么特点的话,那就是它包含了“太多的信息”,包括你可以用来进行自定义的各种环境变量。以下是一些示例:
| 环境变量 | 用途 |
| ------------- | ------------- |
| **INJECTION_DETAIL** | 详细输出所有执行的操作 |
| **INJECTION_TRACE** | 记录对注入函数的调用 (v4.6.6+) |
| **INJECTION_HOST** | 用于设备上注入的 Mac 的 IP 地址 |
设置了 **INJECTION_TRACE** 环境变量后,注入任何文件都将添加对该文件中所有函数和方法调用及其参数值的日志记录,以此作为调试的辅助手段。
InjectionIII 一个鲜为人知的功能是,只要你在某个时候运行过你的应用程序的测试,你就可以注入一个单独的 XCTest 类并让它立即运行——并且在你每次修改它时,它都会报告测试是否失败。
### 致谢:
本项目包含了来自 [rentzsch/mach_inject](https://github.com/rentzsch/mach_inject)、
[erwanb/MachInjectSample](https://github.com/erwanb/MachInjectSample)、
[davedelong/DDHotKey](https://github.com/davedelong/DDHotKey) 和
[acj/TimeLapseBuilder-Swift](https://github.com/acj/TimeLapseBuilder-Swift) 的代码,并遵循它们各自的许可证。
App Tracing 功能通过 [SwiftTrace](https://github.com/johnno1962/SwiftTrace) 项目使用了 [OliverLetterer/imp_implementationForwardingToSelector](https://github.com/OliverLetterer/imp_implementationForwardingToSelector) 的跳板(trampoline)实现,并遵循 MIT 许可证。
SwiftTrace 使用了非常方便的 [https://github.com/facebook/fishhook](https://github.com/facebook/fishhook)。
有关许可的详细信息,请参阅应用程序 bundle 中包含的项目源代码和头文件。
此版本包含了一个经过极轻微修改的优秀
[canviz](https://code.google.com/p/canviz/) 库版本,用于在 HTML 画布中渲染“dot”文件,该库受 MIT 许可证管辖。修改内容为:将节点的 ID 传递给节点标签标签(第 212 行),反转节点和连接它们的线条的渲染顺序(第 406 行),并存储边缘路径以便在 "canviz-0.1/canviz.js" 中为它们着色(第 66 行和第 303 行)。
它包含了 [CodeMirror](http://codemirror.net/) JavaScript 编辑器,用于评估使用注入执行的代码,并遵循 MIT 许可证。
在完成 100 次注入后,系统会提醒你可以在 GitHub 上赞助这个项目。
这个极棒的图标得益于 [pixel-mixer.com](http://pixel-mixer.com/) 的 Katya。
$Date: 2026/02/22 $
标签:CVE监控, DYLD, Incremental Build, InjectionIII, iOS开发, iOS模拟器, Swift, Swift开发, Xcode, 动态加载, 动态库, 威胁情报, 开发效率工具, 开发者工具, 插件, 热更新, 热重载, 移动开发, 编译器