了解了 JavaScriptCore 的使用方式,本篇从 demo 入手对 JSPatch 关键源码进行解读。

关于 JSPatch 的实现原理,JSPatch 作者本人 bang 已经有一系列文章阐述:

这些文章是对 JSPatch 内部实现原理和细节诸如“require实现”、“property实现”、“self/super 关键字”、“nil处理”、“内存问题”等具体设计思路和解决方案的阐述,并没有对 JSPatch 源码进行解读。在未接触源码、不清楚整个热修复流程的情况下去读这几篇文章难免一头雾水,最好的方法是边读源码边对照上述文章,代码中不理解的地方可以去文章中寻找答案。
本文将从一个小demo入手,跟踪代码执行流程,从Cocoa层、JavaScript层、Native层对热修复流程中涉及到的重要步骤和函数进行解析。

JSPatch 使用流程

引入JSPatch,JSPatch 核心部分只有三个文件,十分精巧:

建立一个小demo,在ViewController屏幕中央放置一个button,button 点击事件为空:

// in ViewController.m -----------------
- (IBAction)handle:(id)sender {

}

热修复js文件(main.js)内容就是添加这个点击事件(弹出一个AlertView):

// in main.js ------------------------
defineClass('ViewController', {
  handle: function(sender) {
    require('UIAlertView');
    var alert = UIAlertView.alloc().initWithTitle_message_delegate_cancelButtonTitle_otherButtonTitles("alert", null, null, "ok", null, null);
    alert.show();
  }
})

js 文件编写方法查看JSPatch 基础用法

didFinishLaunchingWithOptions:中开启 JSPatch 引擎、执行 js 脚本:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {

    // 开启 JPEngine.
    [JPEngine startEngine];

    NSString *sourcePath = [[NSBundle mainBundle] pathForResource:@"main" ofType:@"js"];
    NSString *script = [NSString stringWithContentsOfFile:sourcePath encoding:NSUTF8StringEncoding error:nil];

    // 执行js脚本代码.
    [JPEngine evaluateScript:script];

    return YES;
}

修复成功!

修复 step 1:startEngine

[JPEngine startEngine];

该方法向JSContext环境注册了一系列供js调用oc方法的block,这些 block 内部大多是 调用 runtime 相关接口的 static 函数。最终读取JSPatch.js中的代码到JSContext环境,使得main.js可以调用JSPatch.js中定义的方法。
调用关系大致如下:

main.js --->  JSPatch.js ---> OC Block ---> runtime 

源码解读:

+ (void)startEngine
{

//    1.判断是否存在 JSContext 类. ---> iOS 7.0 以下不支持 JavaScriptCore
    if (![JSContext class] || _context) {
        return;
    }

//    2.创建一个 JS 运行环境.
    JSContext *context = [[JSContext alloc] init];

//    3.为了使 JSPatch.js 可以访问 JPEngine 中定义的 C 函数,需为 context 注册 block.

//    3.1 创建类.
    context[@"_OC_defineClass"] = ^(NSString *classDeclaration, JSValue *instanceMethods, JSValue *classMethods) {
        return defineClass(classDeclaration, instanceMethods, classMethods);
    };

//    3.2 给类实现某协议.
    context[@"_OC_defineProtocol"] = ^(NSString *protocolDeclaration, JSValue *instProtocol, JSValue *clsProtocol) {
        return defineProtocol(protocolDeclaration, instProtocol,clsProtocol);
    };

//    3.3 js调用oc的实例方法.
    context[@"_OC_callI"] = ^id(JSValue *obj, NSString *selectorName, JSValue *arguments, BOOL isSuper) {
        return callSelector(nil, selectorName, arguments, obj, isSuper);
    };

//    3.4 js调用oc的类方法.
    context[@"_OC_callC"] = ^id(NSString *className, NSString *selectorName, JSValue *arguments) {
        return callSelector(className, selectorName, arguments, nil, NO);
    };

//    3.5 js 对象转 oc 对象.
    context[@"_OC_formatJSToOC"] = ^id(JSValue *obj) {
        return formatJSToOC(obj);
    };

//    3.6 oc 对象 转 js 对象.
    context[@"_OC_formatOCToJS"] = ^id(JSValue *obj) {
        return formatOCToJS([obj toObject]);
    };

//    3.7 获取对象的动态成员变量.
    context[@"_OC_getCustomProps"] = ^id(JSValue *obj) {
        id realObj = formatJSToOC(obj);
        return objc_getAssociatedObject(realObj, kPropAssociatedObjectKey);
    };

//    3.8 给对象动态添加成员变量.
    context[@"_OC_setCustomProps"] = ^(JSValue *obj, JSValue *val) {
        id realObj = formatJSToOC(obj);
        objc_setAssociatedObject(realObj, kPropAssociatedObjectKey, val, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    };

//    3.9 给 js 对象设置 weak.
    context[@"__weak"] = ^id(JSValue *jsval) {
        id obj = formatJSToOC(jsval);
        return [[JSContext currentContext][@"_formatOCToJS"] callWithArguments:@[formatOCToJS([JPBoxing boxWeakObj:obj])]];
    };

//    3.10 给 js 对象设置 strong.
    context[@"__strong"] = ^id(JSValue *jsval) {
        id obj = formatJSToOC(jsval);
        return [[JSContext currentContext][@"_formatOCToJS"] callWithArguments:@[formatOCToJS(obj)]];
    };

//    3.11 获取 oc 对象超类.
    context[@"_OC_superClsName"] = ^(NSString *clsName) {
        Class cls = NSClassFromString(clsName);
        return NSStringFromClass([cls superclass]);
    };

//    3.12 是否自动转换类型.
    context[@"autoConvertOCType"] = ^(BOOL autoConvert) {
        _autoConvert = autoConvert;
    };

//    3.13 oc number 转换为 string.
    context[@"convertOCNumberToString"] = ^(BOOL convertOCNumberToString) {
        _convertOCNumberToString = convertOCNumberToString;
    };

//    3.14 在JS中调用include方法,可以在一个JS文件中加载其他JS文件.
    context[@"include"] = ^(NSString *filePath) {
        NSString *absolutePath = [_scriptRootDir stringByAppendingPathComponent:filePath];
        if (!_runnedScript) {
            _runnedScript = [[NSMutableSet alloc] init];
        }
        if (absolutePath && ![_runnedScript containsObject:absolutePath]) {
            [JPEngine _evaluateScriptWithPath:absolutePath];
            [_runnedScript addObject:absolutePath];
        }
    };

//    3.15 获取资源文件路径.
    context[@"resourcePath"] = ^(NSString *filePath) {
        return [_scriptRootDir stringByAppendingPathComponent:filePath];
    };

//    3.16 让 js 方法延迟执行.
    context[@"dispatch_after"] = ^(double time, JSValue *func) {
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(time * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            [func callWithArguments:nil];
        });
    };

//    3.17 让js方法在 main queue dispatch async 执行.
    context[@"dispatch_async_main"] = ^(JSValue *func) {
        dispatch_async(dispatch_get_main_queue(), ^{
            [func callWithArguments:nil];
        });
    };

//    3.18 让js方法在 main queue dispatch sync 执行.
    context[@"dispatch_sync_main"] = ^(JSValue *func) {
        if ([NSThread currentThread].isMainThread) {
            [func callWithArguments:nil];
        } else {
            dispatch_sync(dispatch_get_main_queue(), ^{
                [func callWithArguments:nil];
            });
        }
    };

//    3.19 让js方法在 global queue dispatch async 执行.
    context[@"dispatch_async_global_queue"] = ^(JSValue *func) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            [func callWithArguments:nil];
        });
    };

