javascript
JSPatch近期新特性解析
JSPatch在社區(qū)的推動(dòng)下不斷在優(yōu)化改善,這篇文章總結(jié)下這幾個(gè)月以來(lái) JSPatch 的一些新特性,以及它們的實(shí)現(xiàn)原理。
performSelectorInOC
JavaScript 語(yǔ)言是單線程的,在 OC 使用 JavaScriptCore 引擎執(zhí)行 JS 代碼時(shí),會(huì)對(duì) JS 代碼塊加鎖,保證同個(gè) JSContext 下的 JS 代碼都是順序執(zhí)行。所以調(diào)用 JSPatch 替換的方法,以及在 JSPatch 里調(diào)用 OC 方法,都會(huì)在這個(gè)鎖里執(zhí)行,這導(dǎo)致三個(gè)問(wèn)題:
JSPatch替換的方法無(wú)法并行執(zhí)行,如果如果主線程和子線程同時(shí)運(yùn)行了 JSPatch 替換的方法,這些方法的執(zhí)行都會(huì)順序排隊(duì),主線程會(huì)等待子線程的方法執(zhí)行完后再執(zhí)行,如果子線程方法耗時(shí)長(zhǎng),主線程會(huì)等很久,卡住主線程。
某種情況下,JavaScriptCore 的鎖與 OC 代碼上的鎖混合時(shí),會(huì)產(chǎn)生死鎖。
UIWebView 的初始化會(huì)與 JavaScriptCore 沖突。若在 JavaScriptCore 的鎖里(第一次)初始化 UIWebView 會(huì)導(dǎo)致 webview 無(wú)法解析頁(yè)面。
為解決這些問(wèn)題,JSPatch 新增了 .performSelectorInOC(selector, arguments, callback) 接口,可以在執(zhí)行 OC 方法時(shí)脫離 JavaScriptCore 的鎖,同時(shí)又保證程序順序執(zhí)行。
舉個(gè)例子:
defineClass('JPClassA', {methodA: function() {//run in mainThread},methodB: function() {//run in childThreadvar limit = 20;var data = self.readData(limit);var count = data.count();return {data: data, count: count};} })上述例子中若在主線程和子線程同時(shí)調(diào)用 -methodA 和 -methodB,而 -methodB 里self.readData(limit) 這句調(diào)用耗時(shí)較長(zhǎng),就會(huì)卡住主線程方法 -methodA 的執(zhí)行,對(duì)此可以讓這個(gè)調(diào)用改用 .performSelectorInOC() 接口,讓它在 JavaScriptCore 鎖釋放后再執(zhí)行,不卡住其他線程的 JS 方法執(zhí)行:
defineClass('JPClassA', {methodA: function() {//run in mainThread},methodB: function() {//run in childThreadvar limit = 20;return self.performSelectorInOC('readData', [limit], function(ret) {var count = ret.count();return {data: ret, count: count};});} })這兩份代碼在調(diào)用順序上的區(qū)別如下圖:
第一份代碼對(duì)應(yīng)左邊的流程圖,-methodB 方法被替換,當(dāng) OC 調(diào)用到 -methodB 時(shí)會(huì)去到 JSPatch 核心的 JPForwardInvocation 方法里,在這里面調(diào)用 JS 函數(shù) -methodB,調(diào)用時(shí) JavascriptCore 加鎖,接著在 JS 函數(shù)里做這種處理,調(diào)用 reloadData() 函數(shù),進(jìn)而去到 OC 調(diào)用 -reloadData 方法,這時(shí) -reloadData 方法是在 JavaScriptCore 的鎖里調(diào)用的。直到 JS 函數(shù)執(zhí)行完畢 return 后,JavaScriptCore 的才解鎖,結(jié)束本次調(diào)用。
第二份代碼對(duì)應(yīng)右邊的流程圖,前面是一樣的,調(diào)用 JS 函數(shù) -methodB,JavaScriptCore 加鎖,但 -methodB 函數(shù)在調(diào)用某個(gè) OC 方法時(shí)(這里是reloadData()),不直接去調(diào)用,而是直接 return 返回一個(gè)對(duì)象 {obj},這個(gè){obj}的結(jié)構(gòu)如下:
{ __isPerformInOC:1, obj:self.__obj, clsName:self.__clsName, sel: args[0], args: args[1], cb: args[2] }JS 函數(shù)返回這個(gè)對(duì)象,JS 的調(diào)用就結(jié)束了,JavaScriptCore 的鎖也就釋放了。在 OC 可以拿到 JS 函數(shù)的返回值,也就拿到了這個(gè)對(duì)象,然后判斷它是否 __isPerformInOC=1 對(duì)象,若是就根據(jù)對(duì)象里的 selector / 參數(shù)等信息調(diào)用對(duì)應(yīng)的 OC 方法,這時(shí)這個(gè) OC 方法的調(diào)用是在 JavaScriptCore 的鎖之外調(diào)用的,我們的目的就達(dá)到了。
執(zhí)行 OC 方法后,會(huì)去調(diào) {obj} 里的的 cb 函數(shù),把 OC 方法的返回值傳給 cb 函數(shù),重新回到 JS 去執(zhí)行代碼。這里會(huì)循環(huán)判斷這些回調(diào)函數(shù)是否還返回 __isPerformInOC=1 的對(duì)象,若是則重復(fù)上述流程執(zhí)行,不是則結(jié)束。
整個(gè)原理就是這樣,相關(guān)代碼在 這里 和 這里,實(shí)現(xiàn)起來(lái)其實(shí)挺簡(jiǎn)單,也不會(huì)對(duì)其他流程和邏輯造成影響,就是理解起來(lái)會(huì)有點(diǎn)費(fèi)勁。
performSelectorInOC 文檔里還有關(guān)于死鎖的例子,有興趣可以看看。
可變參數(shù)方法調(diào)用
一直以來(lái)這樣參數(shù)個(gè)數(shù)可變的方法是不能在 JSPatch 動(dòng)態(tài)調(diào)用的:
- (instancetype)initWithTitle:(nullable NSString *)title message:(nullable NSString *)message delegate:(nullable id)delegate cancelButtonTitle:(nullable NSString *)cancelButtonTitle otherButtonTitles:(nullable NSString *)otherButtonTitles, ...原因是 JSPatch 調(diào)用 OC 方法時(shí),是根據(jù) JS 傳入的方法名和參數(shù)組裝成 NSInvocation 動(dòng)態(tài)調(diào)用,而 NSInvocation 不支持調(diào)用參數(shù)個(gè)數(shù)可變的方法。
后來(lái) @wjacker 換了種方式,用 objc_msgSend 的方式支持了可變參數(shù)方法的調(diào)用。之前一直想不到使用 objc_msgSend 是因?yàn)樗贿m用于動(dòng)態(tài)調(diào)用,在方法定義和調(diào)用上都是固定的:
1.定義
需要事先定義好調(diào)用方法的參數(shù)類型和個(gè)數(shù),例如想通過(guò) objc_msgSend 調(diào)用方法
- (int)methodWithFloat:(float)num1 withObj:(id)obj withBool:(BOOL)flag那就需要定義一個(gè)這樣的c函數(shù):
int (*new_msgSend)(id, SEL, float, id, BOOL) = (int (*)(id, SEL, float, id, BOOL)) objc_msgSend;才能通過(guò) new_msgSend 調(diào)用這個(gè)方法。而這個(gè)過(guò)程是無(wú)法動(dòng)態(tài)化的,需要編譯時(shí)確定,而各種方法的參數(shù)/返回值類型不同,參數(shù)個(gè)數(shù)不同,是沒(méi)辦法在編譯時(shí)窮舉寫完的,所以不能用于所有方法的調(diào)用。
而對(duì)于可變參數(shù)方法,只支持參數(shù)類型和返回值類型都是 id 類型的方法,已經(jīng)可以滿足大部分需求,所以讓使用它變得可能:
id (*new_msgSend1)(id, SEL, id,...) = (id (*)(id, SEL, id,...)) objc_msgSend;這樣就可以用 new_msgSend1 調(diào)用固定參數(shù)一個(gè),后續(xù)是可變參數(shù)的方法了。實(shí)際上在模擬器這個(gè)方法也可以支持固定參數(shù)是N個(gè)id的方法,也就是已經(jīng)滿足我們調(diào)用可變參數(shù)方法的需求了,但根據(jù)@wjacker 和 @Awhisper 的測(cè)試,在真機(jī)上不行,不同的固定參數(shù)都需要給它定義好對(duì)應(yīng)的函數(shù)才行,官網(wǎng)文檔對(duì)這點(diǎn)略有說(shuō)明。于是,多了一大堆這樣的定義,以應(yīng)付1-10個(gè)固定參數(shù)的情況:
id (*new_msgSend2)(id, SEL, id,id,...) = (id (*)(id, SEL, id,id,...)) objc_msgSend; id (*new_msgSend3)(id, SEL, id,id,id,...) = (id (*)(id, SEL, id,id,id,...)) objc_msgSend; id (*new_msgSend4)(id, SEL, id,id,id,id,...) = (id (*)(id, SEL, id,id,id,id,...)) objc_msgSend; ...2.調(diào)用
解決上述參數(shù)類型和個(gè)數(shù)定義問(wèn)題后,還有調(diào)用的問(wèn)題,objc_msgSend 不像 NSInvocation 可以在運(yùn)行時(shí)動(dòng)態(tài)添加組裝傳入的參數(shù)個(gè)數(shù),objc_msgSend 則需要在編譯時(shí)確定傳入多少個(gè)參數(shù)。這對(duì)于1-10個(gè)參數(shù)的調(diào)用,不得不用 if else 寫10遍調(diào)用語(yǔ)句,另外根據(jù)方法定義的固定參數(shù)個(gè)數(shù)不一樣,還需要調(diào)用不同的 new_msgSend 函數(shù),所以需要寫10!條調(diào)用,于是有了這樣的大長(zhǎng)篇(gist代碼)。后來(lái)用宏格式化了一下,會(huì)好看一點(diǎn)。
defineProtocol
JSPatch 為一個(gè)類新增原本 OC 不存在的方法時(shí),所有的參數(shù)類型都會(huì)定義為 id 類型,這樣實(shí)現(xiàn)是因?yàn)檫@種在 JS 里新增的方法一般不會(huì)在 OC 上調(diào)用,而是在 JS 上用,JS 可以認(rèn)為一切變量都是對(duì)象,沒(méi)有類型之分,所以全部定義為 id 類型。
但在實(shí)際使用 JSPatch 過(guò)程中,出現(xiàn)了這樣的需求:在 OC 里 .h 文件定義了一個(gè)方法,這個(gè)方法里的參數(shù)和返回值不都是 id 類型,但是在 .m 文件中由于疏忽沒(méi)有實(shí)現(xiàn)這個(gè)方法,導(dǎo)致其他地方調(diào)用這個(gè)方法時(shí)找不到這個(gè)方法造成 crash,要用 JSPatch 修復(fù)這樣的 bug,就需要 JSPatch 可以動(dòng)態(tài)添加指定參數(shù)類型的方法。
實(shí)際上如果在 JS 用 defineClass() 給類添加新方法時(shí),通過(guò)某些接口把方法的各參數(shù)和返回值類型名傳進(jìn)去,內(nèi)部再做些處理就可以解決上述問(wèn)題,但這樣會(huì)把 defineClass 接口搞得很復(fù)雜,不希望這樣做。最終 @Awhisper 想出了個(gè)很好的方法,用動(dòng)態(tài)新增 protocol 的方式支持。
首先 defineClass 是支持 protocol 的:
defineClass("JPViewController: UIViewController<UIScrollViewDelegate, UITextViewDelegate>", {})這樣做的作用是,當(dāng)添加 Protocol 里定義的方法,而類里沒(méi)有實(shí)現(xiàn)的方法時(shí),參數(shù)類型不再全是 id,而是會(huì)根據(jù) Protocol 里定義的參數(shù)類型去添加。
于是若想添加一些指定參數(shù)類型的方法,只需動(dòng)態(tài)新增一個(gè) protocol,定義新增的方法名和對(duì)應(yīng)的參數(shù)類型,再在 defineClass 定義里加上這個(gè) protocol 就可以了。這樣的不污染 defineClass() 的接口,也沒(méi)有更多概念,十分簡(jiǎn)潔地解決了這問(wèn)題。范例:
defineProtocol('JPDemoProtocol',{stringWithRect_withNum_withArray: {paramsType:"CGRect, float, NSArray*",returnType:"id",}, }defineClass('JPTestObject : NSObject <JPDemoProtocol>', {stringWithRect_withNum_withArray:function(rect, num, arr){//use rect/num/arr params herereturn @"success";}, }具體實(shí)現(xiàn)原理原作者已寫得挺清楚,參見(jiàn)這里。
支持重寫dealloc方法
之前 JSPatch 不能替換 -dealloc 方法,原因:
1.按之前的流程,JS 替換 -dealloc 方法后,調(diào)用到 -dealloc 時(shí)會(huì)把 self 包裝成 weakObject 傳給 JS,在包裝的時(shí)候就會(huì)出現(xiàn)以下 crash:
Cannot form weak reference to instance (0x7fb74ac26270) of class JPTestObject. It is possible that this object was over-released, or is in the process of deallocation.意思是在 dealloc 過(guò)程中對(duì)象不能賦給一個(gè) weak 變量,無(wú)法包裝成一個(gè) weakObject 給 JS。
2.若在這里不包裝當(dāng)前調(diào)用對(duì)象,或不傳任何對(duì)象給 JS,就可以成功執(zhí)行到 JS 上替換的 dealloc 方法。但這時(shí)沒(méi)有調(diào)用原生 dealloc 方法,此對(duì)象不會(huì)釋放成功,會(huì)造成內(nèi)存泄露。
-dealloc 被替換后,原 -dealloc 方法 IMP 對(duì)應(yīng)的 selector 已經(jīng)變成 ORIGdealloc,若在執(zhí)行完 JS 的 dealloc 方法后再?gòu)?qiáng)制調(diào)用一遍原 OC 的 ORIGdealloc ,會(huì)crash。猜測(cè)原因是 ARC 對(duì) -dealloc 有特殊處理,執(zhí)行它的 IMP(也就是真實(shí)函數(shù))時(shí)傳進(jìn)去的 selectorName 必須是 dealloc,runtime 才可以調(diào)用它的 [super dealloc],做一些其他處理。
到這里我就沒(méi)什么辦法了,后來(lái) @ipinka 來(lái)了一招欺騙 ARC 的實(shí)現(xiàn),解決了這個(gè)問(wèn)題:
1.首先對(duì)與第一個(gè)問(wèn)題,調(diào)用 -dealloc 時(shí) self 不包裝成 weakObject,而是包裝成 assignObject 傳給 JS,解決了這個(gè)問(wèn)題。
2.對(duì)于第二個(gè)問(wèn)題,調(diào)用 ORIGdealloc 時(shí)因?yàn)?selectorName 改變,ARC 不認(rèn)這是 dealloc 方法,于是用下面的方式調(diào)用:
Class instClass = object_getClass(assignSlf); Method deallocMethod = class_getInstanceMethod(instClass, NSSelectorFromString(@"ORIGdealloc")); void (*originalDealloc)(__unsafe_unretained id, SEL) = (__typeof__(originalDealloc))method_getImplementation(deallocMethod);originalDealloc(assignSlf, NSSelectorFromString(@"dealloc"));做的事情就是,拿出 ORIGdealloc 的 IMP,也就是原 OC 上的 dealloc 實(shí)現(xiàn),然后調(diào)用它時(shí) selectorName 傳入 dealloc,這樣 ARC 就能認(rèn)得這個(gè)方法是 dealloc,做相應(yīng)處理了。
擴(kuò)展
JPCleaner即時(shí)回退
有些 JSPatch 使用者有這樣的需求:腳本執(zhí)行后希望可以回退到?jīng)]有替換的狀態(tài)。之前我的建議使用者自己控制下次啟動(dòng)時(shí)不要執(zhí)行,就算回退了,但還是有不重啟 APP 即時(shí)回退的需求。但這個(gè)需求并不是核心功能,所以想辦法把它抽離,放到擴(kuò)展里了。
只需引入 JPCleaner.h,調(diào)用 +cleanAll 接口就可以把當(dāng)前所有被 JSPatch 替換的方法恢復(fù)原樣。另外還有 +cleanClass: 接口支持只回退某個(gè)類。這些接口可以在 OC 調(diào)用,也可以在 JS 腳本動(dòng)態(tài)調(diào)用:
[JPCleaner cleanAll] [JPCleaner cleanClass:@“JPViewController”];實(shí)現(xiàn)原理也很簡(jiǎn)單,在 JSPatch 核心里所有替換的方法都會(huì)保存在內(nèi)部一個(gè)靜態(tài)變量 _JSOverideMethods 里,它的結(jié)構(gòu)是 _JSOverideMethods[cls][selectorName] = jsFunction。我給 JPExtension 添加了個(gè)接口,把這個(gè)靜態(tài)變量暴露給外部,遍歷這個(gè)變量里保存的 class 和 selectorName,把 selector 對(duì)應(yīng)的 IMP 重新指向原生 IMP 就可以了。詳見(jiàn)源碼。
JPLoader
JSPatch 腳本需要后臺(tái)下發(fā),客戶端需要一套打包下載/執(zhí)行的流程,還需要考慮傳輸過(guò)程中安全問(wèn)題,JPLoader 就是幫你做了這些事情。
下載執(zhí)行腳本很簡(jiǎn)單,這里主要做的事是保證傳輸過(guò)程的安全,JPLoader 包含了一個(gè)打包工具 packer.php,用這個(gè)工具對(duì)腳本文件進(jìn)行打包,得出打包文件的 MD5,再對(duì)這個(gè)MD5 值用私鑰進(jìn)行 RSA 加密,把加密后的數(shù)據(jù)跟腳本文件一起大包發(fā)給客戶端。JPLoader 里的程序?qū)@個(gè)加密數(shù)據(jù)用私鑰進(jìn)行解密,再計(jì)算一遍下發(fā)的腳本文件 MD5 值,看解密出來(lái)的值跟這邊計(jì)算出來(lái)的值是否一致,一致說(shuō)明腳本文件從服務(wù)器到客戶端之間沒(méi)被第三方篡改過(guò),保證腳本的安全。對(duì)這一過(guò)程的具體描述詳見(jiàn)舊文 JSPatch部署安全策略。對(duì) JPLoader 的使用方式可以參照 wiki 文檔
總結(jié)
以上是生活随笔為你收集整理的JSPatch近期新特性解析的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 分类条件概率
- 下一篇: cocos2d-x 关于tilemap滚