一:關于JSPatch
JSPatch : 是一個iOS動態(tài)更新框架议惰,只需在項目中引入極小的引擎,就可以使用JavaScript調(diào)用任何Objective-C原生接口劈猪,獲得腳本語言的優(yōu)勢:為項目動態(tài)添加模塊捡需,或替換項目原生代碼動態(tài)修復 bug愈魏。
二:基礎原理
JSPatch 能做到通過 JS 調(diào)用和改寫 OC 方法最根本的原因是 Objective-C 是動態(tài)語言辣之,OC 上所有方法的調(diào)用/類的生成都通過 Objective-C Runtime 在運行時進行掰伸,我們可以通過類名/方法名反射得到相應的類和方法:
Class class = NSClassFromString("UIViewController");
id viewController = [[class alloc] init];
SEL selector = NSSelectorFromString("viewDidLoad");
[viewController performSelector:selector];
也可以替換某個類的方法為新的實現(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這個變量去調(diào)用相應的類方法了怀估,require 做的事很簡單狮鸭,就是在JS全局作用域上創(chuàng)建一個同名變量,變量指向一個對象,對象屬性__isCls表明這是一個Class怕篷,__clsName保存類名历筝,在調(diào)用方法時會用到這兩個屬性酗昼。
var _require = function(clsName) {
if (!global[clsName]) {
global[clsName] = {
__isCls: 1,
__clsName: clsName
}
}
return global[clsName]
}
2.封裝JS對象
_c()元函數(shù):
在 OC 執(zhí)行 JS 腳本前廊谓,通過正則把所有方法調(diào)用都改成調(diào)用__c()函數(shù),再執(zhí)行這個 JS 腳本麻削,做到了類似 OC/Lua/Ruby 等的消息轉發(fā)機制:
UIView.alloc().init()
->
UIView.__c('alloc')().__c('init')()
給 JS 對象基類 Object 的 prototype 加上__c成員蒸痹,這樣所有對象都可以調(diào)用到__c,根據(jù)當前對象類型判斷進行不同操作:
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()就是把相關信息傳給OC呛哟,OC用 Runtime 接口調(diào)用相應方法叠荠,返回結果值,這個調(diào)用就結束了扫责。
3.消息傳遞
OC 端在啟動 JSPatch 引擎時會創(chuàng)建一個JSContext實例榛鼎,JSContext是 JS 代碼的執(zhí)行環(huán)境,可以給JSContext添加方法鳖孤,JS 就可以直接調(diào)用這個方法:
JSContext *context = [[JSContext alloc] init];
context[@"hello"] = ^(NSString *msg) {
NSLog(@"hello %@", msg);
};
[_context evaluateScript:@"hello('word')"];
JS 通過調(diào)用JSContext定義的方法把數(shù)據(jù)傳給 OC者娱,OC 通過返回值傳會給 JS。調(diào)用這種方法苏揣,它的參數(shù)/返回值 JavaScriptCore 都會自動轉換黄鳍,OC 里的 NSArray, NSDictionary, NSString, NSNumber, NSBlock 會分別轉為JS端的數(shù)組/對象/字符串/數(shù)字/函數(shù)類型。
4.對象持有/轉換
結合上述幾點平匈,可以知道UIView.alloc()這個類方法調(diào)用語句是怎樣執(zhí)行的:
a.require('UIView')這句話在 JS 全局作用域生成了UIView這個對象框沟,它有個屬性叫__isCls,表示這代表一個 OC 類增炭。 b.調(diào)用UIView這個對象的alloc()方法忍燥,會去到__c()函數(shù),在這個函數(shù)里判斷到調(diào)用者__isCls屬性隙姿,知道它是代表 OC 類梅垄,把方法名和類名傳遞給 OC 完成調(diào)用。
調(diào)用類方法過程是這樣孟辑,那實例方法呢哎甲?UIView.alloc()會返回一個 UIView 實例對象給 JS,這個 OC 實例對象在 JS 是怎樣表示的饲嗽?怎樣可以在 JS 拿到這個實例對象后可以直接調(diào)用它的實例方法UIView.alloc().init()炭玫?
對于一個自定義id對象,JavaScriptCore 會把這個自定義對象的指針傳給 JS貌虾,這個對象在 JS 無法使用吞加,但在回傳給 OC 時 OC 可以找到這個對象。對于這個對象生命周期的管理,按我的理解如果JS有變量引用時衔憨,這個 OC 對象引用計數(shù)就加1 叶圃,JS 變量的引用釋放了就減1,如果 OC 上沒別的持有者践图,這個OC對象的生命周期就跟著 JS 走了掺冠,會在 JS 進行垃圾回收時釋放。
傳回給 JS 的變量是這個 OC 對象的指針码党,這個指針也可以重新傳回 OC德崭,要在 JS 調(diào)用這個對象的某個實例方法,根據(jù)第2點 JS 接口的描述揖盘,只需在__c()函數(shù)里把這個對象指針以及它要調(diào)用的方法名傳回給 OC 就行了眉厨,現(xiàn)在問題只剩下:怎樣在__c()函數(shù)里判斷調(diào)用者是一個 OC 對象指針?
目前沒找到方法判斷一個 JS 對象是否表示 OC 指針兽狭,這里的解決方法是在 OC 把對象返回給 JS 之前憾股,先把它包裝成一個 NSDictionary:
static NSDictionary *_wrapObj(id obj) {
return @{@"__obj": obj};
}
讓 OC 對象作為這個 NSDictionary 的一個值,這樣在 JS 里這個對象就變成:
{__obj: [OC Object 對象指針]}
這樣就可以通過判斷對象是否有__obj屬性得知這個對象是否表示 OC 對象指針箕慧,在__c函數(shù)里若判斷到調(diào)用者有__obj屬性服球,取出這個屬性,跟調(diào)用的實例方法一起傳回給 OC销钝,就完成了實例方法的調(diào)用有咨。
5.類型轉換
JS 把要調(diào)用的類名/方法名/對象傳給 OC 后,OC 調(diào)用類/對象相應的方法是通過 NSInvocation 實現(xiàn)蒸健,要能順利調(diào)用到方法并取得返回值座享,要做兩件事:
a.取得要調(diào)用的 OC 方法各參數(shù)類型,把 JS 傳來的對象轉為要求的類型進行調(diào)用似忧。 b.根據(jù)返回值類型取出返回值渣叛,包裝為對象傳回給 JS。
OC上盯捌,每個類都是這樣一個結構體:
struct objc_class {
struct objc_class * isa;
const char *name;
….
struct objc_method_list **methodLists; /*方法鏈表*/
};
其中 methodList 方法鏈表里存儲的是 Method 類型:
typedef struct objc_method *Method;
typedef struct objc_ method {
SEL method_name;
char *method_types;
IMP method_imp;
};
Method 保存了一個方法的全部信息淳衙,包括 SEL 方法名,type 各參數(shù)和返回值類型饺著,IMP 該方法具體實現(xiàn)的函數(shù)指針箫攀。
通過 Selector 調(diào)用方法時,會從 methodList 鏈表里找到對應Method進行調(diào)用幼衰,這個 methodList 上的的元素是可以動態(tài)替換的靴跛,可以把某個 Selector 對應的函數(shù)指針I(yè)MP替換成新的,也可以拿到已有的某個 Selector 對應的函數(shù)指針I(yè)MP渡嚣,讓另一個 Selector 跟它對應梢睛,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ù)類型
char *typeDescription = (char *)method_getTypeEncoding(method);
//新增一個ORIGViewDidLoad方法,指向原來的viewDidLoad實現(xiàn)
class_addMethod(cls, @selector(ORIGViewDidLoad), imp, typeDescription);
//把viewDidLoad IMP指向自定義新的實現(xiàn)
class_replaceMethod(cls, selector, viewDidLoadIMP, typeDescription);
這樣就把 UIViewController 的-viewDidLoad方法給替換成我們自定義的方法绝葡,APP里調(diào)用 UIViewController 的viewDidLoad方法都會去到上述 viewDidLoadIMP 函數(shù)里深碱,在這個新的IMP函數(shù)里調(diào)用 JS 傳進來的方法,就實現(xiàn)了替換 viewDidLoad 方法為JS代碼里的實現(xiàn)藏畅,同時為 UIViewController 新增了個方法-ORIGViewDidLoad指向原來 viewDidLoad 的 IMP敷硅,JS 可以通過這個方法調(diào)用到原來的實現(xiàn)。
方法替換就這樣很簡單的實現(xiàn)了墓赴,但這么簡單的前提是竞膳,這個方法沒有參數(shù)。如果這個方法有參數(shù)诫硕,怎樣把參數(shù)值傳給我們新的 IMP 函數(shù)呢?例如 UIViewController 的-viewDidAppear:方法刊侯,調(diào)用者會傳一個 Bool 值章办,我們需要在自己實現(xiàn)的IMP(上述的 viewDidLoadIMP)上拿到這個值,怎樣能拿到滨彻?如果只是針對一個方法寫 IMP藕届,是可以直接拿到這個參數(shù)值的:
static void viewDidAppear (id slf, SEL sel, BOOL animated) {
[function callWithArguments:@(animated)];
}
但我們要的是實現(xiàn)一個通用的IMP,任意方法任意參數(shù)都可以通過這個IMP中轉亭饵,拿到方法的所有參數(shù)回調(diào)JS的實現(xiàn)休偶。
以上主要是JSPatch實現(xiàn)的一些基礎原理,以及代碼展示便于理解辜羊;原理很重要踏兜,但是也要能做出東西呀!這里我們基于三方的JSPatch做個展示:
http://jspatch.com ? 這個是三分的一個平臺八秃;
通過引入SDK,倒入相關的庫碱妆,代碼處理起來很簡單;
1.在app delegate ?啟動中調(diào)用:
[JSPatch startAppWithKey:@""]; //填入自己在該平臺注冊app昔驱,所獲得的key
#ifdef DEBUG
[JSPatch setupDevelopment];
#endif
[JSPatch sync];
然后在該平臺設置設置自己需要上傳的jspatch文件疹尾;
當我們再次運行代碼的時候,已編輯的JSPatch文件就可以起作用了骤肛;(很可惜纳本,三方就是三方,需要花費呀腋颠!正常日活少于1萬繁成,是不要錢的)!
附錄:
1.基礎語法學習:?
https://github.com/bang590/JSPatch/wiki/JSPatch-基礎用法
2.常見問題
https://github.com/bang590/JSPatch/wiki/JSPatch-常見問題
3.懶人快速轉換方法:
http://jspatch.com/Tools/convertor ? (實測過這個轉換的方法秕豫,對于一些簡單的錯誤很好用朴艰,過于復雜的就有點力不從心了观蓄,還是需要對基礎用法有一定認識!)