//    3.20 释放js创建的oc对象.
    context[@"releaseTmpObj"] = ^void(JSValue *jsVal) {
        if ([[jsVal toObject] isKindOfClass:[NSDictionary class]]) {
            void *pointer =  [(JPBoxing *)([jsVal toObject][@"__obj"]) unboxPointer];
            id obj = *((__unsafe_unretained id *)pointer);
            @synchronized(_TMPMemoryPool) {
                [_TMPMemoryPool removeObjectForKey:[NSNumber numberWithInteger:[(NSObject*)obj hash]]];
            }
        }
    };

//    3.21 js调用oc方法进行打印.
    context[@"_OC_log"] = ^() {
        NSArray *args = [JSContext currentArguments];
        for (JSValue *jsVal in args) {
            id obj = formatJSToOC(jsVal);
            NSLog(@"JSPatch.log: %@", obj == _nilObj ? nil : (obj == _nullObj ? [NSNull null]: obj));
        }
    };

//    3.22 将js捕捉到的异常交给oc方法处理.
    context[@"_OC_catch"] = ^(JSValue *msg, JSValue *stack) {
        _exceptionBlock([NSString stringWithFormat:@"js exception, \nmsg: %@, \nstack: \n %@", [msg toObject], [stack toObject]]);
    };

//    4. 注册 JSContext 执行出现异常时的回调.
    context.exceptionHandler = ^(JSContext *con, JSValue *exception) {
        NSLog(@"%@", exception);
        _exceptionBlock([NSString stringWithFormat:@"js exception: %@", exception]);
    };

//    5. 创建OC中的null对象,转换成js的null对象,并设置到JSContext实例让js代码可以获取.
    _nullObj = [[NSObject alloc] init];
    context[@"_OC_null"] = formatOCToJS(_nullObj);

//    6. 保存 context.
    _context = context;

//    7. oc 中的 nil 对象.
    _nilObj = [[NSObject alloc] init];

//    8. 同步锁.
    _JSMethodSignatureLock = [[NSLock alloc] init];
    _JSMethodForwardCallLock = [[NSRecursiveLock alloc] init];

//    9. 在 JSPatch 中注册过的结构体定义(键:结构体名).
    _registeredStruct = [[NSMutableDictionary alloc] init];

//    10. 注册内存警告通知.
#if TARGET_OS_IPHONE
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleMemoryWarning) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
#endif

//    11. 读取JSPatch.js,方便传入的js代码中使用JSPatch.js提供的函数.
    NSString *path = [[NSBundle bundleForClass:[self class]] pathForResource:@"JSPatch" ofType:@"js"];
    if (!path) _exceptionBlock(@"can't find JSPatch.js");
    NSString *jsCore = [[NSString alloc] initWithData:[[NSFileManager defaultManager] contentsAtPath:path] encoding:NSUTF8StringEncoding];

//    12. 加载 JSPatch.js 中的所有 js 代码到JSContext.
    if ([_context respondsToSelector:@selector(evaluateScript:withSourceURL:)]) {
        [_context evaluateScript:jsCore withSourceURL:[NSURL URLWithString:@"JSPatch.js"]];
    } else {
        [_context evaluateScript:jsCore];
    }
}

一张图总结 JSPatch 的功能结构:

修复 step 2:__c()元函数

接下来读取main.js代码后执行:

[JPEngine evaluateScript:script];

该接口并非直接将main.js代码提交到JSContext环境执行,而是先调用_evaluateScript: withSourceURL:方法对main.js原始代码做些修改。
源码解读:

