JSPatch源碼解析

按照常例,是要給demo
JSPatch下載
還有作者詳解

首先搭建第一個(gè)JSPatch項(xiàng)目

1奈揍、下載源碼拖進(jìn)去曲尸,只需要如下目錄

JSPatch目錄

2、創(chuàng)建自己的js文件男翰,然后在js文件中敲下如下代碼

require('UIView, UIColor, UILabel')
defineClass('AppDelegate', {
      // replace the -genView method
        getView: function() {
        var view = self.ORIGgetView();
        view.setBackgroundColor(UIColor.greenColor())
        var label = UILabel.alloc().initWithFrame(view.frame());
        label.setText("JSPatch");
        label.setTextAlignment(1);
        view.addSubview(label);
        return view;
        }
        });

大概意思就是另患,為AppDelegate重寫(xiě)getView方法,方法中調(diào)用ORIGgetView蛾绎,也就是原來(lái)的getview方法昆箕。require就是創(chuàng)建了這幾個(gè)全局變量鸦列,變量指向一個(gè)_clsName為“UIView"的對(duì)象。

require生成類(lèi)對(duì)象時(shí)鹏倘,把類(lèi)名傳入OC薯嗤,OC 通過(guò)runtime方法找出這個(gè)類(lèi)所有的方法返回給 JS,JS 類(lèi)對(duì)象為每個(gè)方法名都生成一個(gè)函數(shù)纤泵,函數(shù)內(nèi)容就是拿著方法名去 OC 調(diào)用相應(yīng)方法骆姐。

3、然后在appdelegate中捏题,調(diào)用玻褪。

 [JPEngine startEngine];
NSString *jsPath = [[NSBundle mainBundle] pathForResource:@"demo" ofType:@"js"];
NSString *script = [NSString stringWithContentsOfFile:jsPath encoding:NSUTF8StringEncoding error:nil];
[JPEngine evaluateScript:script];

這里需要注意的是,在Build Phases 中Copy Bundle Resources 中一定要有demo.js公荧,不然就拿不到了带射。

好,搭建流程就是這么簡(jiǎn)單循狰。具體的怎么新建類(lèi)窟社,或者替換類(lèi)中的方法,可以參考作者gitbub中詳細(xì)的用法介紹晤揣。

JSPatch的原理是運(yùn)用oc的動(dòng)態(tài)性桥爽,所以在讀源碼之前∶潦叮可以先看一下http://www.reibang.com/p/a3f95abc745f ,了解一下OC 中runtime是怎么調(diào)用盗扒,以及偷換運(yùn)行時(shí)方法的(addMethod以及replaceMethod)

從入口開(kāi)始讀源碼

/*!
 @method
 @discussion start the JSPatch engine, execute only once.
 */
+ (void)startEngine;

/*!
 @method
 @description Evaluate Javascript code from a file Path. Call     
  it after +startEngine.
 @param filePath: The filePath of the Javascript code.
 @result The last value generated by the script.
 */
+ (JSValue *)evaluateScriptWithPath:(NSString *)filePath;

剛接觸JSPatch跪楞,只用到了這兩個(gè)方法。

先看第一個(gè)方法

  -(void)startEngine;

具體的源碼可以下載下來(lái)查看侣灶,在源碼里面看到很多相似的東西

比如

JSContext *context = [[JSContext alloc] init];  
context[@"_OC_defineClass"] = ^(NSString *classDeclaration, JSValue *instanceMethods, JSValue *classMethods) {
    return defineClass(classDeclaration, instanceMethods, classMethods);
};

context[@"_OC_defineProtocol"] = ^(NSString *protocolDeclaration, JSValue *instProtocol, JSValue *clsProtocol) {
    return defineProtocol(protocolDeclaration, instProtocol,clsProtocol);
};

JScontext是JavaScriptCore這個(gè)庫(kù)里面的類(lèi)甸祭。暫時(shí)理解一個(gè)js與ios交互的上下文。傳入一個(gè)方法名褥影,js中就可以調(diào)用這個(gè)方法池户,執(zhí)行的就是block中的這段代碼。參數(shù)列表也是從js中調(diào)用方法的時(shí)候傳入凡怎,oc這邊接收校焦。

