• 主页
  • 随笔
  • 技术笔记
  • 全部文章
标签 友链 关于我

  • 主页
  • 随笔
  • 技术笔记
  • 全部文章

UI调试利器-Injection热重载

阅读数:次 2021-09-22
字数统计: 1.7k字   |   阅读时长≈ 7分

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
2
3
#if DEBUG
[[NSBundle bundleWithPath:@"/Applications/InjectionIII.app/Contents/Resources/iOSInjection.bundle"] load];
#endif

4.实现-(void)injected方法

1
2
3
4
- (void)injected{
NSLog(@"I've been injected: %@", self);
[self viewDidLoad];
}

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
2
3
4
5
6
7
8
- (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) 方法开始里,如下:

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
@objc func loadAndInject(tmpfile: String, oldClass: AnyClass? = nil) throws -> [AnyClass] {

print("💉 Loading .dylib ...")
// load patched .dylib into process with new version of class
guard let dl = dlopen("\(tmpfile).dylib", RTLD_NOW) else {
let error = String(cString: dlerror())
if error.contains("___llvm_profile_runtime") {
print("💉 Loading .dylib has failed, try turning off collection of test coverage in your scheme")
}
throw evalError("dlopen() error: \(error)")
}
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|\\$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: (error)”)
}
如上代码所示,dlopen 会把 tmpfile 动态库文件载入运行的 App 里,返回指针 dl。接下来, dlsym 会得到 tmpfile 动态库的符号地址,然后就可以处理类的替换工作了。dlsym 调用对应代 码如下

1
2
3
guard let newSymbol = dlsym(dl, info.dli_sname) else {
throw evalError("Could not locate newly loaded class symbol")
}

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

Injection 的工作原理图如下所示:

  • 本文作者: Grx
  • 本文链接: https://ruixiaoguo.github.io/Grx.github.io/Grx.github.io/2021/09/22/UI调试利器-Injection热重载/
  • 版权声明: 本博客所有文章除特别声明外,均采用 MIT 许可协议。转载请注明出处!
  • OC

扫一扫,分享到微信

利用 RunLoop 原理监控线程卡顿
iOS MMAP初识
  1. 1. 一、前言
    1. 1.0.0.1. iOS 原生代码的编译调试,都是通过一遍又一遍地编译重启 App 来进行的。所以,项目代码量越大,编译时间就越长。虽然我们可以通过将部分代码先编译成二进制集成到工程里,来避免每次都全量编译来加快编译速度,但即使这样,每次编译都还是需要重启 App,需要再走一遍调试流程。
  • 2. 二、使用方法:
    1. 2.1. 1.下载工具:
      1. 2.1.0.0.1. 1.App Store下载Injectionlll。
      2. 2.1.0.0.2. 2.Github下载https://github.com/johnno1962/injectionforxcode
  • 2.2. 2.选择监听文件目录
    1. 2.2.0.1. 打开Injectionlll工具,选择需要监听的整个项目工程(不是.xcodeproj文件)
  • 2.3. 3.项目工程配置:
    1. 2.3.0.1. 在 application:DidFinishLaunching: 方法中加入如下代码:
  • 2.4. 4.实现-(void)injected方法
  • 2.5. 5.运行效果
    1. 2.5.0.1. 在- (void)injected中编写UI代码,command+s 保存切执行代码,Injjection就开始编译修改过的文件为动态库,然后重新进入页面刷新,即可重绘UI。
  • 2.6. 6.需注意的问题
  • 2.7. 7.没有看到效果的问题的总结
  • 3. 三、InjectionIII重载原理
    1. 3.1. 1、流程梳理
    2. 3.2. 2、具体实现
  • © 2014-2024 Grx
    GitHub:hexo-theme-yilia-plus by Litten
    本站总访问量次 | 本站访客数人
    • 标签
    • 友链
    • 关于我

    tag:

    • life
    • OC
    • Google
    • Fastlane
    • Flutter
    • hexo
    • 智能家居
    • Apple Watch
    • 逆向
    • Lottie
    • PHP
    • cocos2d
    • Mac
    • MonkeyKing
    • RN
    • Swift
    • RAC
    • WKWebView
    • WebView
    • Xcode
    • xcode
    • ios
    • Android
    • appledoc
    • MMKV
    • LLVM
    • FreamWork






      
      

    • 唐巧的博客
    • 王巍(喵神)OneVsDen
    • 阿里“念纪“
    • 滴滴-戴铭
    • 郭曜源(ibireme)
    • 阿里”南栀倾寒“
    • 蘑菇街李忠
    • 码农人生
    • 玉令天下
    • bang
    • Ian的博客
    这里是Grx的个人博客:
    iOS开发工程师一枚
    联系方式:
    QQ:1217255509
    Email:grx0917@sina.com
    知识管理,时间管理,自我管理,架构即未来
    欢迎技术交流!