+ (JSValue *)_evaluateScript:(NSString *)script withSourceURL:(NSURL *)resourceURL
{
//    1. script 不存在或当前 iOS 版本低于 7.0 退出.
    if (!script || ![JSContext class]) {
        _exceptionBlock(@"script is nil");
        return nil;
    }
    [self startEngine];

//    2. 正则式构建 (?<!\\\\)\\.\\s*(\\w+)\\s*\\(
    if (!_regex) {
        _regex = [NSRegularExpression regularExpressionWithPattern:_regexStr options:0 error:nil];
    }

//    3. 使用正则式处理 传入的 js代码 >>> 将 alloc()这样的函数调用 替换成 __c("alloc")()
    NSString *formatedScript = [NSString stringWithFormat:@";(function(){try{%@}catch(e){_OC_catch(e.message, e.stack)}})();", [_regex stringByReplacingMatchesInString:script options:0 range:NSMakeRange(0, script.length) withTemplate:_replaceStr]];

//    4.将正则处理后的js代码加载到 context 执行.(进入 JavaScriptCore)
    @try {
        if ([_context respondsToSelector:@selector(evaluateScript:withSourceURL:)]) {
            return [_context evaluateScript:formatedScript withSourceURL:resourceURL];
        } else {
            return [_context evaluateScript:formatedScript];
        }
    }
    @catch (NSException *exception) {
        _exceptionBlock([NSString stringWithFormat:@"%@", exception]);
    }
    return nil;
}

断点调试看一下script经正则处理之后的结果:

;(function(){try{defineClass('ViewController', {
  pushAlertView: function(sender) {
    require('UIAlertView');
    var alert = UIAlertView.__c("alloc")().__c("initWithTitle_message_delegate_cancelButtonTitle_otherButtonTitles")("alert", null, null, "ok", null, null);
    alert.__c("show")();
  }
})}catch(e){_OC_catch(e.message, e.stack)}})();

除了添加一些关键字和异常处理外,最大的变化在于所有函数调用变成了__c("function")的形式。据作者讲这是JSPatch开发过程中最核心的问题,该问题的解决方案也是JSPatch中最精妙之处。
我们进行热修复期望的效果是这样:

但JS 对于调用没定义的属性/变量,只会马上抛出异常,而不像 OC/Lua/ruby 那样有转发机制。因此对于用户传入的js代码中,类似UIView().alloc().init()这样的代码,js其实根本没办法进行处理。

一种解决方案是实现所有js类继承机制,每一个类和方法都事先定义好:

这种方案是不太现实的,为了调用某个方法需要把该类的所有方法都引进来,占用内存极高(NSObject类有将近1000个方法)。

作者最终想出了第二种方案:

在 OC 执行 JS 脚本前,通过正则把所有方法调用都改成调用 __c() 函数,再执行这个 JS 脚本,做到了类似 OC/Lua/Ruby 等的消息转发机制。

UIView.alloc().init()
->
UIView.__c('alloc')().__c('init')()


给 JS 对象基类 Object 的 prototype 加上 c 成员,这样所有对象都可以调用到 c,根据当前对象类型判断进行不同操作:

__c: function(methodName) {
      ...
      ...
      return function(){
        var args = Array.prototype.slice.call(arguments)
        return _methodFunc(slf.__obj, slf.__clsName, methodName, args, slf.__isSuper)
      }
    }

_methodFunc() 把相关信息传给OC,OC用 Runtime 接口调用相应方法,返回结果值,这个调用就结束了。
源码解读:

 /**
   *  instance: 对象
   *  clsName: 类名
   *  methodName: 方法名
   *  args: 参数列表
   *  isSuper: 是否调用super父类的方法
   *  isPerformSelector:是否用performSelector方式调用
   */
  var _methodFunc = function(instance, clsName, methodName, args, isSuper, isPerformSelector) {
    var selectorName = methodName
    if (!isPerformSelector) {

//  不是 performSelector方式的方法调用流程
//  处理得到OC中的方法SEL
      methodName = methodName.replace(/__/g, "-")
      selectorName = methodName.replace(/_/g, ":").replace(/-/g, "_")
      var marchArr = selectorName.match(/:/g)
      var numOfArgs = marchArr ? marchArr.length : 0
      if (args.length > numOfArgs) {
        selectorName += ":"
      }
    }

//  获取调用OC方法后的返回值
//  如果是获取一个OC对象,那么ret = {"__obj":OC对象},因为OC把对象返回给js之前会先包装成NSDictionary
    var ret = instance ? _OC_callI(instance, selectorName, args, isSuper):
                         _OC_callC(clsName, selectorName, args)

//  获取OC方法执行完毕的返回值,并转化成JS对象
    return _formatOCToJS(ret)
  }

修复 step 3:global.defineClass

原脚本代码经过正则处理后交由JSContext环境去执行:

[_context evaluateScript:formatedScript withSourceURL:resourceURL];

回过头看main.js的代码(处理后的):

defineClass(`ViewController`,{instaceMethods...},{classMethods...})

参数依次为类名、实例方法列表、类方法列表。阅读global.defineClass源码会发现defineClass首先会分别对两个方法列表调用_formatDefineMethods,该方法参数有三个:方法列表(js对象)、空js对象、真实类名:

  var _formatDefineMethods = function(methods, newMethods, realClsName) {
    for (var methodName in methods) {
      if (!(methods[methodName] instanceof Function)) return;
      (function(){
        var originMethod = methods[methodName]
        newMethods[methodName] = [originMethod.length, function() {
          try {
            var args = _formatOCToJS(Array.prototype.slice.call(arguments))
            var lastSelf = global.self
            global.self = args[0]
            if (global.self) global.self.__realClsName = realClsName
            args.splice(0,1)
            var ret = originMethod.apply(originMethod, args)
            global.self = lastSelf
            return ret
          } catch(e) {
            _OC_catch(e.message, e.stack)
          }
        }]
      })()
    }
  }

该段代码遍历方法列表对象的方法名,向js空对象中添加属性:方法名为键,一个数组为值。数组第一个元素为对应实现函数的参数个数,第二个元素是方法的具体实现。也就是说,_formatDefineMethodsdefineClass传递过来的js对象进行了修改:

{
    methodName:function(args...){...}
}
-->
{
    methodName:[argCount,function(args...){...新实现}]
}

1. 为什么要传递参数个数?
因为runtime修复类的时候无法直接解析js实现函数,也就无法知道参数个数,但方法替换的过程需要生成方法签名,所以只能从js端拿到js函数的参数个数,并传递给OC。