簡(jiǎn)單看一下jspatch.m中的代碼。

找到j(luò)s中調(diào)用_OC_defineClass的代碼统倒。

  global.defineClass = function(declaration, instMethods,     
clsMethods) {
var newInstMethods = {}, newClsMethods = {}
_formatDefineMethods(instMethods, newInstMethods,declaration)
_formatDefineMethods(clsMethods, newClsMethods,declaration)

var ret = _OC_defineClass(declaration, newInstMethods, newClsMethods)

return require(ret["cls"])
}

global.defineClass這里我理解為寨典,定義了全局的defineClass方法為function(···){};(我在demo.js中調(diào)用的defineclass方法房匆,就算在這里定義的)

然后具體看一下,在defineClass的時(shí)候耸成,都進(jìn)行了哪些操作

先是初始化了兩個(gè)方法的字典對(duì)象(因?yàn)閛c中是運(yùn)行時(shí)發(fā)送消息機(jī)制报亩,所以,一個(gè)方法中需要帶有方法名井氢,方法指針弦追,方法參數(shù),方法接收對(duì)象等參數(shù))花竞。
然后看一下_formatDefineMethods方法中執(zhí)行了什么骗卜,直接貼上源碼

var _formatDefineMethods = function(methods,   
newMethods, declaration) {

//遍歷我們要求覆蓋的方法

 for (var methodName in methods) {
  (function(){
   var originMethod = methods[methodName]
    newMethods[methodName] = [originMethod.length, function() {
//oc轉(zhuǎn)js , arguments在js中代表被傳遞的參數(shù)左胞,這里是為了把參數(shù)轉(zhuǎn)化為js數(shù)組
      var args = _formatOCToJS(Array.prototype.slice.call(arguments))
      var lastSelf = global.self
      var ret;
      try {
        global.self = args[0]
        if (global.self) {
// 把類(lèi)名作為全局變量保存下來(lái)
          global.self.__clsDeclaration = declaration
        }
//刪除第0個(gè)參數(shù)寇仓,也就是self。因?yàn)樵趫?zhí)行的過(guò)程中烤宙,第一個(gè)參數(shù)是消息接收的對(duì)象遍烦,現(xiàn)在需要復(fù)制
這個(gè)方法,所以躺枕,不需要第一個(gè)參數(shù)服猪,因?yàn)檎{(diào)用的對(duì)象可能就不再是self了。
        args.splice(0,1)
 js 中apply
//復(fù)制了originMethod的方法和屬性拐云,我理解為只是更新了參數(shù)罢猪,然后返回方法名。
        ret = originMethod.apply(originMethod, args)
        global.self = lastSelf
      } catch(e) {
        _OC_catch(e.message, e.stack)
      }
      return ret
    }]
  })()
}
}
看以上代碼叉瘩,能夠知道膳帕,是把新方法中的實(shí)現(xiàn)和相關(guān)參數(shù),關(guān)聯(lián)到了老方法中薇缅。也就是生成了一個(gè)方法名和老方法一樣危彩,但是執(zhí)行函數(shù)不一樣的方法(oc用是一個(gè)結(jié)果體),這里生成一個(gè)字典泳桦,在oc中再去拿到相應(yīng)值去處理汤徽。

然后在defineClass方法中,調(diào)用了oc中的方法OC_defineClass(declaration, newInstMethods, newClsMethods)灸撰,具體實(shí)現(xiàn)內(nèi)容可以在源碼中查看谒府,這里,引擎就從js中抽取了我們要覆蓋的類(lèi)浮毯,和方法完疫。然后就交給oc去覆蓋方法。

以下是純oc中的實(shí)現(xiàn)

