IOS關(guān)于熱修復(fù)JSPatch

一:關(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>

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容