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

一、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)行的效果圖如下所示:

使用示例.gif

作者已經(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)擊安裝

Mac App Store下載.png

2、安裝成功航厚,打開應(yīng)用

InjectionII.app期望在路徑中找到您當(dāng)前的Xcode /Applications/Xcode.app顷歌,適用于Swift和Objective-C可以與AppCode一起使用,但您需要首先使用Xcode構(gòu)建項(xiàng)目幔睬,以提供用于確定如何編譯項(xiàng)目的日志眯漩。

啟動(dòng)app.png

3、AppDelegate中注入代碼

InjectionII.app 需要知道您當(dāng)前的Xcode路徑 /Applications/Xcode.app麻顶,適用于SwiftObjective-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
AppDelegate中注入代碼.png

4矫钓、選擇監(jiān)聽文件目錄

首次運(yùn)行時(shí)要尔,會(huì)彈出彈框舍杜,選擇監(jiān)聽文件改變路徑


選擇監(jiān)聽文件目錄.png

選擇后,控制臺(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吸重。

實(shí)現(xiàn)injected方法.png

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 的工作原理圖如下所示:

Injection 的工作原理示意圖.png



參考鏈接:
戴銘·iOS開發(fā)高手課
InjectionIII 成噸的提高iOS開發(fā)效率
InjectionIII:iOS開發(fā)必備效率神器-所見即所得
iOS 使用 InjectionIII 注入動(dòng)態(tài)庫(kù)實(shí)現(xiàn)快速調(diào)試
Injection:iOS熱重載背后的黑魔法

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市沐鼠,隨后出現(xiàn)的幾起案子挚瘟,更是在濱河造成了極大的恐慌,老刑警劉巖迟杂,帶你破解...
    沈念sama閱讀 221,430評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件刽沾,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡排拷,警方通過(guò)查閱死者的電腦和手機(jī)侧漓,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,406評(píng)論 3 398
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)监氢,“玉大人布蔗,你說(shuō)我怎么就攤上這事±烁” “怎么了纵揍?”我有些...
    開封第一講書人閱讀 167,834評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)议街。 經(jīng)常有香客問(wèn)我泽谨,道長(zhǎng),這世上最難降的妖魔是什么特漩? 我笑而不...
    開封第一講書人閱讀 59,543評(píng)論 1 296
  • 正文 為了忘掉前任吧雹,我火速辦了婚禮,結(jié)果婚禮上涂身,老公的妹妹穿的比我還像新娘雄卷。我一直安慰自己,他們只是感情好蛤售,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,547評(píng)論 6 397
  • 文/花漫 我一把揭開白布丁鹉。 她就那樣靜靜地躺著,像睡著了一般悴能。 火紅的嫁衣襯著肌膚如雪揣钦。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,196評(píng)論 1 308
  • 那天搜骡,我揣著相機(jī)與錄音拂盯,去河邊找鬼。 笑死记靡,一個(gè)胖子當(dāng)著我的面吹牛谈竿,可吹牛的內(nèi)容都是我干的团驱。 我是一名探鬼主播,決...
    沈念sama閱讀 40,776評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼空凸,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼嚎花!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起呀洲,我...
    開封第一講書人閱讀 39,671評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤紊选,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后道逗,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體兵罢,經(jīng)...
    沈念sama閱讀 46,221評(píng)論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,303評(píng)論 3 340
  • 正文 我和宋清朗相戀三年滓窍,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了卖词。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,444評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡吏夯,死狀恐怖此蜈,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情噪生,我是刑警寧澤裆赵,帶...
    沈念sama閱讀 36,134評(píng)論 5 350
  • 正文 年R本政府宣布,位于F島的核電站跺嗽,受9級(jí)特大地震影響战授,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜桨嫁,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,810評(píng)論 3 333
  • 文/蒙蒙 一陈醒、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧瞧甩,春花似錦、人聲如沸弥鹦。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,285評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)彬坏。三九已至朦促,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間栓始,已是汗流浹背务冕。 一陣腳步聲響...
    開封第一講書人閱讀 33,399評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留幻赚,地道東北人禀忆。 一個(gè)月前我還...
    沈念sama閱讀 48,837評(píng)論 3 376
  • 正文 我出身青樓臊旭,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親箩退。 傳聞我的和親對(duì)象是個(gè)殘疾皇子离熏,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,455評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容