static NSDictionary *defineClass(NSString *classDeclaration, JSValue *instanceMethods, JSValue *
classMethods)
{
NSDictionary *declarationDict = convertJPDeclarationString(classDeclaration);
NSString *className = declarationDict[@"className"];
NSString *superClassName = declarationDict[@"superClassName"];
//有關(guān)protocol的我直接略過(guò)了亲轨,看著好頭疼趋惨。
NSArray *protocols = [declarationDict[@"protocolNames"] length] ?
[declarationDict[@"protocolNames"] componentsSeparatedByString:@","] : nil;
Class cls = NSClassFromString(className);
if (!cls) {
    Class superCls = NSClassFromString(superClassName);
    if (!superCls) {
        NSCAssert(NO, @"can't find the super class %@", superClassName);
        return @{@"cls": className};
    }
//找到父類(lèi),然后分配內(nèi)存,新建類(lèi)惦蚊,具體用法查閱runtime 的API
    cls = objc_allocateClassPair(superCls, className.UTF8String, 0);
    objc_registerClassPair(cls);
}
for (int i = 0; i < 2; i ++) {(傳進(jìn)來(lái)的參數(shù)中,在前的是實(shí)例方法,在后的是類(lèi)方法)
    BOOL isInstance = i == 0;
// 判斷是實(shí)例方法還是類(lèi)方法器虾?
    JSValue *jsMethods = isInstance ? instanceMethods: classMethods;
//是實(shí)例方法就拿它所屬的類(lèi)讯嫂,是類(lèi)方法就拿它鎖屬的元類(lèi)(oc中,一個(gè)實(shí)例是類(lèi)對(duì)象兆沙,
//他的isa指向它的類(lèi)欧芽,一個(gè)類(lèi)也是類(lèi)(元類(lèi))對(duì)象,它的類(lèi)方法(isa指向元類(lèi))存在于元類(lèi)中)
    Class currCls = isInstance ? cls: objc_getMetaClass(className.UTF8String);
    NSDictionary *methodDict = [jsMethods toDictionary];
//這個(gè)for循環(huán)開(kāi)始遍歷給這個(gè)類(lèi)添加的所有方法葛圃,并覆蓋原有方法
    for (NSString *jsMethodName in methodDict.allKeys) {
        JSValue *jsMethodArr = [jsMethods valueForProperty:jsMethodName];
        int numberOfArg = [jsMethodArr[0] toInt32];
//選擇器名千扔,也就是一個(gè)方法(method)中的方法名
        NSString *selectorName = convertJPSelectorString(jsMethodName);
        if ([selectorName componentsSeparatedByString:@":"].count - 1 < numberOfArg) {
            selectorName = [selectorName stringByAppendingString:@":"];
        }
        JSValue *jsMethod = jsMethodArr[1];
//如果currCls實(shí)現(xiàn)了這個(gè)方法,則override库正,覆蓋
        if (class_respondsToSelector(currCls, NSSelectorFromString(selectorName))) {
//overrideMethod方法中的核心方法是曲楚,replaceMethod,給一個(gè)對(duì)象傳入一個(gè)selector方法名
//和一個(gè)需要覆蓋這個(gè)selector的imp(函數(shù)地址)和相關(guān)的參數(shù)
   overrideMethod(currCls, selectorName, jsMethod, !isInstance, NULL);
        } else {
     BOOL overrided = NO;
  for (NSString *protocolName in protocols) {
     char *types = methodTypesInProtocol(protocolName, selectorName, isInstance, YES);
    if (!types) types = methodTypesInProtocol(protocolName, selectorName, isInstance, NO);
                if (types) {
                    overrideMethod(currCls, selectorName, jsMethod, !isInstance, types);
                    free(types);
                    overrided = YES;
                    break;
                }
            }
            if (!overrided) {
                NSMutableString *typeDescStr = [@"@@:" mutableCopy];
                for (int i = 0; i < numberOfArg; i ++) {
                    [typeDescStr appendString:@"@"];
                }
                overrideMethod(currCls, selectorName, jsMethod, !isInstance, [typeDescStr cStringUsingEncoding:NSUTF8StringEncoding]);
            }
        }
    }
}
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wundeclared-selector"
class_addMethod(cls, @selector(getProp:), (IMP)getPropIMP, "@@:@");
class_addMethod(cls, @selector(setProp:forKey:), (IMP)setPropIMP, "v@:@@");
#pragma clang diagnostic pop
return @{@"cls": className};
}

大概的調(diào)用流程就是這樣的,(js拿到各種參數(shù)->轉(zhuǎn)oc->runtime添加方法)更詳細(xì)的原理以及思考可以在作者github中看到褥符。其他還有很多其他函數(shù)的實(shí)現(xiàn)龙誊。如果要讀,也可以直接從入口startEngine中去看喷楣。

