iOS InjectionIII工具的使用及重载原理

一、InjectionIII使用

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

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

那么问题来了,原生代码怎样才能够实现动态极速调试,以此来大幅提高编译调试速度呢? 所幸的是,John Holdsworth 开发了一个叫作 Injection 的工具可以动态地将 Swift 或 Objective-C 的代码在已运行的程序中执行,以加快调试速度,同时保证程序不用重启。OC运行的效果图如下所示:

使用示例.gif

作者已经开源了这个工具,地址是:https://github.com/johnno1962/InjectionIII ,也可以从 Mac App Store 获得。这里分享下Mac App Store下载安装使用的具体方法。

注意
目前只支持模拟器运行

1、Mac App Store下载

这个是 Mac 上的一款 App,可以在 Mac App Store 中搜索 Injection,那款免费的 App 就是,现在已经更新到第三个版本,点击安装

Mac App Store下载.png

2、安装成功,打开应用

InjectionII.app期望在路径中找到您当前的Xcode /Applications/Xcode.app,适用于Swift和Objective-C可以与AppCode一起使用,但您需要首先使用Xcode构建项目,以提供用于确定如何编译项目的日志。

启动app.png

3、AppDelegate中注入代码

InjectionII.app 需要知道您当前的Xcode路径 /Applications/Xcode.app,适用于SwiftObjective-C可以与AppCode一起使用,但您需要首先使用Xcode构建项目,以提供用于确定如何编译项目的日志。

要使用注入,下载并运行应用程序,您只需将以下内容之一添加到应用程序代理中即可 applicationDidFinishLaunching:

#if DEBUG
// or oc
[[NSBundle bundleWithPath:@"/Applications/InjectionIII.app/Contents/Resources/iOSInjection.bundle"] load];
// or switf
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
AppDelegate中注入代码.png

4、选择监听文件目录

首次运行时,会弹出弹框,选择监听文件改变路径


选择监听文件目录.png

选择后,控制台会打印类似日志

💉 Injection connected, watching /Users/zjh48/Desktop/ZJHInjectionIIIDemo/**

5、实现- (void)injected方法

在对应的VC控制器中实现 - (void)injected 编写代码,写完后,command+s 保存切执行代码,Injjection就开始编译修改过的文件为动态库,然后我们在Injected方法内做UI reload工作,即可重绘UI。

实现injected方法.png

6、没有看到效果的问题的总结

  • 确认 Injection 监听的目录和 Xcode 项目目录是否一致。
  • 再看下有没有保存成功,也就是针筒的颜色由绿色变成红色。
  • 确认上面那句话有没有打印,也就是说有没有真的运行这个工具。
  • 如果修改的是 cell / item 上面的内容,需要上下滚动才能看到效果。
  • 如果修改的是一个普通页面的内容,最好是退出这个页面,再进入这个页面。
  • 确认 Xcode 的版本和启动时添加的代码是否匹配,Xcode10 需要 iOSInjection10.bundle 才能生效

二、InjectionIII重载原理

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 的代码如下:

- (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 ...")
        // 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 调用对应代 码如下

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

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

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

Injection 的工作原理示意图.png



参考链接:
戴铭·iOS开发高手课
InjectionIII 成吨的提高iOS开发效率
InjectionIII:iOS开发必备效率神器-所见即所得
iOS 使用 InjectionIII 注入动态库实现快速调试
Injection:iOS热重载背后的黑魔法

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容