IOS關于熱修復JSPatch

一:關于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 ? (實測過這個轉換的方法秕豫,對于一些簡單的錯誤很好用朴艰,過于復雜的就有點力不從心了观蓄,還是需要對基礎用法有一定認識!)

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末祠墅,一起剝皮案震驚了整個濱河市侮穿,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌毁嗦,老刑警劉巖亲茅,帶你破解...
    沈念sama閱讀 218,607評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異狗准,居然都是意外死亡克锣,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,239評論 3 395
  • 文/潘曉璐 我一進店門腔长,熙熙樓的掌柜王于貴愁眉苦臉地迎上來袭祟,“玉大人,你說我怎么就攤上這事捞附〗砣椋” “怎么了?”我有些...
    開封第一講書人閱讀 164,960評論 0 355
  • 文/不壞的土叔 我叫張陵鸟召,是天一觀的道長胆绊。 經(jīng)常有香客問我,道長欧募,這世上最難降的妖魔是什么压状? 我笑而不...
    開封第一講書人閱讀 58,750評論 1 294
  • 正文 為了忘掉前任,我火速辦了婚禮跟继,結果婚禮上种冬,老公的妹妹穿的比我還像新娘。我一直安慰自己还栓,他們只是感情好碌廓,可當我...
    茶點故事閱讀 67,764評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著剩盒,像睡著了一般谷婆。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上辽聊,一...
    開封第一講書人閱讀 51,604評論 1 305
  • 那天纪挎,我揣著相機與錄音,去河邊找鬼跟匆。 笑死异袄,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的玛臂。 我是一名探鬼主播烤蜕,決...
    沈念sama閱讀 40,347評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼封孙,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了讽营?” 一聲冷哼從身側響起虎忌,我...
    開封第一講書人閱讀 39,253評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎橱鹏,沒想到半個月后膜蠢,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,702評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡莉兰,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,893評論 3 336
  • 正文 我和宋清朗相戀三年挑围,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片糖荒。...
    茶點故事閱讀 40,015評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡杉辙,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出寂嘉,到底是詐尸還是另有隱情奏瞬,我是刑警寧澤,帶...
    沈念sama閱讀 35,734評論 5 346
  • 正文 年R本政府宣布泉孩,位于F島的核電站,受9級特大地震影響并淋,放射性物質(zhì)發(fā)生泄漏寓搬。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,352評論 3 330
  • 文/蒙蒙 一县耽、第九天 我趴在偏房一處隱蔽的房頂上張望句喷。 院中可真熱鬧,春花似錦兔毙、人聲如沸唾琼。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,934評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽锡溯。三九已至,卻和暖如春哑姚,著一層夾襖步出監(jiān)牢的瞬間祭饭,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,052評論 1 270
  • 我被黑心中介騙來泰國打工叙量, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留倡蝙,地道東北人。 一個月前我還...
    沈念sama閱讀 48,216評論 3 371
  • 正文 我出身青樓绞佩,卻偏偏與公主長得像寺鸥,于是被迫代替她去往敵國和親猪钮。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,969評論 2 355

推薦閱讀更多精彩內(nèi)容