在實(shí)際的操作中趟大,我們需要設(shè)計(jì)一個(gè)下載機(jī)制,然后在通過(guò)作者提供的使用方法铣焊,把需要修復(fù)的類(lèi)的某個(gè)方法替換掉逊朽,就可以實(shí)行熱修復(fù)了。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末曲伊,一起剝皮案震驚了整個(gè)濱河市叽讳,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌熊昌,老刑警劉巖绽榛,帶你破解...
    沈念sama閱讀 217,826評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異婿屹,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)推溃,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,968評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)昂利,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人铁坎,你說(shuō)我怎么就攤上這事蜂奸。” “怎么了硬萍?”我有些...
    開(kāi)封第一講書(shū)人閱讀 164,234評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵扩所,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我朴乖,道長(zhǎng)祖屏,這世上最難降的妖魔是什么助赞? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,562評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮袁勺,結(jié)果婚禮上雹食,老公的妹妹穿的比我還像新娘。我一直安慰自己期丰,他們只是感情好群叶,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,611評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著钝荡,像睡著了一般街立。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上埠通,一...
    開(kāi)封第一講書(shū)人閱讀 51,482評(píng)論 1 302
  • 那天赎离,我揣著相機(jī)與錄音,去河邊找鬼植阴。 笑死蟹瘾,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的掠手。 我是一名探鬼主播憾朴,決...
    沈念sama閱讀 40,271評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼喷鸽!你這毒婦竟也來(lái)了众雷?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,166評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤做祝,失蹤者是張志新(化名)和其女友劉穎砾省,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體混槐,經(jīng)...
    沈念sama閱讀 45,608評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡编兄,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,814評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了声登。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片狠鸳。...
    茶點(diǎn)故事閱讀 39,926評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖悯嗓,靈堂內(nèi)的尸體忽然破棺而出件舵,到底是詐尸還是另有隱情,我是刑警寧澤脯厨,帶...
    沈念sama閱讀 35,644評(píng)論 5 346
  • 正文 年R本政府宣布铅祸,位于F島的核電站,受9級(jí)特大地震影響合武,放射性物質(zhì)發(fā)生泄漏临梗。R本人自食惡果不足惜涡扼,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,249評(píng)論 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望夜焦。 院中可真熱鬧壳澳,春花似錦、人聲如沸茫经。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,866評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)卸伞。三九已至抹镊,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間荤傲,已是汗流浹背垮耳。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,991評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留遂黍,地道東北人终佛。 一個(gè)月前我還...
    沈念sama閱讀 48,063評(píng)論 3 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像雾家,于是被迫代替她去往敵國(guó)和親铃彰。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,871評(píng)論 2 354

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

  • JSPatch是一個(gè)可以在線修復(fù)bug的輕量級(jí)框架芯咧,項(xiàng)目中嵌入這個(gè)框架可以讓你的app具有熱更新的能力牙捉。你可以通過(guò)...
    daixunry閱讀 6,024評(píng)論 5 38
  • 轉(zhuǎn)載:原文鏈接 http://blog.cnbang.net/tech/2808/ JSPatch以小巧的體積做到...
    made_China閱讀 392評(píng)論 0 0
  • http://blog.cnbang.net/tech/2808/ JSPatch實(shí)現(xiàn)原理詳解 注:本文較早撰寫(xiě),...
    hypercode閱讀 1,194評(píng)論 0 1
  • 寶寶今年又大了一歲敬飒,但是說(shuō)話依舊很雷人邪铲,同時(shí)也給我們帶來(lái)了無(wú)限的樂(lè)趣。 一天无拗,我和寶寶在外面吃飯带到,吃飯的地方離家有...
    柳絮XM閱讀 285評(píng)論 4 4
  • 周末阴孟,已經(jīng)連續(xù)一個(gè)多月沒(méi)有休息過(guò)的米粒決定給自己好好的放個(gè)假,好久沒(méi)有專(zhuān)心的陪過(guò)快三歲的女兒税迷。女兒已對(duì)她的說(shuō)辭持...
    臨界紫蘇閱讀 260評(píng)論 1 5