前言
每次從開發(fā)Flutter開發(fā)切回到原生開發(fā)時候最不習(xí)慣的就是原生沒有熱重載功能模捂。
簡單地調(diào)一下字體顏色捶朵,view大小都要重新編譯,既耗時又費力狂男。
所以想了一下可不可以讓原生開發(fā)也可以享受到熱重載功能综看,在UI調(diào)試下可以做到 "即寫即看"。
iOS端的熱更新主要可以分成倆大塊岖食。
一種是基于JSCore红碑。
它建立起了Objective-C 與 JavaScript通的橋梁。
代表框架有,React Native析珊, Weex羡鸥,JSPatch? 等等優(yōu)秀框架。
還有一種是更為小眾一些的忠寻。
自己實現(xiàn)了一個 OC 語法的簡單解釋器惧浴,包含了基礎(chǔ)的詞法分析與語法分析,從而能夠在運行期將 OC 代碼生成抽象語法樹 AST 然后進行執(zhí)行奕剃。再通過 runtime 進行方法替換 方法添加等操作衷旅,進而實現(xiàn)了動態(tài)化的效果。
代表框架有纵朋,OCEval, OCRunner 等框架 (據(jù)我觀察貌似都是獨立開發(fā)者柿顶,目前暫時還無法做到商用級別。但也可以拿他們的基礎(chǔ)框架進行魔改操软,大部分基礎(chǔ)工作已經(jīng)做完了)嘁锯。
React Native 與 Weex 比較重,需要項目級支持聂薪。
JSPatch 是需要下發(fā)js代碼進行熱修復(fù)家乘,雖然可以滿足需求,但弊端也比較明顯胆建,那就是需要將OC代碼強行翻譯成JS語法烤低,大段落使用時候不是很方便。
用OC語法解釋器笆载,雖然可以做到OC代碼不轉(zhuǎn)譯, 但還是需要對框架做一些魔改才可以按照自己習(xí)慣方便使用? (自己做語法分析, 生成抽象語法樹 這塊還是值得研究的扑馁。理論上可以做到 下發(fā)OC代碼, 可以基于 解釋器+runtime 做到整個app都動態(tài)化)。
我設(shè)想的場景是只是在debug環(huán)境下凉驻,即寫即看腻要。
線上修復(fù)不想打擦邊球, 以及引入一些不可靠的因素。
所以才去的方案是通過動態(tài)庫來在開發(fā)階段做到動態(tài)化涝登。
眾所周知Objective-C 這門語言天生具有動態(tài)性雄家,可以任意的在運行時替換方法, 成員變量等等。那這樣就聯(lián)想到可不可以下發(fā)動態(tài)庫胀滚,在運行時加載這個動態(tài)庫并替換新加載進來的動態(tài)庫里類對象/元類對象的一些信息趟济。這樣就可以再開發(fā)環(huán)境下做到 "即寫即看"的效果。
實踐
項目搭建
項目分倆部分咽笼。
第一部分:? macOS項目(監(jiān)聽熱重載項目文件變化)
第二部分:? iOS項目(熱重載目標項目)
第一部分 (監(jiān)聽文件變化項目)
首先我們需要有一個mac端程序監(jiān)聽指定文件夾下文件的變化顷编,從而將保存變化后的.m文件通過運行預(yù)先編寫好的 shell腳本進行 編譯 成 .o 文件并打包成 一個dylib,發(fā)送給app剑刑。
shell腳本整體流程思路 :
1) 接收 要編譯的 .m 文件路徑 以及 .m引用的其他類的 .o文件路徑 (這一步是生成動態(tài)庫必要的媳纬,不然會因為LinkFileList 引用出錯而無法編譯出一個dylib)
2) 通過clang 先將 .m 編譯成 .o 并儲存起來
3) 再將重新編譯后的 .o 文件編譯成dylib
4) 這時候可以多加一個參數(shù)判斷是向真機還是模擬器發(fā)送動態(tài)庫双肤。如果向真機發(fā)送的話需要做個簽名操作,不然真機無法dlopen 這個 dylib
5) 向目標傳輸編譯好的dylib
監(jiān)聽軟件部分
這部分可以通過?FSEventStream 來實現(xiàn)監(jiān)聽文件變化钮惠。
shell腳本方面可以使用?NSTask 來執(zhí)行腳本名茅糜。
* 我是在這里生成了LinkFileList。是從變化文件字符串中提取出 引入文件并遍歷拼接成一個有效 依賴鏈接素挽。不知道有沒有更好的方法生成該文件需要依賴的 LinkFileList蔑赘。
最終成型后的mac端監(jiān)聽軟件
這里勾選中真機 并 鍵入手機ip地址將會熱重載手機端app。解除勾選將會熱重載模擬器毁菱。這樣的話倆者都可以兼顧了米死。
第一個部分這樣就差不多可以了。
第二部分 (熱重載目標項目)
首先項目里面要搭建一個http服務(wù)贮庞,我們這里選擇的是用 GCDWebServer。
GCDWebServer 是一個基于GCD 可以用于macOS & iOS 上的一個輕量的HTTP server究西,該庫實現(xiàn)了基于web的文件上傳等功能窗慎。
然后要開始編寫解析mac上編譯并連接好的dylib了。
① 通過 dlopen 打開傳進來的 dylib
? ? dlopen(dylibPatch,RTLD_NOW)
② 獲取內(nèi)存中所有鏡像
? ? int32_t images= _dyld_image_count();? ? // 所有內(nèi)存中鏡像
③ 循環(huán)鏡像獲取剛剛注入的動態(tài)庫鏡像卤材。(這個步驟是必須的遮斥,不然會踩坑)
? ? ? ? for(uint32_ti =0; i < images; i++) {
? ? ? ? ? ? pszModName =_dyld_get_image_name(i);
? ? ? ? ? ? if(!strcmp(pszModName, dylibPatch)){? // 判斷鏡像地址是否與傳進來的dylib地址一致? ? ? ? ? ?
? ? ? ? ? ? ? ? base? = (void*)_dyld_get_image_header(i);
? ? ? ? ? ? ? ? slide =_dyld_get_image_vmaddr_slide(i);
? ? ? ? ? ? }
? ? ? ? }
④ 獲取注入動態(tài)庫結(jié)構(gòu)體地址
? ? ? ? Dl_infoinfo;
? ? ? ? dladdr((mach_header_t*)base, &info);
? ? ? ? machHeader1 = (structmach_header_64*)info.dli_fbase;
⑤ mach-O 文件里面的 class列表信息存在Data斷。獲取data段 classList 信息 (這個節(jié)列出了所有的class扇丛,包括元類對象)
? ? ? ? uint64_tsize =0;
? ? ? ? char*referencesSection =getsectdatafromheader_64(machHeader1,? ? ? ? ? ? "__DATA","__objc_classlist", &size );
⑥ 獲取注入dylib 類對象
? ? Class class = classReferences[i];
⑦ 對象替換
? ? // 獲取要替換類名稱
? ? constchar*className = class_getName(newClass);
? ? // 獲取當前內(nèi)存中類對象
? ? Class oldClass = objc_getClass(className);
? ? // 判斷是否是注入進來的類對象
? ? if? ( newClass != oldClass ) {
? ? ? ? 開始進行方法替換
? ? }
⑧ 刪除掉傳上來的動態(tài)庫
? ? [[NSFileManager defaultManager] removeItemAtPath:patch error:nil];
⑨ 發(fā)送廣播
? ? dispatch_async(dispatch_get_main_queue(), ^{
? ? ? ? ? ? [[NSNotificationCenter defaultCenter] postNotificationName:@"DWHotReload" object:nil];
? ? });
? ?* 這一步可以優(yōu)化成通過消息轉(zhuǎn)發(fā)來調(diào)用术吗。demo圖省事直接發(fā)了個廣播
待解決問題
上述步驟就是個大概的一個解析流程。
如果想要做到真正項目應(yīng)用級的話帆精,需要潤色點是shell腳本, 引用三方庫時候鏈接編譯問題较屿。
支持swift。
期待最后的落地場景
最終期望落地是卓练,測試的同學(xué)在debug頁面開啟接收dylib開關(guān)隘蝎,開發(fā)同學(xué)只要本地修復(fù)問題后直接下發(fā)dylib,直接在測試同學(xué)設(shè)備上修正好襟企。 因為是直接編寫的OC/swift 代碼所以嘱么,不會像是使用jspatch 需要最終回歸一下正式代碼。這條路很漫長顽悼。曼振。。 慢慢走蔚龙。
推薦框架
強烈推薦injection的框架冰评。oc項目無侵入的可以直接熱重載。但swift 項目在有些bridge時候會發(fā)生異常府蛇。
誤區(qū)
網(wǎng)上很多文章都再說dlopen只能在模擬器上使用集索,其實并不正確的。
真機上無法dlopen加載dylib,大體是犯了倆個錯誤务荆。
一妆距,編譯時target依賴的是x86架構(gòu)
二,打包成dylib后沒有做有效簽名
如果這倆點都做了其實dylib可以在真機上dlopen加載成功 (逆向開發(fā)后的插件就是dylib函匕,它能注入到真機二進制文件里咱們自己的也必然可以)
* 本demo也參考了部分injection思路娱据,并使用了該工程里的方法互換方法。
demo地址
https://github.com/378804441/DWHotReload
demo? 分為三個部分
① 本地監(jiān)聽文件改變工程 (FSEventStreamDemo)
② 編譯shell腳本 (shell)
③ Demo工程 (DWDebugHR)
注意: 工程直接跑不起來盅惜,要想跑起來的話可以按照錯誤提示自己配置下路徑中剩。開發(fā)階段我都寫得我本地絕對路徑 哈哈哈哈