John Holdsworth 开发了一个叫作 Injection 的工具可以动态地将 Swift 或Objective-C 的代码在已运行的程序中执行,以加快调试速度,同时保证程序不用重启。
一、前言
iOS 原生代码的编译调试,都是通过一遍又一遍地编译重启 App 来进行的。所以,项目代码量越大,编译时间就越长。虽然我们可以通过将部分代码先编译成二进制集成到工程里,来避免每次都全量编译来加快编译速度,但即使这样,每次编译都还是需要重启 App,需要再走一遍调试流程。
二、使用方法:
1.下载工具:
1.App Store下载Injectionlll。
2.Github下载https://github.com/johnno1962/injectionforxcode
2.选择监听文件目录
打开Injectionlll工具,选择需要监听的整个项目工程(不是.xcodeproj文件)
3.项目工程配置:
在 application:DidFinishLaunching: 方法中加入如下代码:
1 | #if DEBUG |
4.实现-(void)injected方法
1 | - (void)injected{ |
5.运行效果
在- (void)injected中编写UI代码,command+s 保存切执行代码,Injjection就开始编译修改过的文件为动态库,然后重新进入页面刷新,即可重绘UI。
6.需注意的问题
1.真机,cmd+s无效,只支持模拟器调试。
2.在swift中方法injected前要加@objc。
3.frame布局生效,snapkit约束不生效?更改约束的时候没有清空之前的约束,会存在两个约束,造成约束冲突,所以先清空。若是不清空的话需要重新进入页面才能生效。
7.没有看到效果的问题的总结
- 确认 Injection 监听的目录和 Xcode 项目目录是否一致。
- 再看下有没有保存成功,也就是针筒的颜色由绿色变成红色。
- 确认上面那句话有没有打印,也就是说有没有真的运行这个工具。
- 如果修改的是 cell / item 上面的内容,需要上下滚动才能看到效果。
- 如果修改的是一个普通页面的内容,最好是退出这个页面,再进入这个页面。
- 确认 Xcode 的版本和启动时添加的代码是否匹配,Xcode10 需要 iOSInjection10.bundle 才能生效
三、InjectionIII重载原理
启动了一个mac server监控工程目录,cmd+s的时候,获取injected方法内的代码,生成新的dylib文件,替换旧的动态链接库,实现即时刷新UI。
1、流程梳理
首先我们修改一个文件,Injection工具会通过File Watcher监听观察文件改动,然后将改动的文件编译,打包,这时候Injection工具会给我们的App发个消息:“兄弟我这边ready了,你更新下代码”;我们的App收到消息后更新代码后再给Injection个反馈:“好的大佬,代码已经更新,UI也刷新了”;Injection收到反馈后,工具会变绿,完美的闭环式沟通。注意这里的过程,App要收消息,那么必须要有对应的代码,如何实现?App的代码如何更新?
我们知道如果要让既有App,执行自己的代码可以通过注入动态库,静态的注入可以使用optool工具修改MachO的Load Commands然后重签,运行时可以使用dlopen或者Bundle(path: “**.bundle”).load()加载,作者也正是采用这种方式,文中AppDelegate注入代码,工具初始化,就是为了实现注入动态库。
这里有一点需要说明一下,模拟器下iOS可加载Mac任意文件,不存在沙盒的说法,而真机设备如果加载动态库,只能加载App.content目录下的,换句话说,这个工具只支持模拟器。
2、具体实现
Injection 会监听源代码文件的变化,如果文件被改动了,Injection Server 就会执行 rebuildClass 重新进行编译、打包成动态库,也就是 .dylib 文件。编译、打包成动态库后使用 writeSting 方法通过 Socket 通知运行的 App。writeString 的代码如下:
1 | - (BOOL)writeString:(NSString *)string { |
Server 会在后台发送和监听 Socket 消息,实现逻辑在 InjectionServer.mm 的 runInBackground 方法里。Client 也会开启一个后台去发送和监听 Socket 消息,实现逻辑在 InjectionClient.mm 里的 runInBackground 方法里。
Client 接收到消息后会调用 inject(tmpfile: String) 方法,运行时进行类的动态替换。inject(tmpfile: String) 方法的代码大部分都是做新类动态替换旧类。inject(tmpfile: String) 的 入参 tmpfile 是动态库的文件路径,那么这个动态库是如何加载到可执行文件里的呢?具体的实 现在 inject(tmpfile: String) 方法开始里,如下:
1 | let newClasses = try SwiftEval.instance.loadAndInject(tmpfile: tmpfile) |
先看下 SwiftEval.instance.loadAndInject(tmpfile: tmpfile) 这个方法的代码实现:
1 | @objc func loadAndInject(tmpfile: String, oldClass: AnyClass? = nil) throws -> [AnyClass] { |
在这段代码中,是不是看到你所熟悉的动态库加载函数 dlopen 了呢?
guard let dl = dlopen(“(tmpfile).dylib”, RTLD_NOW) else {
throw evalError(“dlopen() error: (error)”)
}
如上代码所示,dlopen 会把 tmpfile 动态库文件载入运行的 App 里,返回指针 dl。接下来, dlsym 会得到 tmpfile 动态库的符号地址,然后就可以处理类的替换工作了。dlsym 调用对应代 码如下
1 | guard let newSymbol = dlsym(dl, info.dli_sname) else { |
当类的方法都被替换后,我们就可以开始重新绘制界面了。整个过程无需重新编译和重启 App, 至此使用动态库方式极速调试的目的就达成了。
Injection 的工作原理图如下所示:
- 本文作者: Grx
- 本文链接: https://ruixiaoguo.github.io/Grx.github.io/Grx.github.io/2021/09/22/UI调试利器-Injection热重载/
- 版权声明: 本博客所有文章除特别声明外,均采用 MIT 许可协议。转载请注明出处!