在介绍热重载黑魔法前,先再次安利一波John Holdsworth的这个成吨提高iOS开发效率的工具Injection,Appstore可以直接免费下载,最新一版是5月17日更新。废话不多说,先看效果:
没错几乎是Xcode这边改代码,模拟器那边瞬间见效果,尤其是我们的产品迭代几年后,每次编译都巨慢,是不是设计妹子让你微调UI时你又可以秀的飞起了,从此摆脱coding五分钟编译2小时的囧境。
使用方法
虽然今天的主线并非工具的安装使用,我还是简单介绍下,Appstore下好InjectionIII后,在我们的项目中applicationDidFinishLaunching方法加入如下代码
然后在任意继承自OC的类中加入如下代码,在我们修改了对应文件按下COMMAND + S后,Injjection就开始编译修改过的文件为动态库,然后我们在Injected方法内做UI reload工作,即可重绘UI。
接下来是这次分享的重点,Injection到底如何实现热重载,采用了什么样的黑魔法?
热重载原理
组内的同学有在玩这个玩具,下午茶歇,大家开始讨论这个工具如何实现,出于好奇,我抽空仔细分析了下,在分析这个玩具的实现原理时,我竟然没想到直接对着源码看(作者真是好人,这么香的软件,竟然免费而且开源,就算每次下载1元,也能致富吧),而是采用逆向分析的技术,后来才发现github有源码,对着源码看了下我之前的分析的八九不离十。
先带大家把整个热重载过程理一遍,首先我们修改一个文件,Injection工具会通过File Watcher监听观察文件改动,然后将改动的文件编译,打包,这时候Injection工具会给我们的App发个消息,兄弟我这边ready了,你更新下代码;我们的App收到消息后更新代码后再给Injection个反馈,好的大佬,代码已经更新,UI也刷新了;Injection收到反馈后,工具会变绿,完美的闭环式沟通。注意这里的过程,App要收消息,那么必须要有对应的代码,如何实现?App的代码如何更新?
我们知道如果要让既有App,执行自己的代码可以通过注入动态库,静态的注入可以使用optool工具修改MachO的Load Commands然后重签,运行时可以使用dlopen或者Bundle(path: "**.bundle").load()加载,作者也正是采用这种方式,文中第一图,工具初始化,就是为了实现注入动态库。
这里有一点需要说明一下,模拟器下iOS可加载Mac任意文件,不存在沙盒的说法,而真机设备如果加载动态库,只能加载App.content目录下的,换句话说,这个工具只支持模拟器,为了验证我的说法使用lldb image list -o -f
查看执行这句代码后,动态库列表中多了iOSInjection,就算我们不看源码,用脚想都知道,这个库会负责和mac Injection通信。
继续跟着老峰的思路,我研究时是用Hopper,反编译iOSInjection,这里直接看源码,可见iOSInjection确实有负责即时通信的代码SimpleSocket,这里贴部分代码有兴趣的同学可去文尾链接读源码
建立连接后Injection就可以和我们的App愉快的沟通了,接下来我们看,Injection如何实现代码更新,我们去修改下View的backgroundColor然后保存,这时控制台,可见如下日志:
*** Compiling /Users/apple/Work Project/modules/Library/LibraryManagerController.swift ***
Loading .dylib - Ignore any duplicate class warning...
objc[70014]: Class _DraftLibraryManagerController is implemented in both /Users//Library/Developer/CoreSimulator/Devices/DC5118EC-D835/data/Containers/Bundle/Application/EB9E4DA7/Collector.app/Collector (0x10e31c530) and /Users/Library/Containers/com.johnholdsworth.InjectionIII/Data/eval104.dylib (0x13b023fa8). One of the two will be used. Which one is undefined.
通过上边日志 可知 :
保存后重新编译了我们修改的类文件LibraryManagerController.swift
我们编译了一个同名的类,并且忽略了编译器duplicate class警告
在eval101.dylib,Collector中有2份一样的类,只是方法实现不同
这里我们可以看到修改后的文件被编译为了eval101.dylib动态库,当UI更新后使用lldb image list -o -f 可见修改后的类文件确实以动态库的方式注入了我们的App内。
通过阅读Mac端 Injection源码可知观察到文件修改后InjectionServer会执行rebuildClass,成功编译后生成eval101.dylib,然后通过writestring给我们的App发"INJECT"消息,通知App更新代码
接下来看我们的App收到消息后做了什么,可以看到收到Inject后调了SwiftInjection 的inject(tmpfile: String)方法
我们看下SwiftInjection 的inject(tmpfile: String)方法源码,可知,通过
SwiftEval.instance.loadAndInject方法dlopen加载eval101.dylib动态库,这样就把修改后的代码注入了我们的App,然后通过OC runtime 的class_replaceMethod把整个类的实现方法都替换了,然后再调SwiftInjected.injected我们的类收到消息开始重绘UI,然后给给Mac Injection发反馈消息。
好了,整个Injection整个实现流程大概就这样,当然里边还有很多细节的东西,需要读者们自己探索研究发现。iOS热重载原理并不复杂,了解掌握这些原理或许我们可以自己做些有趣的玩具。
John Holdsworth
最后介绍下Injection的作者John Holdsworth,1962年出生的,1984年读大学,现在56岁,大概有20多年的开发经验,2008年开始学iOS也就是46岁开始学iOS的,你还好意思以老为借口拒绝学新知识么,他的年龄和我们父辈年龄差不太多,附英文介绍及社区活跃度,太强了每天都在Coding,真心佩服这样的大佬,致敬!
AppleOS Software Engineer
From assembly language though scripting languages, Java(Script) and SQL to Objective-C(++) and Swift, John has the answer to your low level engineering and performance and tuning requirements. Available for remote work in the London or US timezone John has been an independent iOS and MacOS dev since 2008.