2. 为什么要修改方法实现?

  • 参数转化为js对象。这涉及到对象生命周期的管理,具体查看<实现原理一>4.对象持有/转换。
  • self 处理。使得js修复代码中我们可以像在 OC 中一样使用self,具体查看<实现原理一>6.self 关键字。
  • args.splice(0,1)删除前两个参数:
    OC中进行消息转发,前两个参数是selfselector,实际调用js的具体实现的时候,需要把这两个参数删除。

我们可以使用 safari 对 JSPatch.js 进行调试(JS 断点调试)看看处理之后的newInstMethods

回到defineClass,调用_formatDefineMethods之后,拿着要重写的类名和经过处理的js对象,调用_OC_defineClass,也就是OC端定义的block方法。

修复 step 4:_OC_defineClass

JPEngine中的defineClass对类进行真正的重写操作,将类名、selector、方法实现(IMP)、方法签名等runtime重写方法所需的基本元素提取出来。
源码解读:

/**
 *  定义一个类/覆盖或新增一个方法.
 *
 *  @param classDeclaration 类的声明(需要替换或者新增的类名:继承的父类名 <实现的协议1,实现的协议2>)
 *  @param instanceMethods  {实例方法}
 *  @param classMethods     {类方法}
 *
 *  @return 返回@{@"cls": className, @"superCls": superClassName}
 */
static NSDictionary *defineClass(NSString *classDeclaration, JSValue *instanceMethods, JSValue *classMethods)
{
//    1.使用 NSScanner 分离 classDeclaration.
    NSScanner *scanner = [NSScanner scannerWithString:classDeclaration];

    NSString *className;        //类名
    NSString *superClassName;   //父类名
    NSString *protocolNames;    //实现的协议名
    [scanner scanUpToString:@":" intoString:&className];
    if (!scanner.isAtEnd) {
        scanner.scanLocation = scanner.scanLocation + 1;
        [scanner scanUpToString:@"<" intoString:&superClassName];
        if (!scanner.isAtEnd) {
            scanner.scanLocation = scanner.scanLocation + 1;
            [scanner scanUpToString:@">" intoString:&protocolNames];
        }
    }

    if (!superClassName) superClassName = @"NSObject";
    className = trim(className);
    superClassName = trim(superClassName);

    NSArray *protocols = [protocolNames length] ? [protocolNames componentsSeparatedByString:@","] : nil;

//    2.获取该Class对象.
    Class cls = NSClassFromString(className);
    if (!cls) {
        Class superCls = NSClassFromString(superClassName);
        if (!superCls) {
            _exceptionBlock([NSString stringWithFormat:@"can't find the super class %@", superClassName]);
            return @{@"cls": className};
        }
//        2.1 该Class对象为nil,为JS端添加一个新的类.
        cls = objc_allocateClassPair(superCls, className.UTF8String, 0);
        objc_registerClassPair(cls);
    }

    if (protocols.count > 0) {
        for (NSString* protocolName in protocols) {
            Protocol *protocol = objc_getProtocol([trim(protocolName) cStringUsingEncoding:NSUTF8StringEncoding]);
            class_addProtocol (cls, protocol);
        }
    }

    for (int i = 0; i < 2; i ++) {
        BOOL isInstance = i == 0;
        JSValue *jsMethods = isInstance ? instanceMethods: classMethods;
//        3.若是添加实例方法,直接使用Class对象;
//          若是添加类方法,需要获取元类.
        Class currCls = isInstance ? cls: objc_getMetaClass(className.UTF8String);
//      把js对象转换成OC的字典,从而可以取到方法名、参数个数、具体实现.
        NSDictionary *methodDict = [jsMethods toDictionary];
        for (NSString *jsMethodName in methodDict.allKeys) {
//            遍历字典的key,即方法名,根据方法名取出的值还是JSValue对象,它代表的是数组,第一个值是参数的个数,第二个值是函数的实现.
            JSValue *jsMethodArr = [jsMethods valueForProperty:jsMethodName];
            int numberOfArg = [jsMethodArr[0] toInt32];
            NSString *selectorName = convertJPSelectorString(jsMethodName);

            if ([selectorName componentsSeparatedByString:@":"].count - 1 < numberOfArg) {
                selectorName = [selectorName stringByAppendingString:@":"];
            }
            JSValue *jsMethod = jsMethodArr[1];
            if (class_respondsToSelector(currCls, NSSelectorFromString(selectorName))) {
//                4.如果要替换的类已经定义了该方法,直接对该方法替换和实现消息转发.
                overrideMethod(currCls, selectorName, jsMethod, !isInstance, NULL);
            } else {
                BOOL overrided = NO;
                for (NSString *protocolName in protocols) {
//                    5.1 遍历protocolsNames,依次获取协议对象和协议方法中的type和name
                    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) {
//                    5.2 上述两种情况都不满足.js端请求添加一个新的方法.
                    if (![[jsMethodName substringToIndex:1] isEqualToString:@"_"]) {
//                      方法名的处理:_改为:
                        NSMutableString *typeDescStr = [@"@@:" mutableCopy];
                        for (int i = 0; i < numberOfArg; i ++) {
                            [typeDescStr appendString:@"@"];
                        }
//                        构造一个typeDescription为"@@:\@*"的IMP.将这个IMP添加到类中.
                        overrideMethod(currCls, selectorName, jsMethod, !isInstance, [typeDescStr cStringUsingEncoding:NSUTF8StringEncoding]);
                    }
                }
            }
        }
    }

//    6.为该类添加两个方法,使js脚本拥有设置property的方法.
#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

//    7.返回字典给js脚本
    return @{@"cls": className, @"superCls": superClassName};
}

由源码可见,方法名、实现等处理好之后最终执行overrideMethod方法。

修复 step 5:overrideMethod

overrideMethod是实现“替换”的最后一步。通过调用一系列runtime 方法增加/替换实现的api,使用jsvalue中将要替换的方法实现来替换oc类中的方法实现。
该函数做的事情比较多,一张图概括如下:

