? ? ? ? ? ? ? ? JSPatch簡(jiǎn)介
JSPatch誕生于2015年5月,最初是騰訊廣研高級(jí)iOS開(kāi)發(fā)@bang的個(gè)人項(xiàng)目。
它能夠使用JavaScript調(diào)用Objective-C的原生接口,從而動(dòng)態(tài)植入代碼來(lái)替換舊代碼,以實(shí)現(xiàn)修復(fù)線上bug。
JSPatch在Github.com上開(kāi)源后獲得了3000多個(gè)star和500多fork,廣受關(guān)注,目前已被應(yīng)用在大量騰訊/阿里/百度的App中。
? ? ? ? ? JSPatch基礎(chǔ)原理
Objective-C是動(dòng)態(tài)語(yǔ)言,具有運(yùn)行時(shí)特性,該特性可通過(guò)類名稱和方法名的字符串獲取該類和該方法,并實(shí)例化和調(diào)用。
Class class = NSClassFromString(“UIViewController");
id viewController = [[class alloc] init];
SEL selector = NSSelectorFromString(“viewDidLoad");[viewController performSelector:selector];
也可以替換某個(gè)類的方法為新的實(shí)現(xiàn):
staticvoidnewViewDidLoad(id slf, SEL sel){}class_replaceMethod(class, selector, newViewDidLoad, @"");
還可以新注冊(cè)一個(gè)類,為類添加方法:
Class cls = objc_allocateClassPair(superCls,"JPObject",0);objc_registerClassPair(cls);class_addMethod(cls, selector, implement, typedesc);
Javascript調(diào)用
我們可以用Javascript對(duì)象定義一個(gè)Objective-C類:
{? __isCls:1,? __clsName:"UIView"}
在OC執(zhí)行JS腳本前,通過(guò)正則把所有方法調(diào)用都改成調(diào)用 __c() 函數(shù),再執(zhí)行這個(gè)JS腳本,做到了類似OC/Lua/Ruby等的消息轉(zhuǎn)發(fā)機(jī)制:
UIView.alloc().init()->UIView.__c('alloc')().__c('init')()
給JS對(duì)象基類 Object 的 prototype 加上c 成員,這樣所有對(duì)象都可以調(diào)用到c,根據(jù)當(dāng)前對(duì)象類型判斷進(jìn)行不同操作:
Object.prototype.__c =function(methodName){if(!this.__obj && !this.__clsName)returnthis[methodName].bind(this);varself =thisreturnfunction(){varargs =Array.prototype.slice.call(arguments)return_methodFunc(self.__obj, self.__clsName, methodName, args, self.__isSuper)? }}
互傳消息
JS和OC是通過(guò)JavaScriptCore互傳消息的。OC端在啟動(dòng)JSPatch引擎時(shí)會(huì)創(chuàng)建一個(gè) JSContext 實(shí)例,JSContext 是JS代碼的執(zhí)行環(huán)境,可以給 JSContext 添加方法。JS通過(guò)調(diào)用 JSContext 定義的方法把數(shù)據(jù)傳給OC,OC通過(guò)返回值傳會(huì)給JS。調(diào)用這種方法,它的參數(shù)/返回值 JavaScriptCore 都會(huì)自動(dòng)轉(zhuǎn)換,OC里的 NSArray, NSDictionary, NSString, NSNumber, NSBlock 會(huì)分別轉(zhuǎn)為JS端的數(shù)組/對(duì)象/字符串/數(shù)字/函數(shù)類型。
對(duì)于一個(gè)自定義id對(duì)象,JavaScriptCore 會(huì)把這個(gè)自定義對(duì)象的指針傳給JS,這個(gè)對(duì)象在JS無(wú)法使用,但在回傳給OC時(shí)OC可以找到這個(gè)對(duì)象。對(duì)于這個(gè)對(duì)象生命周期的管理,如果JS有變量引用時(shí),這個(gè)OC對(duì)象引用計(jì)數(shù)就加1 ,JS變量的引用釋放了就減1,如果OC上沒(méi)別的持有者,這個(gè)OC對(duì)象的生命周期就跟著JS走了,會(huì)在JS進(jìn)行垃圾回收時(shí)釋放。
方法替換
把UIViewController的-viewWillAppear:方法通過(guò)class_replaceMethod()接口指向_objc_msgForward,這是一個(gè)全局 IMP,OC 調(diào)用方法不存在時(shí)都會(huì)轉(zhuǎn)發(fā)到這個(gè) IMP 上,這里直接把方法替換成這個(gè) IMP,這樣調(diào)用這個(gè)方法時(shí)就會(huì)走到-forwardInvocation:。
為UIViewController添加-ORIGviewWillAppear:和-_JPviewWillAppear:兩個(gè)方法,前者指向原來(lái)的IMP實(shí)現(xiàn),后者是新的實(shí)現(xiàn),稍后會(huì)在這個(gè)實(shí)現(xiàn)里回調(diào)JS函數(shù)。
改寫(xiě)UIViewController的-forwardInvocation:方法為自定義實(shí)現(xiàn)。一旦OC里調(diào)用 UIViewController 的-viewWillAppear:方法,經(jīng)過(guò)上面的處理會(huì)把這個(gè)調(diào)用轉(zhuǎn)發(fā)到-forwardInvocation:,這時(shí)已經(jīng)組裝好了一個(gè) NSInvocation,包含了這個(gè)調(diào)用的參數(shù)。在這里把參數(shù)從 NSInvocation 反解出來(lái),帶著參數(shù)調(diào)用上述新增加的方法-JPviewWillAppear:,在這個(gè)新方法里取到參數(shù)傳給JS,調(diào)用JS的實(shí)現(xiàn)函數(shù)。整個(gè)調(diào)用過(guò)程就結(jié)束了,整個(gè)過(guò)程圖示如下:

JSPatch方法替換
最后一個(gè)問(wèn)題,我們把 UIViewController 的-forwardInvocation:方法的實(shí)現(xiàn)給替換掉了,如果程序里真有用到這個(gè)方法對(duì)消息進(jìn)行轉(zhuǎn)發(fā),原來(lái)的邏輯怎么辦?首先我們?cè)谔鎿Q-forwardInvocation:方法前會(huì)新建一個(gè)方法-ORIGforwardInvocation:,保存原來(lái)的實(shí)現(xiàn)IMP,在新的-forwardInvocation:實(shí)現(xiàn)里做了個(gè)判斷,如果轉(zhuǎn)發(fā)的方法是我們想改寫(xiě)的,就走我們的邏輯,若不是,就調(diào)-ORIGforwardInvocation:走原來(lái)的流程。
JSPathch語(yǔ)法
1. require
在使用Objective-C類之前需要調(diào)用require('className’):
1 ?類型轉(zhuǎn)換
Property
? ? ?獲取/修改 Property 等于調(diào)用這個(gè) Property 的 getter / setter 方法,獲取時(shí)記得加():
view.setBackgroundColor(redColor);varbgColor=view.backgroundColor();
方法名前加ORIG即可調(diào)用未覆蓋前的 OC 原方法:
// OC@implementationJPTableViewController- (void)viewDidLoad{}@end
// JSdefineClass("JPTableViewController", {viewDidLoad:function() {self.ORIGviewDidLoad();? },})
4. 特殊類型
JSPatch原生支持 CGRect / CGPoint / CGSize / NSRange 這四個(gè) struct 類型,用 JS 對(duì)象表示:
// Obj-CUIView *view = [[UIViewalloc]initWithFrame:CGRectMake(20,20,100,100)];[viewsetCenter:CGPointMake(10,10)];[viewsizeThatFits:CGSizeMake(100,100)];CGFloatx = view.frame.origin.x;NSRangerange =NSMakeRange(0,1);
// JSvarview=UIView.alloc().initWithFrame({x:20, y:20, width:100, height:100})view.setCenter({x:10, y:10})view.sizeThatFits({width:100, height:100})varx=view.frame().xvarrange={location:0, length:1}
//在JS里面判斷是否為空要判斷false:
var url = "";
var rawData = NSData.dataWithContentsOfURL(NSURL.URLWithString(url));
if (!rawData){
console.log('是否為空');
}