<audio id="audio" title="06 | App 如何通过注入动态库的方式实现极速编译调试？" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/2e/fe/2e82135e45c225a4c9347a67338b7bfe.mp3"></audio>

你好，我是戴铭。

在上一篇文章中，我和你分享了链接器的基础知识。今天我们再继续聊聊，动态库链接器的实际应用，也就是编译调试的提速问题。

iOS 原生代码的编译调试，都是通过一遍又一遍地编译重启 App 来进行的。所以，项目代码量越大，编译时间就越长。虽然我们可以通过将部分代码先编译成二进制集成到工程里，来避免每次都全量编译来加快编译速度，但即使这样，每次编译都还是需要重启 App，需要再走一遍调试流程。

对于开发者来说，提高编译调试的速度就是提高生产效率。试想一下，如果上线前一天突然发现了一个严重的bug，每次编译调试都要耗费几十分钟，结果这一天的黄金时间，一晃就过去了。到最后，可能就是上线时间被延误。这个责任可不轻啊。

那么问题来了，原生代码怎样才能够实现动态极速调试，以此来大幅提高编译调试速度呢？在回答这个问题之前，我们先看看有哪些工具是这么玩儿的。了解了它们的玩法，我们也就自然清楚这个问题的答案了。

## Swift Playground

说到iOS代码动态极速调试的工具，你首先能想到的估计就是 Playground。它是 Xcode 里集成的一个能够快速、实时调试程序的工具，可以实现所见即所得的效果，如下图所示：

<img src="https://static001.geekbang.org/resource/image/46/01/46007bcd100b7b23edccd46b760e5b01.png" alt="">

可以看到，任何的代码修改都能够实时地在右侧反馈出来。

## Flutter Hot Reload

Flutter 是 Google 开发的一个跨平台开发框架，调试也是快速实时的。官方的效果动画如下：

<img src="https://static001.geekbang.org/resource/image/6d/1d/6d8b83e4e063dbccf279adfe2b66dd1d.gif" alt="">

可以看到，在 Flutter 编辑器中修改文字 clicked 为 tapped 后点击 reload，模拟器中的文字立刻就改变了，程序没有重启。同样地，修改按钮图标也会立刻生效。

接下来，我们先看看 Flutter 是怎么实现实时编译的。

Flutter 会在点击 reload 时去查看自上次编译以后改动过的代码，重新编译涉及到的代码库，还包括主库，以及主库的相关联库。所有这些重新编译过的库都会转换成内核文件发到 Dart VM 里，Dart VM 会重新加载新的内核文件，加载后会让 Flutter framework 触发所有的Widgets 和 Render Objects 进行重建、重布局、重绘。

Flutter 为了能够支持跨平台开发，使用了自研的 Dart 语言配合在 App 内集成 Dart VM 的方式运行 Flutter 程序。目前 Flutter 还没有达到 Cocoa 框架那样的普及程度，所以如果你不是使用 Flutter 来开发 iOS 程序的话，想要达到极速调试应该要怎么做呢？

# Injection for Xcode

所幸的是，John Holdsworth 开发了一个叫作 Injection 的工具可以动态地将 Swift 或 Objective-C 的代码在已运行的程序中执行，以加快调试速度，同时保证程序不用重启。John Holdsworth 也提供了动画演示效果，如下：

<img src="https://static001.geekbang.org/resource/image/a2/13/a239763b1a5c7226e5ee8d7481285a13.gif" alt="">

