一:關(guān)于JSPatch
JSPatch : 是一個(gè)iOS動(dòng)態(tài)更新框架,只需在項(xiàng)目中引入極小的引擎,就可以使用JavaScript調(diào)用任何Objective-C原生接口,獲得腳本語(yǔ)言的優(yōu)勢(shì):為項(xiàng)目動(dòng)態(tài)添加模塊,或替換項(xiàng)目原生代碼動(dòng)態(tài)修復(fù) bug。
二:基礎(chǔ)原理
JSPatch 能做到通過(guò) JS 調(diào)用和改寫(xiě) OC 方法最根本的原因是 Objective-C 是動(dòng)態(tài)語(yǔ)言,OC 上所有方法的調(diào)用/類(lèi)的生成都通過(guò) Objective-C Runtime 在運(yùn)行時(shí)進(jìn)行,我們可以通過(guò)類(lèi)名/方法名反射得到相應(yīng)的類(lèi)和方法:
Class class = NSClassFromString("UIViewController");
id viewController = [[class alloc] init];
SEL selector = NSSelectorFromString("viewDidLoad");
[viewController performSelector:selector];
也可以替換某個(gè)類(lèi)的方法為新的實(shí)現(xiàn):
Class cls = objc_allocateClassPair(superCls, "JPObject", 0);
objc_registerClassPair(cls);
class_addMethod(cls, selector, implement, typedesc);
三:方法調(diào)用
1. 調(diào)用require('UIView')后,就可以直接使用UIView這個(gè)變量去調(diào)用相應(yīng)的類(lèi)方法了,require 做的事很簡(jiǎn)單,就是在JS全局作用域上創(chuàng)建一個(gè)同名變量,變量指向一個(gè)對(duì)象,對(duì)象屬性__isCls表明這是一個(gè)Class,__clsName保存類(lèi)名,在調(diào)用方法時(shí)會(huì)用到這兩個(gè)屬性。
var _require = function(clsName) {
if (!global[clsName]) {
global[clsName] = {
__isCls: 1,
__clsName: clsName
}
}
return global[clsName]
}
2.封裝JS對(duì)象
_c()元函數(shù):
在 OC 執(zhí)行 JS 腳本前,通過(guò)正則把所有方法調(diào)用都改成調(diào)用__c()函數(shù),再執(zhí)行這個(gè) JS 腳本,做到了類(lèi)似 OC/Lua/Ruby 等的消息轉(zhuǎn)發(fā)機(jī)制:
UIView.alloc().init()
->
UIView.__c('alloc')().__c('init')()
給 JS 對(duì)象基類(lèi) Object 的 prototype 加上__c成員,這樣所有對(duì)象都可以調(diào)用到__c,根據(jù)當(dāng)前對(duì)象類(lèi)型判斷進(jìn)行不同操作:
Object.prototype.__c = function(methodName) {
if (!this.__obj && !this.__clsName) return this[methodName].bind(this);
var self = this
return function(){
var args = Array.prototype.slice.call(arguments)
return _methodFunc(self.__obj, self.__clsName, methodName, args, self.__isSuper)
}
}
_methodFunc()就是把相關(guān)信息傳給OC,OC用 Runtime 接口調(diào)用相應(yīng)方法,返回結(jié)果值,這個(gè)調(diào)用就結(jié)束了。
3.消息傳遞
OC 端在啟動(dòng) JSPatch 引擎時(shí)會(huì)創(chuàng)建一個(gè)JSContext實(shí)例,JSContext是 JS 代碼的執(zhí)行環(huán)境,可以給JSContext添加方法,JS 就可以直接調(diào)用這個(gè)方法:
JSContext *context = [[JSContext alloc] init];
context[@"hello"] = ^(NSString *msg) {
NSLog(@"hello %@", msg);
};
[_context evaluateScript:@"hello('word')"];
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ù)類(lèi)型。
4.對(duì)象持有/轉(zhuǎn)換
結(jié)合上述幾點(diǎn),可以知道UIView.alloc()這個(gè)類(lèi)方法調(diào)用語(yǔ)句是怎樣執(zhí)行的:
a.require('UIView')這句話(huà)在 JS 全局作用域生成了UIView這個(gè)對(duì)象,它有個(gè)屬性叫__isCls,表示這代表一個(gè) OC 類(lèi)。 b.調(diào)用UIView這個(gè)對(duì)象的alloc()方法,會(huì)去到__c()函數(shù),在這個(gè)函數(shù)里判斷到調(diào)用者_(dá)_isCls屬性,知道它是代表 OC 類(lèi),把方法名和類(lèi)名傳遞給 OC 完成調(diào)用。
調(diào)用類(lèi)方法過(guò)程是這樣,那實(shí)例方法呢?UIView.alloc()會(huì)返回一個(gè) UIView 實(shí)例對(duì)象給 JS,這個(gè) OC 實(shí)例對(duì)象在 JS 是怎樣表示的?怎樣可以在 JS 拿到這個(gè)實(shí)例對(duì)象后可以直接調(diào)用它的實(shí)例方法UIView.alloc().init()?
對(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í)釋放。
傳回給 JS 的變量是這個(gè) OC 對(duì)象的指針,這個(gè)指針也可以重新傳回 OC,要在 JS 調(diào)用這個(gè)對(duì)象的某個(gè)實(shí)例方法,根據(jù)第2點(diǎn) JS 接口的描述,只需在__c()函數(shù)里把這個(gè)對(duì)象指針以及它要調(diào)用的方法名傳回給 OC 就行了,現(xiàn)在問(wèn)題只剩下:怎樣在__c()函數(shù)里判斷調(diào)用者是一個(gè) OC 對(duì)象指針?
目前沒(méi)找到方法判斷一個(gè) JS 對(duì)象是否表示 OC 指針,這里的解決方法是在 OC 把對(duì)象返回給 JS 之前,先把它包裝成一個(gè) NSDictionary:
static NSDictionary *_wrapObj(id obj) {
return @{@"__obj": obj};
}
讓 OC 對(duì)象作為這個(gè) NSDictionary 的一個(gè)值,這樣在 JS 里這個(gè)對(duì)象就變成:
{__obj: [OC Object 對(duì)象指針]}
這樣就可以通過(guò)判斷對(duì)象是否有__obj屬性得知這個(gè)對(duì)象是否表示 OC 對(duì)象指針,在__c函數(shù)里若判斷到調(diào)用者有__obj屬性,取出這個(gè)屬性,跟調(diào)用的實(shí)例方法一起傳回給 OC,就完成了實(shí)例方法的調(diào)用。
5.類(lèi)型轉(zhuǎn)換
JS 把要調(diào)用的類(lèi)名/方法名/對(duì)象傳給 OC 后,OC 調(diào)用類(lèi)/對(duì)象相應(yīng)的方法是通過(guò) NSInvocation 實(shí)現(xiàn),要能順利調(diào)用到方法并取得返回值,要做兩件事:
a.取得要調(diào)用的 OC 方法各參數(shù)類(lèi)型,把 JS 傳來(lái)的對(duì)象轉(zhuǎn)為要求的類(lèi)型進(jìn)行調(diào)用。 b.根據(jù)返回值類(lèi)型取出返回值,包裝為對(duì)象傳回給 JS。
OC上,每個(gè)類(lèi)都是這樣一個(gè)結(jié)構(gòu)體:
struct objc_class {
struct objc_class * isa;
const char *name;
….
struct objc_method_list **methodLists; /*方法鏈表*/
};
其中 methodList 方法鏈表里存儲(chǔ)的是 Method 類(lèi)型:
typedef struct objc_method *Method;
typedef struct objc_ method {
SEL method_name;
char *method_types;
IMP method_imp;
};
Method 保存了一個(gè)方法的全部信息,包括 SEL 方法名,type 各參數(shù)和返回值類(lèi)型,IMP 該方法具體實(shí)現(xiàn)的函數(shù)指針。
通過(guò) Selector 調(diào)用方法時(shí),會(huì)從 methodList 鏈表里找到對(duì)應(yīng)Method進(jìn)行調(diào)用,這個(gè) methodList 上的的元素是可以動(dòng)態(tài)替換的,可以把某個(gè) Selector 對(duì)應(yīng)的函數(shù)指針I(yè)MP替換成新的,也可以拿到已有的某個(gè) Selector 對(duì)應(yīng)的函數(shù)指針I(yè)MP,讓另一個(gè) Selector 跟它對(duì)應(yīng),Runtime 提供了一些接口做這些事,以替換 UIViewController 的-viewDidLoad:方法為例:
static void viewDidLoadIMP (id slf, SEL sel) {
JSValue *jsFunction = …;
[jsFunction callWithArguments:nil];
}
Class cls = NSClassFromString(@"UIViewController");
SEL selector = @selector(viewDidLoad);
Method method = class_getInstanceMethod(cls, selector);
//獲得viewDidLoad方法的函數(shù)指針
IMP imp = method_getImplementation(method)
//獲得viewDidLoad方法的參數(shù)類(lèi)型
char *typeDescription = (char *)method_getTypeEncoding(method);
//新增一個(gè)ORIGViewDidLoad方法,指向原來(lái)的viewDidLoad實(shí)現(xiàn)
class_addMethod(cls, @selector(ORIGViewDidLoad), imp, typeDescription);
//把viewDidLoad IMP指向自定義新的實(shí)現(xiàn)
class_replaceMethod(cls, selector, viewDidLoadIMP, typeDescription);
這樣就把 UIViewController 的-viewDidLoad方法給替換成我們自定義的方法,APP里調(diào)用 UIViewController 的viewDidLoad方法都會(huì)去到上述 viewDidLoadIMP 函數(shù)里,在這個(gè)新的IMP函數(shù)里調(diào)用 JS 傳進(jìn)來(lái)的方法,就實(shí)現(xiàn)了替換 viewDidLoad 方法為JS代碼里的實(shí)現(xiàn),同時(shí)為 UIViewController 新增了個(gè)方法-ORIGViewDidLoad指向原來(lái) viewDidLoad 的 IMP,JS 可以通過(guò)這個(gè)方法調(diào)用到原來(lái)的實(shí)現(xiàn)。
方法替換就這樣很簡(jiǎn)單的實(shí)現(xiàn)了,但這么簡(jiǎn)單的前提是,這個(gè)方法沒(méi)有參數(shù)。如果這個(gè)方法有參數(shù),怎樣把參數(shù)值傳給我們新的 IMP 函數(shù)呢?例如 UIViewController 的-viewDidAppear:方法,調(diào)用者會(huì)傳一個(gè) Bool 值,我們需要在自己實(shí)現(xiàn)的IMP(上述的 viewDidLoadIMP)上拿到這個(gè)值,怎樣能拿到?如果只是針對(duì)一個(gè)方法寫(xiě) IMP,是可以直接拿到這個(gè)參數(shù)值的:
static void viewDidAppear (id slf, SEL sel, BOOL animated) {
[function callWithArguments:@(animated)];
}
但我們要的是實(shí)現(xiàn)一個(gè)通用的IMP,任意方法任意參數(shù)都可以通過(guò)這個(gè)IMP中轉(zhuǎn),拿到方法的所有參數(shù)回調(diào)JS的實(shí)現(xiàn)。
以上主要是JSPatch實(shí)現(xiàn)的一些基礎(chǔ)原理,以及代碼展示便于理解;原理很重要,但是也要能做出東西呀!這里我們基于三方的JSPatch做個(gè)展示:
http://jspatch.com ? 這個(gè)是三分的一個(gè)平臺(tái);
通過(guò)引入SDK,倒入相關(guān)的庫(kù),代碼處理起來(lái)很簡(jiǎn)單;
1.在app delegate ?啟動(dòng)中調(diào)用:
[JSPatch startAppWithKey:@""]; //填入自己在該平臺(tái)注冊(cè)app,所獲得的key
#ifdef DEBUG
[JSPatch setupDevelopment];
#endif
[JSPatch sync];
然后在該平臺(tái)設(shè)置設(shè)置自己需要上傳的jspatch文件;
當(dāng)我們?cè)俅芜\(yùn)行代碼的時(shí)候,已編輯的JSPatch文件就可以起作用了;(很可惜,三方就是三方,需要花費(fèi)呀!正常日活少于1萬(wàn),是不要錢(qián)的)!
附錄:
1.基礎(chǔ)語(yǔ)法學(xué)習(xí):?
https://github.com/bang590/JSPatch/wiki/JSPatch-基礎(chǔ)用法
2.常見(jiàn)問(wèn)題
https://github.com/bang590/JSPatch/wiki/JSPatch-常見(jiàn)問(wèn)題
3.懶人快速轉(zhuǎn)換方法:
http://jspatch.com/Tools/convertor ? (實(shí)測(cè)過(guò)這個(gè)轉(zhuǎn)換的方法,對(duì)于一些簡(jiǎn)單的錯(cuò)誤很好用,過(guò)于復(fù)雜的就有點(diǎn)力不從心了,還是需要對(duì)基礎(chǔ)用法有一定認(rèn)識(shí)?。?br>