JSPatch源码解析

按照常例,是要给demo
JSPatch下载
还有作者详解

首先搭建第一个JSPatch项目

1、下载源码拖进去,只需要如下目录

JSPatch目录

2、创建自己的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重写getView方法,方法中调用ORIGgetView,也就是原来的getview方法。require就是创建了这几个全局变量,变量指向一个_clsName为“UIView"的对象。

require生成类对象时,把类名传入OC,OC 通过runtime方法找出这个类所有的方法返回给 JS,JS 类对象为每个方法名都生成一个函数,函数内容就是拿着方法名去 OC 调用相应方法。

3、然后在appdelegate中,调用。

 [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,不然就拿不到了。

好,搭建流程就是这么简单。具体的怎么新建类,或者替换类中的方法,可以参考作者gitbub中详细的用法介绍。

JSPatch的原理是运用oc的动态性,所以在读源码之前。可以先看一下http://www.jianshu.com/p/a3f95abc745f ,了解一下OC 中runtime是怎么调用,以及偷换运行时方法的(addMethod以及replaceMethod)

从入口开始读源码

/*!
 @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,只用到了这两个方法。

先看第一个方法

  -(void)startEngine;

具体的源码可以下载下来查看,在源码里面看到很多相似的东西

比如

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这个库里面的类。暂时理解一个js与ios交互的上下文。传入一个方法名,js中就可以调用这个方法,执行的就是block中的这段代码。参数列表也是从js中调用方法的时候传入,oc这边接收。

简单看一下jspatch.m中的代码。

找到js中调用_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中调用的defineclass方法,就算在这里定义的)

然后具体看一下,在defineClass的时候,都进行了哪些操作

先是初始化了两个方法的字典对象(因为oc中是运行时发送消息机制,所以,一个方法中需要带有方法名,方法指针,方法参数,方法接收对象等参数)。
然后看一下_formatDefineMethods方法中执行了什么,直接贴上源码

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

//遍历我们要求覆盖的方法

 for (var methodName in methods) {
  (function(){
   var originMethod = methods[methodName]
    newMethods[methodName] = [originMethod.length, function() {
//oc转js , arguments在js中代表被传递的参数,这里是为了把参数转化为js数组
      var args = _formatOCToJS(Array.prototype.slice.call(arguments))
      var lastSelf = global.self
      var ret;
      try {
        global.self = args[0]
        if (global.self) {
// 把类名作为全局变量保存下来
          global.self.__clsDeclaration = declaration
        }
//删除第0个参数,也就是self。因为在执行的过程中,第一个参数是消息接收的对象,现在需要复制
这个方法,所以,不需要第一个参数,因为调用的对象可能就不再是self了。
        args.splice(0,1)
 js 中apply
//复制了originMethod的方法和属性,我理解为只是更新了参数,然后返回方法名。
        ret = originMethod.apply(originMethod, args)
        global.self = lastSelf
      } catch(e) {
        _OC_catch(e.message, e.stack)
      }
      return ret
    }]
  })()
}
}
看以上代码,能够知道,是把新方法中的实现和相关参数,关联到了老方法中。也就是生成了一个方法名和老方法一样,但是执行函数不一样的方法(oc用是一个结果体),这里生成一个字典,在oc中再去拿到相应值去处理。

然后在defineClass方法中,调用了oc中的方法OC_defineClass(declaration, newInstMethods, newClsMethods),具体实现内容可以在源码中查看,这里,引擎就从js中抽取了我们要覆盖的类,和方法。然后就交给oc去覆盖方法。

以下是纯oc中的实现

static NSDictionary *defineClass(NSString *classDeclaration, JSValue *instanceMethods, JSValue *
classMethods)
{
NSDictionary *declarationDict = convertJPDeclarationString(classDeclaration);
NSString *className = declarationDict[@"className"];
NSString *superClassName = declarationDict[@"superClassName"];
//有关protocol的我直接略过了,看着好头疼。
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};
    }
//找到父类,然后分配内存,新建类,具体用法查阅runtime 的API
    cls = objc_allocateClassPair(superCls, className.UTF8String, 0);
    objc_registerClassPair(cls);
}
for (int i = 0; i < 2; i ++) {(传进来的参数中,在前的是实例方法,在后的是类方法)
    BOOL isInstance = i == 0;
// 判断是实例方法还是类方法?
    JSValue *jsMethods = isInstance ? instanceMethods: classMethods;
//是实例方法就拿它所属的类,是类方法就拿它锁属的元类(oc中,一个实例是类对象,
//他的isa指向它的类,一个类也是类(元类)对象,它的类方法(isa指向元类)存在于元类中)
    Class currCls = isInstance ? cls: objc_getMetaClass(className.UTF8String);
    NSDictionary *methodDict = [jsMethods toDictionary];
//这个for循环开始遍历给这个类添加的所有方法,并覆盖原有方法
    for (NSString *jsMethodName in methodDict.allKeys) {
        JSValue *jsMethodArr = [jsMethods valueForProperty:jsMethodName];
        int numberOfArg = [jsMethodArr[0] toInt32];
//选择器名,也就是一个方法(method)中的方法名
        NSString *selectorName = convertJPSelectorString(jsMethodName);
        if ([selectorName componentsSeparatedByString:@":"].count - 1 < numberOfArg) {
            selectorName = [selectorName stringByAppendingString:@":"];
        }
        JSValue *jsMethod = jsMethodArr[1];
//如果currCls实现了这个方法,则override,覆盖
        if (class_respondsToSelector(currCls, NSSelectorFromString(selectorName))) {
//overrideMethod方法中的核心方法是,replaceMethod,给一个对象传入一个selector方法名
//和一个需要覆盖这个selector的imp(函数地址)和相关的参数
   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};
}

大概的调用流程就是这样的,(js拿到各种参数->转oc->runtime添加方法)更详细的原理以及思考可以在作者github中看到。其他还有很多其他函数的实现。如果要读,也可以直接从入口startEngine中去看。

在实际的操作中,我们需要设计一个下载机制,然后在通过作者提供的使用方法,把需要修复的类的某个方法替换掉,就可以实行热修复了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,053评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,527评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,779评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,685评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,699评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,609评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,989评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,654评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,890评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,634评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,716评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,394评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,976评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,950评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,191评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,849评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,458评论 2 342

推荐阅读更多精彩内容

  • JSPatch是一个可以在线修复bug的轻量级框架,项目中嵌入这个框架可以让你的app具有热更新的能力。你可以通过...
    daixunry阅读 5,999评论 5 38
  • 转载:原文链接 http://blog.cnbang.net/tech/2808/ JSPatch以小巧的体积做到...
    made_China阅读 390评论 0 0
  • http://blog.cnbang.net/tech/2808/ JSPatch实现原理详解 注:本文较早撰写,...
    hypercode阅读 1,179评论 0 1
  • 宝宝今年又大了一岁,但是说话依旧很雷人,同时也给我们带来了无限的乐趣。 一天,我和宝宝在外面吃饭,吃饭的地方离家有...
    柳絮XM阅读 278评论 4 4
  • 周末,已经连续一个多月没有休息过的米粒决定给自己好好的放个假,好久没有专心的陪过快三岁的女儿。女儿已对她的说辞持...
    临界紫苏阅读 250评论 1 5