4.向class添加名为ORIG+selector,对应原始selector的IMP。
这一步是为了让js通过这个方法调用原来的实现。

5.向class添加名为ORIGforwardInvocation的方法,实现是原始的forwardInvocation的IMP。
这一步是为了保存forwardInvocation的旧有实现,在新的实现中做判断,如果转发的方法是欲改写的,就走新逻辑,反之走原来的流程。
源码解读:

/**
 *  使用jsvalue中将要替换的方法实现来替换oc类中的方法实现
 *
 *  @param cls             被替换的类
 *  @param selectorName    被替换实现的SEL
 *  @param function        在js中定义的将要替换的新的实现
 *  @param isClassMethod   是否类方法(如果是-->寻找MetaClass)
 *  @param typeDescription 被替换的实现方法的编码
 */

static void overrideMethod(Class cls, NSString *selectorName, JSValue *function, BOOL isClassMethod, const char *typeDescription)
{
//    1. 要重写的方法的SEL.
    SEL selector = NSSelectorFromString(selectorName);

//    2. 获取重写方法的具体实现函数的格式编码.
    if (!typeDescription) {
        Method method = class_getInstanceMethod(cls, selector);
        typeDescription = (char *)method_getTypeEncoding(method);
    }

//    3.获取 class 中被重写 SEL 对应的原始IMP.
    IMP originalImp = class_respondsToSelector(cls, selector) ? class_getMethodImplementation(cls, selector) : NULL;

//    4.准备进入消息转发处理的系统函数实现IMP.
    IMP msgForwardIMP = _objc_msgForward;

//    5.针对“非 arm64”架构,消息转发应使用 _objc_msgForward_stret 系统函数.
//      因为_objc_msgForward函数在cpu架构不是 arm64 时,处理返回值是一些特殊 struct 时可能造成 crash.
    #if !defined(__arm64__)
        if (typeDescription[0] == '{') {
            //In some cases that returns struct, we should use the '_stret' API:
            //http://sealiesoftware.com/blog/archive/2008/10/30/objc_explain_objc_msgSend_stret.html
            //NSMethodSignature knows the detail but has no API to return, we can only get the info from debugDescription.
            NSMethodSignature *methodSignature = [NSMethodSignature signatureWithObjCTypes:typeDescription];
            if ([methodSignature.debugDescription rangeOfString:@"is special struct return? YES"].location != NSNotFound) {
                msgForwardIMP = (IMP)_objc_msgForward_stret;
            }
        }
    #endif

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wundeclared-selector"

    if (class_getMethodImplementation(cls, @selector(forwardInvocation:)) != (IMP)JPForwardInvocation) {

//     6.将cls中原来 forwardInvocaiton: 的实现替换成 JPForwardInvocation:函数实现.
//       class_replaceMethod()返回的是替换之前的 IMP.
        IMP originalForwardImp = class_replaceMethod(cls, @selector(forwardInvocation:), (IMP)JPForwardInvocation, "v@:@");
        if (originalForwardImp) {
//     7.为cls添加新的SEL(ORIGforwardInvocation:),指向原始 forwardInvocation: 的实现IMP.
            class_addMethod(cls, @selector(ORIGforwardInvocation:), originalForwardImp, "v@:@");
        }
    }
#pragma clang diagnostic pop
    [cls jp_fixMethodSignature];
    if (class_respondsToSelector(cls, selector)) {
        NSString *originalSelectorName = [NSString stringWithFormat:@"ORIG%@", selectorName];
        SEL originalSelector = NSSelectorFromString(originalSelectorName);
        if(!class_respondsToSelector(cls, originalSelector)) {
//      8.为cls添加新的SEL(ORIG...:)指向被替换方法的原始实现IMP.
            class_addMethod(cls, originalSelector, originalImp, typeDescription);
        }
    }

//      9.构造替换实现后的新SEL:(JP...)
    NSString *JPSelectorName = [NSString stringWithFormat:@"_JP%@", selectorName];

//     10.记录新SEL对应js传过来的待替换目标方法的实现.
    _initJPOverideMethods(cls);
    _JSOverideMethods[cls][JPSelectorName] = function;

//    11.替换原SEL的实现IMP为msgForwardIMP
//       让被替换的方法调用时,直接进入“消息转发”流程(_objc_msgForward 或 _objc_msgForward_stret)
//       这一步放到最后是为了避免在 overrideMethod 过程中调用原sel导致的线程问题.
    class_replaceMethod(cls, selector, msgForwardIMP, typeDescription);
}

至此,selector具体实现 IMP 的替换工作已经完成了。接下来便可以分析一下点击button后的handle事件。

调用 step 1:JPForwardInvocation

经过上一步处理,handle:直接走objc_msgForward进行消息转发环节。当点击button,调用handle:的时候,函数调用的参数会被封装到NSInvocation对象,走到forwardInvocation方法。上一步中forwardInvocation方法的实现替换成了JPForwardInvocation负责拦截系统消息转发函数传入的NSInvocation并从中获取到所有的方法执行参数值,是实现替换和新增方法的核心
源码解读:

/**
 *  替换原有的forwarInvocation:方法
 *
 *  @param assignSlf  self
 *  @param selector   原始SEL
 *  @param invocation 封装了函数调用参数的NSInvocation对象
 */
