JSPatch在社區的推進下不斷在優化改善,這篇文章總結下這幾個月以來 JSPatch 的一些新特性,以及它們的實現原理。php
JavaScript 語言是單線程的,在 OC 使用 JavaScriptCore 引擎執行 JS 代碼時,會對 JS 代碼塊加鎖,保證同個 JSContext 下的 JS 代碼都是順序執行。因此調用 JSPatch 替換的方法,以及在 JSPatch 裏調用 OC 方法,都會在這個鎖裏執行,這致使三個問題:html
JSPatch替換的方法沒法並行執行,若是若是主線程和子線程同時運行了 JSPatch 替換的方法,這些方法的執行都會順序排隊,主線程會等待子線程的方法執行完後再執行,若是子線程方法耗時長,主線程會等好久,卡住主線程。ios
某種狀況下,JavaScriptCore 的鎖與 OC 代碼上的鎖混合時,會產生死鎖。git
UIWebView 的初始化會與 JavaScriptCore 衝突。若在 JavaScriptCore 的鎖裏(第一次)初始化 UIWebView 會致使 webview 沒法解析頁面。github
爲解決這些問題,JSPatch 新增了 .performSelectorInOC(selector, arguments, callback)
接口,能夠在執行 OC 方法時脫離 JavaScriptCore 的鎖,同時又保證程序順序執行。web
舉個例子:數組
defineClass('JPClassA', { methodA: function() { //run in mainThread }, methodB: function() { //run in childThread var limit = 20; var data = self.readData(limit); var count = data.count(); return {data: data, count: count}; } })
上述例子中若在主線程和子線程同時調用 -methodA
和 -methodB
,而 -methodB
裏self.readData(limit)
這句調用耗時較長,就會卡住主線程方法 -methodA
的執行,對此可讓這個調用改用 .performSelectorInOC()
接口,讓它在 JavaScriptCore 鎖釋放後再執行,不卡住其餘線程的 JS 方法執行:安全
defineClass('JPClassA', { methodA: function() { //run in mainThread }, methodB: function() { //run in childThread var limit = 20; return self.performSelectorInOC('readData', [limit], function(ret) { var count = ret.count(); return {data: ret, count: count}; }); } })
這兩份代碼在調用順序上的區別以下圖:服務器
第一份代碼對應左邊的流程圖,-methodB
方法被替換,當 OC 調用到 -methodB
時會去到 JSPatch 核心的 JPForwardInvocation
方法裏,在這裏面調用 JS 函數 -methodB
,調用時 JavascriptCore 加鎖,接着在 JS 函數裏作這種處理,調用 reloadData()
函數,進而去到 OC 調用 -reloadData
方法,這時 -reloadData
方法是在 JavaScriptCore 的鎖裏調用的。直到 JS 函數執行完畢 return 後,JavaScriptCore 的才解鎖,結束本次調用。app
第二份代碼對應右邊的流程圖,前面是同樣的,調用 JS 函數 -methodB
,JavaScriptCore 加鎖,但 -methodB
函數在調用某個 OC 方法時(這裏是reloadData()
),不直接去調用,而是直接 return 返回一個對象 {obj}
,這個{obj}
的結構以下:
{ __isPerformInOC:1, obj:self.__obj, clsName:self.__clsName, sel: args[0], args: args[1], cb: args[2] }
JS 函數返回這個對象,JS 的調用就結束了,JavaScriptCore 的鎖也就釋放了。在 OC 能夠拿到 JS 函數的返回值,也就拿到了這個對象,而後判斷它是否 __isPerformInOC=1
對象,如果就根據對象裏的 selector / 參數等信息調用對應的 OC 方法,這時這個 OC 方法的調用是在 JavaScriptCore 的鎖以外調用的,咱們的目的就達到了。
執行 OC 方法後,會去調 {obj}
裏的的 cb 函數,把 OC 方法的返回值傳給 cb 函數,從新回到 JS 去執行代碼。這裏會循環判斷這些回調函數是否還返回 __isPerformInOC=1
的對象,如果則重複上述流程執行,不是則結束。
整個原理就是這樣,相關代碼在 這裏 和 這裏,實現起來其實挺簡單,也不會對其餘流程和邏輯形成影響,就是理解起來會有點費勁。
performSelectorInOC 文檔裏還有關於死鎖的例子,有興趣能夠看看。
一直以來這樣參數個數可變的方法是不能在 JSPatch 動態調用的:
- (instancetype)initWithTitle:(nullable NSString *)title message:(nullable NSString *)message delegate:(nullable id)delegate cancelButtonTitle:(nullable NSString *)cancelButtonTitle otherButtonTitles:(nullable NSString *)otherButtonTitles, ...
緣由是 JSPatch 調用 OC 方法時,是根據 JS 傳入的方法名和參數組裝成 NSInvocation 動態調用,而 NSInvocation 不支持調用參數個數可變的方法。
後來 @wjacker 換了種方式,用 objc_msgSend 的方式支持了可變參數方法的調用。以前一直想不到使用 objc_msgSend 是由於它不適用於動態調用,在方法定義和調用上都是固定的:
1.定義
須要事先定義好調用方法的參數類型和個數,例如想經過 objc_msgSend 調用方法
- (int)methodWithFloat:(float)num1 withObj:(id)obj withBool:(BOOL)flag
那就須要定義一個這樣的c函數:
int (*new_msgSend)(id, SEL, float, id, BOOL) = (int (*)(id, SEL, float, id, BOOL)) objc_msgSend;
才能經過 new_msgSend 調用這個方法。而這個過程是沒法動態化的,須要編譯時肯定,而各類方法的參數/返回值類型不一樣,參數個數不一樣,是沒辦法在編譯時窮舉寫完的,因此不能用於全部方法的調用。
而對於可變參數方法,只支持參數類型和返回值類型都是 id 類型的方法,已經能夠知足大部分需求,因此讓使用它變得可能:
id (*new_msgSend1)(id, SEL, id,...) = (id (*)(id, SEL, id,...)) objc_msgSend;
這樣就能夠用 new_msgSend1 調用固定參數一個,後續是可變參數的方法了。實際上在模擬器這個方法也能夠支持固定參數是N個id的方法,也就是已經知足咱們調用可變參數方法的需求了,但根據@wjacker 和 @Awhisper 的測試,在真機上不行,不一樣的固定參數都須要給它定義好對應的函數才行,官網文檔對這點略有說明。因而,多了一大堆這樣的定義,以應付1-10個固定參數的狀況:
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.調用
解決上述參數類型和個數定義問題後,還有調用的問題,objc_msgSend 不像 NSInvocation 能夠在運行時動態添加組裝傳入的參數個數,objc_msgSend 則須要在編譯時肯定傳入多少個參數。這對於1-10個參數的調用,不得不用 if else 寫10遍調用語句,另外根據方法定義的固定參數個數不同,還須要調用不一樣的 new_msgSend 函數,因此須要寫10!條調用,因而有了這樣的大長篇(gist代碼)。後來用宏格式化了一下,會好看一點。
JSPatch 爲一個類新增本來 OC 不存在的方法時,全部的參數類型都會定義爲 id
類型,這樣實現是由於這種在 JS 裏新增的方法通常不會在 OC 上調用,而是在 JS 上用,JS 能夠認爲一切變量都是對象,沒有類型之分,因此所有定義爲 id
類型。
但在實際使用 JSPatch 過程當中,出現了這樣的需求:在 OC 裏 .h
文件定義了一個方法,這個方法裏的參數和返回值不都是 id
類型,可是在 .m
文件中因爲疏忽沒有實現這個方法,致使其餘地方調用這個方法時找不到這個方法形成 crash,要用 JSPatch 修復這樣的 bug,就須要 JSPatch 能夠動態添加指定參數類型的方法。
實際上若是在 JS 用 defineClass()
給類添加新方法時,經過某些接口把方法的各參數和返回值類型名傳進去,內部再作些處理就能夠解決上述問題,但這樣會把 defineClass 接口搞得很複雜,不但願這樣作。最終 @Awhisper 想出了個很好的方法,用動態新增 protocol 的方式支持。
首先 defineClass 是支持 protocol 的:
defineClass("JPViewController: UIViewController<UIScrollViewDelegate, UITextViewDelegate>", {})
這樣作的做用是,當添加 Protocol 裏定義的方法,而類裏沒有實現的方法時,參數類型再也不全是 id,而是會根據 Protocol 裏定義的參數類型去添加。
因而若想添加一些指定參數類型的方法,只需動態新增一個 protocol,定義新增的方法名和對應的參數類型,再在 defineClass 定義里加上這個 protocol 就能夠了。這樣的不污染 defineClass()
的接口,也沒有更多概念,十分簡潔地解決了這問題。範例:
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 here return @"success"; }, }
具體實現原理原做者已寫得挺清楚,參見這裏。
以前 JSPatch 不能替換 -dealloc
方法,緣由:
1.按以前的流程,JS 替換 -dealloc
方法後,調用到 -dealloc
時會把 self
包裝成 weakObject 傳給 JS,在包裝的時候就會出現如下 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 過程當中對象不能賦給一個 weak 變量,沒法包裝成一個 weakObject 給 JS。
2.若在這裏不包裝當前調用對象,或不傳任何對象給 JS,就能夠成功執行到 JS 上替換的 dealloc 方法。但這時沒有調用原生 dealloc 方法,此對象不會釋放成功,會形成內存泄露。
-dealloc
被替換後,原 -dealloc
方法 IMP 對應的 selector 已經變成 ORIGdealloc
,若在執行完 JS 的 dealloc 方法後再強制調用一遍原 OC 的 ORIGdealloc
,會crash。猜想緣由是 ARC 對 -dealloc
有特殊處理,執行它的 IMP(也就是真實函數)時傳進去的 selectorName 必須是 dealloc
,runtime 才能夠調用它的 [super dealloc],作一些其餘處理。
到這裏我就沒什麼辦法了,後來 @ipinka 來了一招欺騙 ARC 的實現,解決了這個問題:
1.首先對與第一個問題,調用 -dealloc
時 self 不包裝成 weakObject,而是包裝成 assignObject 傳給 JS,解決了這個問題。
2.對於第二個問題,調用 ORIGdealloc
時由於 selectorName 改變,ARC 不認這是 dealloc 方法,因而用下面的方式調用:
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 實現,而後調用它時 selectorName 傳入 dealloc,這樣 ARC 就能認得這個方法是 dealloc,作相應處理了。
有些 JSPatch 使用者有這樣的需求:腳本執行後但願能夠回退到沒有替換的狀態。以前個人建議使用者本身控制下次啓動時不要執行,就算回退了,但仍是有不重啓 APP 即時回退的需求。但這個需求並非核心功能,因此想辦法把它抽離,放到擴展裏了。
只需引入 JPCleaner.h,調用 +cleanAll 接口就能夠把當前全部被 JSPatch 替換的方法恢復原樣。另外還有 +cleanClass:
接口支持只回退某個類。這些接口能夠在 OC 調用,也能夠在 JS 腳本動態調用:
[JPCleaner cleanAll] [JPCleaner cleanClass:@「JPViewController」];
實現原理也很簡單,在 JSPatch 核內心全部替換的方法都會保存在內部一個靜態變量 _JSOverideMethods
裏,它的結構是 _JSOverideMethods[cls][selectorName] = jsFunction
。我給 JPExtension 添加了個接口,把這個靜態變量暴露給外部,遍歷這個變量裏保存的 class 和 selectorName,把 selector 對應的 IMP 從新指向原生 IMP 就能夠了。詳見源碼。
JSPatch 腳本須要後臺下發,客戶端須要一套打包下載/執行的流程,還須要考慮傳輸過程當中安全問題,JPLoader 就是幫你作了這些事情。
下載執行腳本很簡單,這裏主要作的事是保證傳輸過程的安全,JPLoader 包含了一個打包工具 packer.php,用這個工具對腳本文件進行打包,得出打包文件的 MD5,再對這個MD5 值用私鑰進行 RSA 加密,把加密後的數據跟腳本文件一塊兒大包發給客戶端。JPLoader 裏的程序對這個加密數據用私鑰進行解密,再計算一遍下發的腳本文件 MD5 值,看解密出來的值跟這邊計算出來的值是否一致,一致說明腳本文件從服務器到客戶端之間沒被第三方篡改過,保證腳本的安全。對這一過程的具體描述詳見舊文 JSPatch部署安全策略。對 JPLoader 的使用方式能夠參照 wiki 文檔