一、InjectionIII使用
iOS 原生代碼的編譯調(diào)試,都是通過(guò)一遍又一遍地編譯重啟 App 來(lái)進(jìn)行的必尼。所以,項(xiàng)目代碼量越大篡撵,編譯時(shí)間就越長(zhǎng)判莉。雖然我們可以通過(guò)將部分代碼先編譯成二進(jìn)制集成到工程里,來(lái)避免每次都全量編譯來(lái)加快編譯速度育谬,但即使這樣券盅,每次編譯都還是需要重啟 App,需要再走一遍調(diào)試流程膛檀。
對(duì)于開發(fā)者來(lái)說(shuō)锰镀,提高編譯調(diào)試的速度就是提高生產(chǎn)效率娘侍。試想一下,如果上線前一天突然發(fā)現(xiàn)了一個(gè)嚴(yán)重的 bug泳炉,每次編譯調(diào)試都要耗費(fèi)幾十分鐘憾筏,結(jié)果這一天的黃金時(shí)間,一晃就過(guò)去了花鹅。到最后氧腰,可能就是上線時(shí)間被延誤。這個(gè)責(zé)任可不輕啊刨肃。
那么問(wèn)題來(lái)了古拴,原生代碼怎樣才能夠?qū)崿F(xiàn)動(dòng)態(tài)極速調(diào)試,以此來(lái)大幅提高編譯調(diào)試速度呢? 所幸的是之景,John Holdsworth 開發(fā)了一個(gè)叫作 Injection 的工具可以動(dòng)態(tài)地將 Swift 或 Objective-C 的代碼在已運(yùn)行的程序中執(zhí)行斤富,以加快調(diào)試速度,同時(shí)保證程序不用重啟锻狗。OC運(yùn)行的效果圖如下所示:
作者已經(jīng)開源了這個(gè)工具,地址是:https://github.com/johnno1962/InjectionIII 焕参,也可以從 Mac App Store 獲得轻纪。這里分享下Mac App Store下載安裝使用的具體方法。
注意
目前只支持模擬器運(yùn)行
1叠纷、Mac App Store下載
這個(gè)是 Mac 上的一款 App刻帚,可以在 Mac App Store 中搜索 Injection,那款免費(fèi)的 App 就是涩嚣,現(xiàn)在已經(jīng)更新到第三個(gè)版本崇众,點(diǎn)擊安裝
2、安裝成功航厚,打開應(yīng)用
InjectionII.app期望在路徑中找到您當(dāng)前的Xcode /Applications/Xcode.app顷歌,適用于Swift和Objective-C可以與AppCode一起使用,但您需要首先使用Xcode構(gòu)建項(xiàng)目幔睬,以提供用于確定如何編譯項(xiàng)目的日志眯漩。
3、AppDelegate中注入代碼
InjectionII.app
需要知道您當(dāng)前的Xcode路徑 /Applications/Xcode.app
麻顶,適用于Swift
和Objective-C
可以與AppCode
一起使用赦抖,但您需要首先使用Xcode構(gòu)建項(xiàng)目,以提供用于確定如何編譯項(xiàng)目的日志辅肾。
要使用注入队萤,下載并運(yùn)行應(yīng)用程序,您只需將以下內(nèi)容之一添加到應(yīng)用程序代理中即可 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
4矫钓、選擇監(jiān)聽文件目錄
首次運(yùn)行時(shí)要尔,會(huì)彈出彈框舍杜,選擇監(jiān)聽文件改變路徑
選擇后,控制臺(tái)會(huì)打印類似日志
?? Injection connected, watching /Users/zjh48/Desktop/ZJHInjectionIIIDemo/**
5盈电、實(shí)現(xiàn)- (void)injected
方法
在對(duì)應(yīng)的VC控制器中實(shí)現(xiàn) - (void)injected
編寫代碼蝴簇,寫完后,command+s
保存切執(zhí)行代碼匆帚,Injjection就開始編譯修改過(guò)的文件為動(dòng)態(tài)庫(kù)熬词,然后我們?cè)贗njected方法內(nèi)做UI reload工作,即可重繪UI吸重。
6互拾、沒(méi)有看到效果的問(wèn)題的總結(jié)
- 確認(rèn) Injection 監(jiān)聽的目錄和 Xcode 項(xiàng)目目錄是否一致。
- 再看下有沒(méi)有保存成功嚎幸,也就是針筒的顏色由綠色變成紅色颜矿。
- 確認(rèn)上面那句話有沒(méi)有打印,也就是說(shuō)有沒(méi)有真的運(yùn)行這個(gè)工具嫉晶。
- 如果修改的是 cell / item 上面的內(nèi)容骑疆,需要上下滾動(dòng)才能看到效果。
- 如果修改的是一個(gè)普通頁(yè)面的內(nèi)容替废,最好是退出這個(gè)頁(yè)面箍铭,再進(jìn)入這個(gè)頁(yè)面。
- 確認(rèn) Xcode 的版本和啟動(dòng)時(shí)添加的代碼是否匹配椎镣,Xcode10 需要 iOSInjection10.bundle 才能生效
二诈火、InjectionIII重載原理
1、流程梳理
首先我們修改一個(gè)文件状答,Injection工具會(huì)通過(guò)File Watcher監(jiān)聽觀察文件改動(dòng)冷守,然后將改動(dòng)的文件編譯,打包惊科,這時(shí)候Injection工具會(huì)給我們的App發(fā)個(gè)消息:“兄弟我這邊ready了拍摇,你更新下代碼”;我們的App收到消息后更新代碼后再給Injection個(gè)反饋:“好的大佬译断,代碼已經(jīng)更新授翻,UI也刷新了”;Injection收到反饋后孙咪,工具會(huì)變綠堪唐,完美的閉環(huán)式溝通。注意這里的過(guò)程翎蹈,App要收消息淮菠,那么必須要有對(duì)應(yīng)的代碼,如何實(shí)現(xiàn)荤堪?App的代碼如何更新合陵?
我們知道如果要讓既有App枢赔,執(zhí)行自己的代碼可以通過(guò)注入動(dòng)態(tài)庫(kù),靜態(tài)的注入可以使用optool工具修改MachO的Load Commands然后重簽拥知,運(yùn)行時(shí)可以使用dlopen或者Bundle(path: "**.bundle").load()加載踏拜,作者也正是采用這種方式,文中AppDelegate注入代碼低剔,工具初始化速梗,就是為了實(shí)現(xiàn)注入動(dòng)態(tài)庫(kù)。
這里有一點(diǎn)需要說(shuō)明一下襟齿,模擬器下iOS可加載Mac任意文件姻锁,不存在沙盒的說(shuō)法,而真機(jī)設(shè)備如果加載動(dòng)態(tài)庫(kù)猜欺,只能加載App.content目錄下的位隶,換句話說(shuō),這個(gè)工具只支持模擬器开皿。
2涧黄、具體實(shí)現(xiàn)
Injection 會(huì)監(jiān)聽源代碼文件的變化,如果文件被改動(dòng)了赋荆,Injection Server 就會(huì)執(zhí)行 rebuildClass 重新進(jìn)行編譯弓熏、打包成動(dòng)態(tài)庫(kù),也就是 .dylib 文件糠睡。編譯、打包成動(dòng)態(tài)庫(kù)后使用 writeSting
方法通過(guò) Socket
通知運(yùn)行的 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 會(huì)在后臺(tái)發(fā)送和監(jiān)聽 Socket 消息狈孔,實(shí)現(xiàn)邏輯在 InjectionServer.mm
的 runInBackground 方法里。Client 也會(huì)開啟一個(gè)后臺(tái)去發(fā)送和監(jiān)聽 Socket 消息材义,實(shí)現(xiàn)邏輯在 InjectionClient.mm
里的 runInBackground 方法里均抽。
Client 接收到消息后會(huì)調(diào)用 inject(tmpfile: String)
方法,運(yùn)行時(shí)進(jìn)行類的動(dòng)態(tài)替換其掂。inject(tmpfile: String) 方法的代碼大部分都是做新類動(dòng)態(tài)替換舊類油挥。inject(tmpfile: String) 的 入?yún)?tmpfile 是動(dòng)態(tài)庫(kù)的文件路徑,那么這個(gè)動(dòng)態(tài)庫(kù)是如何加載到可執(zhí)行文件里的呢?具體的實(shí) 現(xiàn)在 inject(tmpfile: String) 方法開始里款熬,如下:
let newClasses = try SwiftEval.instance.loadAndInject(tmpfile: tmpfile)
先看下 SwiftEval.instance.loadAndInject(tmpfile: tmpfile) 這個(gè)方法的代碼實(shí)現(xiàn):
@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) }
}
}
在這段代碼中深寥,是不是看到你所熟悉的動(dòng)態(tài)庫(kù)加載函數(shù) dlopen 了呢?
guard let dl = dlopen("\(tmpfile).dylib", RTLD_NOW) else {
throw evalError("dlopen() error: \(error)")
}
如上代碼所示,dlopen 會(huì)把 tmpfile 動(dòng)態(tài)庫(kù)文件載入運(yùn)行的 App 里贤牛,返回指針 dl惋鹅。接下來(lái), dlsym 會(huì)得到 tmpfile 動(dòng)態(tài)庫(kù)的符號(hào)地址殉簸,然后就可以處理類的替換工作了闰集。dlsym 調(diào)用對(duì)應(yīng)代 碼如下
guard let newSymbol = dlsym(dl, info.dli_sname) else {
throw evalError("Could not locate newly loaded class symbol")
}
當(dāng)類的方法都被替換后沽讹,我們就可以開始重新繪制界面了。整個(gè)過(guò)程無(wú)需重新編譯和重啟 App武鲁, 至此使用動(dòng)態(tài)庫(kù)方式極速調(diào)試的目的就達(dá)成了爽雄。
Injection 的工作原理圖如下所示:
參考鏈接:
戴銘·iOS開發(fā)高手課
InjectionIII 成噸的提高iOS開發(fā)效率
InjectionIII:iOS開發(fā)必備效率神器-所見即所得
iOS 使用 InjectionIII 注入動(dòng)態(tài)庫(kù)實(shí)現(xiàn)快速調(diào)試
Injection:iOS熱重載背后的黑魔法