JSPatch源码解析

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

首先搭建第一个JSPatch项目

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

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中去看。

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

你可能感兴趣的:(JSPatch源码解析)