作者已经开源了这个工具，地址是[https://github.com/johnno1962/InjectionIII](https://github.com/johnno1962/InjectionIII)  。使用方式就是 clone 下代码，构建 InjectionPluginLite/InjectionPlugin.xcodeproj ；删除方式是，在终端里运行下面这行代码：

```
rm -rf ~/Library/Application\ Support/Developer/Shared/Xcode/Plug-ins/InjectionPlugin.xcplugin

```

构建完成后，我们就可以编译项目。这时添加一个新的方法：

```
- (void)injected
{
    NSLog(@&quot;I've been injected: %@&quot;, self);
}

```

然后在这个方法中添加一个断点，按下 ctrl + = ，接下来你会发现程序运行时会停到断点处，这样你的代码就成功地被运行中的 App 执行了。那么，**Injection 是怎么做到的呢？**

Injection 会监听源代码文件的变化，如果文件被改动了，Injection Server 就会执行 rebuildClass 重新进行编译、打包成动态库，也就是 .dylib 文件。编译、打包成动态库后使用 writeSting 方法通过 Socket 通知运行的 App。writeString 的代码如下：

```
- (BOOL)writeString:(NSString *)string {
    const char *utf8 = string.UTF8String;
    uint32_t length = (uint32_t)strlen(utf8);
    if (write(clientSocket, &amp;length, sizeof length) != sizeof length ||
        write(clientSocket, utf8, length) != length)
        return FALSE;
    return TRUE;
}

```

Server 会在后台发送和监听 Socket 消息，实现逻辑在 `InjectionServer.mm` 的 runInBackground 方法里。Client 也会开启一个后台去发送和监听 Socket 消息，实现逻辑在 `InjectionClient.mm`里的 runInBackground 方法里。

Client 接收到消息后会调用 inject(tmpfile: String) 方法，运行时进行类的动态替换。inject(tmpfile: String) 方法的具体实现代码，你可以点击[这个链接](https://github.com/johnno1962/InjectionIII/blob/master/InjectionBundle/SwiftInjection.swift)查看。

inject(tmpfile: String) 方法的代码大部分都是做新类动态替换旧类。inject(tmpfile: String) 的入参 tmpfile 是动态库的文件路径，那么这个动态库是如何加载到可执行文件里的呢？具体的实现在inject(tmpfile: String) 方法开始里，如下：

```
let newClasses = try SwiftEval.instance.loadAndInject(tmpfile: tmpfile)

```

你先看下 SwiftEval.instance.loadAndInject(tmpfile: tmpfile) 这个方法的代码实现：

```
@objc func loadAndInject(tmpfile: String, oldClass: AnyClass? = nil) throws -&gt; [AnyClass] {

    print(&quot;???? Loading .dylib - Ignore any duplicate class warning...&quot;)
    // load patched .dylib into process with new version of class
    guard let dl = dlopen(&quot;\(tmpfile).dylib&quot;, RTLD_NOW) else {
        throw evalError(&quot;dlopen() error: \(String(cString: dlerror()))&quot;)
    }
    print(&quot;???? Loaded .dylib - Ignore any duplicate class warning...&quot;)

    if oldClass != nil {
        // find patched version of class using symbol for existing

        var info = Dl_info()
        guard dladdr(unsafeBitCast(oldClass, to: UnsafeRawPointer.self), &amp;info) != 0 else {
            throw evalError(&quot;Could not locate class symbol&quot;)
        }

        debug(String(cString: info.dli_sname))
        guard let newSymbol = dlsym(dl, info.dli_sname) else {
            throw evalError(&quot;Could not locate newly loaded class symbol&quot;)
        }

        return [unsafeBitCast(newSymbol, to: AnyClass.self)]
    }
    else {
        // grep out symbols for classes being injected from object file

        try injectGenerics(tmpfile: tmpfile, handle: dl)

        guard shell(command: &quot;&quot;&quot;
            \(xcodeDev)/Toolchains/XcodeDefault.xctoolchain/usr/bin/nm \(tmpfile).o | grep -E ' S _OBJC_CLASS_\\$_| _(_T0|\\$S).*CN$' | awk '{print $3}' &gt;\(tmpfile).classes
            &quot;&quot;&quot;) else {
            throw evalError(&quot;Could not list class symbols&quot;)
        }
        guard var symbols = (try? String(contentsOfFile: &quot;\(tmpfile).classes&quot;))?.components(separatedBy: &quot;\n&quot;) else {
            throw evalError(&quot;Could not load class symbol list&quot;)
        }
        symbols.removeLast()

        return Set(symbols.flatMap { dlsym(dl, String($0.dropFirst())) }).map { unsafeBitCast($0, to: AnyClass.self) }

```

在这段代码中，你是不是看到你所熟悉的动态库加载函数 dlopen 了呢？

```
guard let dl = dlopen(&quot;\(tmpfile).dylib&quot;, RTLD_NOW) else {
    throw evalError(&quot;dlopen() error: \(String(cString: dlerror()))&quot;)
}

```

如上代码所示，dlopen 会把 tmpfile 动态库文件载入运行的 App 里，返回指针 dl。接下来，dlsym 会得到 tmpfile 动态库的符号地址，然后就可以处理类的替换工作了。dlsym 调用对应代码如下：

```
guard let newSymbol = dlsym(dl, info.dli_sname) else {
    throw evalError(&quot;Could not locate newly loaded class symbol&quot;)
}

```

当类的方法都被替换后，我们就可以开始重新绘制界面了。整个过程无需重新编译和重启 App，至此使用动态库方式极速调试的目的就达成了。

我把Injection的工作原理用一张图表示了出来，如下所示：

<img src="https://static001.geekbang.org/resource/image/4f/c9/4f49ea2047d2dd2d5c4646b0ba55b8c9.png" alt="">

## 小结

今天这篇文章，我和你详细分享了动态库链接器的一个非常实用的应用场景：如何使用动态库加载方式进行极速调试。由此我们可以看出，类似链接器这样的底层知识是非常重要的。

当然了，这只是一个场景，还有更多的场景等待着我们去发掘。比如把 Injection 技术扩展开想，每当你修改了另一个人负责的代码就给那个人发条消息，同时将修改的代码编译、打包成动态库直接让对方看到修改的情况，这样不仅是提高了自己的效率，还提高了整个团队的沟通效率。怎么样？是不是有种想立刻尝试的感觉，心动不如行动，动手写起来吧。

所以，打好了底层知识的基础以后，我们才可以利用它们去提高开发效率，为用户提供更稳定、性能更好的 App 。

今天这篇文章最后，我留给你的一个小作业是，思考一下底层知识还有哪些运用场景，并在评论区分享出来吧。

感谢你的收听，欢迎你在评论区给我留言分享你的观点，也欢迎把它分享给更多的朋友一起阅读。

<img src="https://static001.geekbang.org/resource/image/6c/89/6c844d233e74aec08417be65e4ef1d89.jpg" alt="">