static void JPForwardInvocation(__unsafe_unretained id assignSlf, SEL selector, NSInvocation *invocation)
{
//    1.表示oc对象是否已经被释放
    BOOL deallocFlag = NO;
    id slf = assignSlf;

//    2.获取invocation中参数的数量
    NSMethodSignature *methodSignature = [invocation methodSignature];
    NSInteger numberOfArguments = [methodSignature numberOfArguments];

//    3.转化调用的SEL为JPSEL(这是JSPatch中缓存JSValue* function的key格式)
    NSString *selectorName = NSStringFromSelector(invocation.selector);
    NSString *JPSelectorName = [NSString stringWithFormat:@"_JP%@", selectorName];

//    4.判断JPSEL是否有对应的js函数的实现,如果没有就走原始方法的消息转发的流程.
    JSValue *jsFunc = getJSFunctionInObjectHierachy(slf, JPSelectorName);
    if (!jsFunc) {
        JPExecuteORIGForwardInvocation(slf, selector, invocation);
        return;
    }

//    5.从NSInvocation中获取调用的参数,把self与相应的参数都转换成js对象并封装到一个集合中
//      js端重写的函数,传递过来是JSValue类型,用callWithArgument:调用js方法,参数也要是js对象.

//    5.1 初始化数组,存储NSInvacation中获取的参数列表,传给对应的js函数
    NSMutableArray *argList = [[NSMutableArray alloc] init];
    if ([slf class] == slf) {
//        5.2 类方法:设置__clsName标识表明这是一个类对象
        [argList addObject:[JSValue valueWithObject:@{@"__clsName": NSStringFromClass([slf class])} inContext:_context]];
    } else if ([selectorName isEqualToString:@"dealloc"]) {
//        5.3 要被释放的对象:使用assign来保存self指针
        [argList addObject:[JPBoxing boxAssignObj:slf]];
        deallocFlag = YES;
    } else {
//        5.4 使用 weak 保存self 指针
        [argList addObject:[JPBoxing boxWeakObj:slf]];
    }

//    5.5 NSInvocation 对象的前两个参数是self和_cmd(http://stackoverflow.com/questions/5788346/calling-a-selector-with-unknown-number-of-arguments-using-reflection-introspec)
//    所以直接从第3个参数开始获取
    for (NSUInteger i = 2; i < numberOfArguments; i++) {
        const char *argumentType = [methodSignature getArgumentTypeAtIndex:i];
//        返回值如果是const,获取encoding来判断类型.
        switch(argumentType[0] == 'r' ? argumentType[1] : argumentType[0]) {

//                从invocation中获取参数,添加到argList中.
            #define JP_FWD_ARG_CASE(_typeChar, _type) \
            case _typeChar: {   \
                _type arg;  \
                [invocation getArgument:&arg atIndex:i];    \
                [argList addObject:@(arg)]; \
                break;  \
            }
            JP_FWD_ARG_CASE('c', char)
            JP_FWD_ARG_CASE('C', unsigned char)
            JP_FWD_ARG_CASE('s', short)
            JP_FWD_ARG_CASE('S', unsigned short)
            JP_FWD_ARG_CASE('i', int)
            JP_FWD_ARG_CASE('I', unsigned int)
            JP_FWD_ARG_CASE('l', long)
            JP_FWD_ARG_CASE('L', unsigned long)
            JP_FWD_ARG_CASE('q', long long)
            JP_FWD_ARG_CASE('Q', unsigned long long)
            JP_FWD_ARG_CASE('f', float)
            JP_FWD_ARG_CASE('d', double)
            JP_FWD_ARG_CASE('B', BOOL)
            case '@': {
//                id类型参数使用__unsafe__unretained
                __unsafe_unretained id arg;
                [invocation getArgument:&arg atIndex:i];
//                block参数使用copy,_nilObj表示nil
                if ([arg isKindOfClass:NSClassFromString(@"NSBlock")]) {
                    [argList addObject:(arg ? [arg copy]: _nilObj)];
                } else {
                    [argList addObject:(arg ? arg: _nilObj)];
                }
                break;
            }
            case '{': {
//                处理结构体类型参数
//                获取结构体类型名称,把参数包装成JSValue类型
                NSString *typeString = extractStructName([NSString stringWithUTF8String:argumentType]);
                #define JP_FWD_ARG_STRUCT(_type, _transFunc) \
                if ([typeString rangeOfString:@#_type].location != NSNotFound) {    \
                    _type arg; \
                    [invocation getArgument:&arg atIndex:i];    \
                    [argList addObject:[JSValue _transFunc:arg inContext:_context]];  \
                    break; \
                }
                JP_FWD_ARG_STRUCT(CGRect, valueWithRect)
                JP_FWD_ARG_STRUCT(CGPoint, valueWithPoint)
                JP_FWD_ARG_STRUCT(CGSize, valueWithSize)
                JP_FWD_ARG_STRUCT(NSRange, valueWithRange)

//                自定义类型的结构体处理
                @synchronized (_context) {
                    NSDictionary *structDefine = _registeredStruct[typeString];
                    if (structDefine) {
                        size_t size = sizeOfStructTypes(structDefine[@"types"]);
                        if (size) {
                            void *ret = malloc(size);
                            [invocation getArgument:ret atIndex:i];
                            NSDictionary *dict = getDictOfStruct(ret, structDefine);
                            [argList addObject:[JSValue valueWithObject:dict inContext:_context]];
                            free(ret);
                            break;
                        }
                    }
                }

                break;
            }
            case ':': {
//                selector类型处理
                SEL selector;
                [invocation getArgument:&selector atIndex:i];
                NSString *selectorName = NSStringFromSelector(selector);
                [argList addObject:(selectorName ? selectorName: _nilObj)];
                break;
            }
            case '^':
            case '*': {
//                指针类型处理
                void *arg;
                [invocation getArgument:&arg atIndex:i];
                [argList addObject:[JPBoxing boxPointer:arg]];
                break;
            }
            case '#': {
//                Class类型
                Class arg;
                [invocation getArgument:&arg atIndex:i];
                [argList addObject:[JPBoxing boxClass:arg]];
                break;
            }
            default: {
                NSLog(@"error type %s", argumentType);
                break;
            }
        }
    }

    if (_currInvokeSuperClsName) {
        Class cls = NSClassFromString(_currInvokeSuperClsName);
        NSString *tmpSelectorName = [[selectorName stringByReplacingOccurrencesOfString:@"_JPSUPER_" withString:@"_JP"] stringByReplacingOccurrencesOfString:@"SUPER_" withString:@"_JP"];
        if (!_JSOverideMethods[cls][tmpSelectorName]) {
            NSString *ORIGSelectorName = [selectorName stringByReplacingOccurrencesOfString:@"SUPER_" withString:@"ORIG"];
            [argList removeObjectAtIndex:0];
            id retObj = callSelector(_currInvokeSuperClsName, ORIGSelectorName, [JSValue valueWithObject:argList inContext:_context], [JSValue valueWithObject:@{@"__obj": slf, @"__realClsName": @""} inContext:_context], NO);
            id __autoreleasing ret = formatJSToOC([JSValue valueWithObject:retObj inContext:_context]);
            [invocation setReturnValue:&ret];
            return;
        }
    }

//    6.将上面获得的参数列表数组转化为对应的js对象数组
    NSArray *params = _formatOCToJSList(argList);
    char returnType[255];
//    7.获取返回值类型
    strcpy(returnType, [methodSignature methodReturnType]);

    // Restore the return type
    if (strcmp(returnType, @encode(JPDouble)) == 0) {
        strcpy(returnType, @encode(double));
    }
    if (strcmp(returnType, @encode(JPFloat)) == 0) {
        strcpy(returnType, @encode(float));
    }

//    7.1 返回值是否为const,如果是,获取后面的encoding来判断类型
    switch (returnType[0] == 'r' ? returnType[1] : returnType[0]) {
    ...(返回值的其它封装规则,具体看源码)
    }
    if (_pointersToRelease) {
        for (NSValue *val in _pointersToRelease) {
            void *pointer = NULL;
            [val getValue:&pointer];
            CFRelease(pointer);
        }
        _pointersToRelease = nil;
    }

//    8.待替换的方法是 delloc 需要特殊处理:
    if (deallocFlag) {
        slf = nil;
        Class instClass = object_getClass(assignSlf);
        Method deallocMethod = class_getInstanceMethod(instClass, NSSelectorFromString(@"ORIGdealloc"));
        //获取原delloc imp 指针,调用delloc,防止内存泄漏.
        void (*originalDealloc)(__unsafe_unretained id, SEL) = (__typeof__(originalDealloc))method_getImplementation(deallocMethod);
        originalDealloc(assignSlf, NSSelectorFromString(@"dealloc"));
    }
}

接下来执行JS中定义的方法实现。“修复 step 2”中已经讨论过,现在main.js中所有的函数都被替换成名为__c('methodName')的函数调用,__c调用了_methodFunc函数,_methodFunc会根据方法类型调用_OC_call

var ret = instance ? _OC_callI(instance, selectorName, args, isSuper):
                         _OC_callC(clsName, selectorName, args)

_OC_callI_OC_callC最终都会调用一个static函数callSelector

调用 step 2:callSelector

main.js中类似UIAlertView.alloc().init()实际是通过callSelector调用 OC 的方法。

  • 将 js 对象和参数转化为 OC 对象;
  • 判断是否调用的是父类的方法,如果是,就走父类的方法实现;
  • 把参数等信息封装成NSInvocation对象,并执行,然后返回结果。

源码解读:

/**
 *  完成oc中的方法调用
 *
 *  @param className    类名(nil --> 表示实例方法)
 *  @param selectorName 方法SEL值
 *  @param arguments    方法执行参数
 *  @param instance     对象(js对象中的变量,如: var UIAlertView = { __clsName : 'UIAlertView'})
 *  @param isSuper      是否调用的是父类方法
 *
 *  @return 方法执行后的结果值,返回给js代码中.
 */
static id callSelector(NSString *className, NSString *selectorName, JSValue *arguments, JSValue *instance, BOOL isSuper)
{
    NSString *realClsName = [[instance valueForProperty:@"__realClsName"] toString];

    if (instance) {
//    1.将js封装的instance对象进行拆装,得到oc对象.
        instance = formatJSToOC(instance);
        if (!instance || instance == _nilObj || [instance isKindOfClass:[JPBoxing class]]) return @{@"__isNil": @(YES)};
    }
//    2.将js封装的参数列表转为oc类型.
    id argumentsObj = formatJSToOC(arguments);

    if (instance && [selectorName isEqualToString:@"toJS"]) {
//      3.如果要执行的方法是"toJS",即转化为js类型,对于NSString/NSDictory/NSArray/NSData需进行特殊处理
//        因为JSPatch中需使用JPBoxing包装OC中的上述对象,防止JavaScriptCore.framework转换类型.
        if ([instance isKindOfClass:[NSString class]] || [instance isKindOfClass:[NSDictionary class]] || [instance isKindOfClass:[NSArray class]] || [instance isKindOfClass:[NSDate class]]) {
            return _unboxOCObjectToJS(instance);
        }
    }

//    4.根据类名与selectorName获得对应的类对象与selector
    Class cls = instance ? [instance class] : NSClassFromString(className);
    SEL selector = NSSelectorFromString(selectorName);

    NSString *superClassName = nil;
//    5.判断是否调用的是父类的方法,如果是,走父类的方法实现
    if (isSuper) {
//        5.1 定义新的SEL:SUPERSEL
        NSString *superSelectorName = [NSString stringWithFormat:@"SUPER_%@", selectorName];
        SEL superSelector = NSSelectorFromString(superSelectorName);

        Class superCls;
        if (realClsName.length) {
            Class defineClass = NSClassFromString(realClsName);
            superCls = defineClass ? [defineClass superclass] : [cls superclass];
        } else {
            superCls = [cls superclass];
        }

        Method superMethod = class_getInstanceMethod(superCls, selector);
        IMP superIMP = method_getImplementation(superMethod);

//        5.2  将SUPERSEL指向superIMP的实现
        class_addMethod(cls, superSelector, superIMP, method_getTypeEncoding(superMethod));

//        5.3  查找父类中是否有添加JPSEL的实现
        NSString *JPSelectorName = [NSString stringWithFormat:@"_JP%@", selectorName];
        JSValue *overideFunction = _JSOverideMethods[superCls][JPSelectorName];
        if (overideFunction) {
//            如果有,进行imp替换
            overrideMethod(cls, superSelectorName, overideFunction, NO, NULL);
        }

        selector = superSelector;
        superClassName = NSStringFromClass(superCls);
    }


    NSMutableArray *_markArray;

//    6.通过类对象与selector构造对应的NSMethodSignature签名
    NSInvocation *invocation;
    NSMethodSignature *methodSignature;
    if (!_JSMethodSignatureCache) {
        _JSMethodSignatureCache = [[NSMutableDictionary alloc]init];
    }
    if (instance) {
        [_JSMethodSignatureLock lock];
        if (!_JSMethodSignatureCache[cls]) {
            _JSMethodSignatureCache[(id<NSCopying>)cls] = [[NSMutableDictionary alloc]init];
        }
        methodSignature = _JSMethodSignatureCache[cls][selectorName];
        if (!methodSignature) {
            methodSignature = [cls instanceMethodSignatureForSelector:selector];
            methodSignature = fixSignature(methodSignature);
            _JSMethodSignatureCache[cls][selectorName] = methodSignature;
        }
        [_JSMethodSignatureLock unlock];
        if (!methodSignature) {
            _exceptionBlock([NSString stringWithFormat:@"unrecognized selector %@ for instance %@", selectorName, instance]);
            return nil;
        }
//        7.根据签名构造NSInvocation对象
        invocation = [NSInvocation invocationWithMethodSignature:methodSignature];
//        8.为invocation对象设置target
        [invocation setTarget:instance];
    } else {
        methodSignature = [cls methodSignatureForSelector:selector];
        methodSignature = fixSignature(methodSignature);
        if (!methodSignature) {
            _exceptionBlock([NSString stringWithFormat:@"unrecognized selector %@ for class %@", selectorName, className]);
            return nil;
        }
        invocation= [NSInvocation invocationWithMethodSignature:methodSignature];
        [invocation setTarget:cls];
    }
//        9.为invocation对象设置selector
    [invocation setSelector:selector];

//    10.根据签名得知每个参数的实际类型
    NSUInteger numberOfArguments = methodSignature.numberOfArguments;
    NSInteger inputArguments = [(NSArray *)argumentsObj count];
    if (inputArguments > numberOfArguments - 2) {
//        10.1 多参数方法仅支持 id 类型参数和 id 类型返回,直接revoke并返回.
        id sender = instance != nil ? instance : cls;
        id result = invokeVariableParameterMethod(argumentsObj, methodSignature, sender, selector);
        return formatOCToJS(result);
    }

//    10.2 将JS传递过来的参数进行对应的转换(如 NSNumber -> int),转换后为 NSInvocation 对象设置参数.
    for (NSUInteger i = 2; i < numberOfArguments; i++) {
        const char *argumentType = [methodSignature getArgumentTypeAtIndex:i];
        id valObj = argumentsObj[i-2];
        switch (argumentType[0] == 'r' ? argumentType[1] : argumentType[0]) {
            //...(具体转换规则看源代码)
        }
    }

    if (superClassName) _currInvokeSuperClsName = superClassName;

//    11.执行 invoke 方法,并且传递指定的参数
    [invocation invoke];

    if (superClassName) _currInvokeSuperClsName = nil;
    if ([_markArray count] > 0) {
        for (JPBoxing *box in _markArray) {
            void *pointer = [box unboxPointer];
            id obj = *((__unsafe_unretained id *)pointer);
            if (obj) {
                @synchronized(_TMPMemoryPool) {
                    [_TMPMemoryPool setObject:obj forKey:[NSNumber numberWithInteger:[(NSObject*)obj hash]]];
                }
            }
        }
    }

    char returnType[255];
    strcpy(returnType, [methodSignature methodReturnType]);

    // Restore the return type
    if (strcmp(returnType, @encode(JPDouble)) == 0) {
        strcpy(returnType, @encode(double));
    }
    if (strcmp(returnType, @encode(JPFloat)) == 0) {
        strcpy(returnType, @encode(float));
    }

    id returnValue;
    if (strncmp(returnType, "v", 1) != 0) {
        if (strncmp(returnType, "@", 1) == 0) {
            void *result;
//            12. 获取 invocation 运行返回值.
            [invocation getReturnValue:&result];

//            13. 将返回值封装成JS对应的对象并返回.
            //For performance, ignore the other methods prefix with alloc/new/copy/mutableCopy
            if ([selectorName isEqualToString:@"alloc"] || [selectorName isEqualToString:@"new"] ||
                [selectorName isEqualToString:@"copy"] || [selectorName isEqualToString:@"mutableCopy"]) {
                returnValue = (__bridge_transfer id)result;
            } else {
                returnValue = (__bridge id)result;
            }
            return formatOCToJS(returnValue);

        } else {
            switch (returnType[0] == 'r' ? returnType[1] : returnType[0]) {
            //...(具体转换规则见源代码)
            return returnValue;
        }
    }
    return nil;
}

至此,JSPatch 热修复核心步骤方法替换方法调用就结束了。

总结

JSPatch 基于JavaScriptCore.framework和Objective-C中的runtime技术。

  • 采用 iOS7 后引入的 JavaScriptCore.framework作为 JavaScript 引擎解析js脚本,执行js代码并与OC端代码进行桥接。
  • 使用Objective-C runtime中的method swizzling方式达到使用js脚本动态替换原有OC方法的目的,并利用forwardInvocation消息转发机制使得在js脚本中调用OC的方法成为可能。

JSPatch 实现过程中还有许多细节问题诸如 Special Struct、内存管理、JPBoxingnil处理等,更多详细内容可以阅读作者的原理详解系列文章以及GitHub wiki。学习 JSPatch 不仅可以弄清 iOS 热修复机制,也可以体会到如何利用 runtime 这一 OC 最重要的特性来实现一些强大的功能。这里不得不佩服作者深厚的编程功底和各种精彩的奇思妙想。