背景
John Holdsworth 开发了一个支持 OC、Swift 以及 Swift 和 OC 混编项目的 UI热重载工具 Injection
可以动态地将 Swift 或 Objective-C 的代码在已运行的程序中执行,修改完UI直接com+s,不用重新编译运行就能看到UI效果。可以用来提高调试代码的速度。
动画演示效果如下:
Injection使用方法
1、安装Injection
github下载最新release版本,或者AppStore下载安装即可,推荐github下载安装,github更新比AppStore更新快。如果你的项目使用混编OC时,强烈建议使用github的releases版本
2、项目配置
1、安装后,打开InjectionIII,选择Open Project,选择你的项目目录
2、 在AppDelegate的DidFinishLaunchingWithOptions配置InjectionIII的路径
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
#ifdef DEBUG
[[NSBundle bundleWithPath:@"/Applications/InjectionIII.app/Contents/Resources/iOSInjection.bundle"] load];
#endif
return YES;
}
3、 在需要动态调试的页面控制器中写上injected
方法, 把需要操作的UI方法添加到injected
中执行, 如果想让全部的控制器都能使用, 直接添加到BaseViewController
// Objective-C:
- (void)injected {
#ifdef DEBUG
NSLog(@"I've been injected: %@", self);
[self viewDidLoad];
#endif
}
// Swift
@objc func injected() {
#if DEBUG
print("I've been injected: \(self)")
self.viewDidLoad()
#endif
}
4、 重新编译项目, 控制台可以看到
**💉 InjectionIII connected /Users/***/Desktop/***/**/***.xcworkspace**
**💉 Watching files under /Users/***/Desktop/****
// 下面的只是警告, 作者在Issue中已经解释, 不耽误正常使用.
**💉 💉 ⚠️ Your project file seems to be in the Desktop or Documents folder and may prevent InjectionIII working as it has special permissions.**
5、 修改完UI, 直接cmd + S
就能看到效果, 部分页面可能耗时比较久或无法使用, 正常页面均能使用
iOS原生项目热重载的原理
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, &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) 方法的代码大部分都是做新类动态替换旧类。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 -> [AnyClass] {
print("???? Loading .dylib - Ignore any duplicate class warning...")
// load patched .dylib into process with new version of class
guard let dl = dlopen("\(tmpfile).dylib", RTLD_NOW) else {
throw evalError("dlopen() error: \(String(cString: dlerror()))")
}
print("???? Loaded .dylib - Ignore any duplicate class warning...")
if oldClass != nil {
// find patched version of class using symbol for existing
var info = Dl_info()
guard dladdr(unsafeBitCast(oldClass, to: UnsafeRawPointer.self), &info) != 0 else {
throw evalError("Could not locate class symbol")
}
debug(String(cString: info.dli_sname))
guard let newSymbol = dlsym(dl, info.dli_sname) else {
throw evalError("Could not locate newly loaded class symbol")
}
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: """
\(xcodeDev)/Toolchains/XcodeDefault.xctoolchain/usr/bin/nm \(tmpfile).o | grep -E ' S _OBJC_CLASS_\\$_| _(_T0|\\$S).*CN$' | awk '{print $3}' >\(tmpfile).classes
""") else {
throw evalError("Could not list class symbols")
}
guard var symbols = (try? String(contentsOfFile: "\(tmpfile).classes"))?.components(separatedBy: "\n") else {
throw evalError("Could not load class symbol list")
}
symbols.removeLast()
return Set(symbols.flatMap { dlsym(dl, String($0.dropFirst())) }).map { unsafeBitCast($0, to: AnyClass.self) }
熟悉的动态库加载函数 dlopen:
guard let dl = dlopen("\(tmpfile).dylib", RTLD_NOW) else {
throw evalError("dlopen() error: \(String(cString: dlerror()))")
}
如上代码所示,dlopen 会把 tmpfile 动态库文件载入运行的 App 里,返回指针 dl。接下来,dlsym 会得到 tmpfile 动态库的符号地址,然后就可以处理类的替换工作了。dlsym 调用对应代码如下:
guard let newSymbol = dlsym(dl, info.dli_sname) else {
throw evalError("Could not locate newly loaded class symbol")
}
当类的方法都被替换后,我们就可以开始重新绘制界面了。整个过程无需重新编译和重启 App,至此使用动态库方式极速调试的目的就达成了。 Injection 的工作原理如下所示:
参考:
06 | App 如何通过注入动态库的方式实现极速编译调试?-极客时间
GitHub - johnno1962/InjectionIII: Re-write of Injection for Xcode in (mostly) Swift
文章的确不错啊https://www.cscnn.com/
游戏界失和谐,求复古版本!:https://501h.com/jingpin/1899.html
《助产士》剧情片高清在线免费观看:https://www.jgz518.com/xingkong/65456.html