本文作为 JSPatch 源码阅读笔记,记录了 v0.0.1 到 v1.2.2 版本之间的主要 commit 变化,尝试从源码反向理解 JSPatch 实现原理和细节。

未完待续。

v0.0.1

JSPatch 文件结构简单得让人不可思议,只有 JPEngine{.h,.m} 和 JSPatch.js 三个文件。

JPEngine.h

JSPatch 依赖 <JavaScriptCore/JavaScriptCore.h><objc/runtime.h>

@interface JPEngine : NSObject
+ (void)startEngine;
+ (JSValue *)evaluateScript:(NSString *)script;
+ (JSContext *)context;
@end

JPEngine.m

startEngine 方法里新建并维持一个 JSContext 静态实例,并注入一些 JavaScript 方法和 exceptionHandler,最后在创建的 context 里加载 patch 文件。

注入方法包括:

  • _OC_defineClass
  • _OC_callI
  • _OC_callC
  • _OC_getBlockArguments
  • _OC_dispatch_after
  • _OC_dispatch_async_main
  • _OC_dispatch_sync_main
  • _OC_dispatch_async_global_queue
  • _OC_log
  • _OC_catch

evaluateScript 方法在 JSContext 类静态实例环境下,执行 patch 文件。其中,多了一步 try/catch 替换工作,把这个 patch 执行体都放在一个 try/catch 方法里,同时,替换 patch 文件里写的 .().__c()

有两个属性 cacheArgumentscacheArgumentsIdx,作用是 ???

另外,有 5 个全局静态变量 cacheArgumentscacheArgumentsIdx_propKeys

这个 JPEngine.m 文件里剩下的基本都是 C 函数。一个个解析如下:

static const void *propKey(NSString *propName)

Q: 为什么不直接保存 propName,而要转换到 propKey 呢?

读取保存在 _propKeys 字典里 propName 对应的 propKey。在 getPropIMPsetPropIMP 方法里被调用,用途是通过 associatedObject 方式给类添加属性。

static NSDictionary *defineClass(NSString *classDeclaration, JSValue *instanceMethods, JSValue *classMethods)
  1. 获取定义的类和父类名称,如果类不存在,通过注册 ClassPair 方法注册新类,方法如下:
Class superCls = NSClassFromString(superClassName);
cls = objc_allocateClassPair(superCls, className.UTF8String, 0);
objc_registerClassPair(cls);
  1. 通过两次循环,遍历实例方法和类方法,通过 class_copyMethodList 函数,遍历类层次结构里已定义的方法,如果存在 jsMethods 里定义的待替换的方法,则调用 overrideMethod 函数完成替换。 通过字典保存已替换的方法名,不重复替换。 不过,代码中存在循环变量重复定义,没有调用 free 函数等问题。

  2. 增加 getProp:setProp:forKey: 方法

class_addMethod(cls, @selector(getProp:), (IMP)getPropIMP, "@24@0:8@16");
class_addMethod(cls, @selector(setProp:forKey:), (IMP)setPropIMP, "v32@0:8@16@24");

其中,type encoding 很有意思,待分析?(PS,后续作者去掉了 type 长度限制)

  1. 返回字典类型结果,包含类,已替换的实例方法,已替换的类方法

通过一堆宏定义方法返回类型,例如

JPMETHOD_IMPLEMENTATION_RET(id, id, return [ret toObject])

经过宏展开得到

static id JPMethodImplement_id(id slf, SEL selector) {
  JSValue *fun = getJSFunctionInObjectHierachy(slf, selector);
  JSValue *ret = [fun callWithArguments:_TMPInvocationArguments];
  id obj = formatJSToOC(ret);
  if (obj == _nilObj || ([obj isKindOfClass:[NSNumber class]] && strcmp([obj objCType], "c") == 0 && ![obj boolValue]))
    return ((void *)0);
  return obj;
  ;
}

JPImplementation 作为 IMP 类型,替换需要 override 的方法。

overrideMethod

static void overrideMethod(Class cls, NSString *selectorName, JSValue *function, BOOL isClassMethod)

__JPNONImplementSelector 不存在的 IMP 实现,用于将待替换的方法走动态消息转发,最终走到 JPForwardInvocation 方法。

IMP originalImp = class_respondsToSelector(cls, selector) ? class_getMethodImplementation(cls, selector) : NULL;
class_replaceMethod(cls, selector, class_getMethodImplementation(cls, @selector(__JPNONImplementSelector)), typeDescription);
SEL newForwardSelector = @selector(ORIGforwardInvocation:);
if (!class_respondsToSelector(cls, newForwardSelector)) {
    IMP originalForwardImp = class_replaceMethod(cls, @selector(forwardInvocation:), (IMP)JPForwardInvocation, "v@:@");
    class_addMethod(cls, newForwardSelector, originalForwardImp, "v@:@");
}

这里的 forwardInvocation: 就是动态消息转发的原方法,也被替换为 JPForwardInvocation

// 将 selector 原实现添加到 ORIGselectorName 新方法
NSString *originalSelectorName = [NSString stringWithFormat:@"ORIG%@", selectorName];
SEL originalSelector = NSSelectorFromString(originalSelectorName);
if(!class_respondsToSelector(cls, originalSelector)) {
    class_addMethod(cls, originalSelector, originalImp, typeDescription);
}
_methodFunc

把相关信息传给 Objective-C,Objective-C 用 Runtime 接口调用相应方法,返回结果值。

JSClass 对象包含以下属性:

  __obj // OC 实例指针
  __isSuper // 是否是 OC 实例的父类
  __clsName // OC 实例对应的类名
  __super // OC 实例的父类的对应的 JSClass 对象

阅读 JSPatch 的 JavaScript 代码应从 patch 示例代码入手,直接从 JSPatch.js 入手的话,很多代码看起来不知为何缘由,例如:

require('UIView')

首先,require 方法是定义在 global 全局对象上的自定义方法,调用了 _require 内部方法,在 global 全局对象上定义了 UIView 这个属性,保存了对应的一个对象 {__isCls: 1, __clsName: 'UIView'}。这样,至此就能调用 UIView 这个全局属性了。

  global.defineClass = function(declaration, instMethods, clsMethods) {
    // 包装传入的函数,参数类型转换
    var newInstMethods = {}, newClsMethods = {}
    _formatDefineMethod(instMethods, newInstMethods, 1)
    _formatDefineMethod(clsMethods, newClsMethods, 0)

    // 调用 JSContext 定义的 _OC_defineClass 方法,执行方法替换
    // 返回的 JS 对象包含类名 cls,实例方法名数组 instMethods,类方法名数组 clsMethods
    var ret = _OC_defineClass(declaration, newInstMethods, newClsMethods)

    // 剔除 OC 已替换的方法,剩下的是 JS 本地方法,重置这些方法执行时上下文环境和参数类型
    _formatLocalMethods(instMethods, ret["instMethods"], 1)
    _formatLocalMethods(clsMethods, ret["clsMethods"], 1)

    // 按类保存定义的实例方法和类方法
    localMethods[ret["cls"]] = {
      instMethods: instMethods,
      clsMethods: clsMethods
    }

    return require(ret["cls"])
  }

defineClass 方法用于在 JS 里定义 OC 类,类方法和实例方法。

  var _formatDefineMethod = function(methods, newMethods, isInst) {
    for (var methodName in methods) {
      (function(){
        var originMethod = methods[methodName]
        newMethods[methodName] = function() {
          var args = _formatOCToJS(Array.prototype.slice.call(arguments))
          if (isInst) {
            global.self = args[0]
            args.splice(0,1)
          }
          var ret = _formatJSToOC(originMethod.apply(originMethod, args))
          if (isInst) {
            global.self = null
          }
          return ret
        }
      })()
    }
  }

主要做两件事,一是将传入的 OC 参数转换成 JSClass 对象,调用定义的 JS 方法,再将 JS 返回结果转换成 OC 对象。二是如果实例方法调用,在调用前后设置 global.self 属性,让 JS 写的代码也能像 OC 一样有 self 属性,而 self 属性的值就是执行方法实例。

  var _formatOCToJS = function(obj) {
     if (obj === undefined || obj === null) return null

     // 如果是 OC 返回的对象,则包含 __isObj 属性,重新封装成 JSClass 对象
     if (typeof obj == "object" && obj.__isObj) {
       return _toJSObj(obj)
     }

     // 如果是数组,递归处理
     if (obj instanceof Array) {
        var ret = []
        obj.forEach(function(o){
          ret.push(_formatOCToJS(o))
        })
        return ret
     }

     // 如果是方法,则包装一个新方法,将其传入参数转换类型
     if (obj instanceof Function) {
        return function() {
          var args = Array.prototype.slice.call(arguments)
          obj.apply(obj, _formatJSToOC(args))
        }
     }

     // 如果是 JS 对象,则遍历全部属性值,进行类型转换
     if (obj instanceof Object) {
        var ret = {}
        for (var key in obj) {
          ret[key] = _formatOCToJS(obj[key])
        }
        return ret
     }

     // 其他基本数据类型,则不处理
     return obj
  }
  var _formatJSToOC = function(obj) {
     // 如果是 JSClass 对象,则返回 OC 对象指针
     if (obj instanceof Object && obj.__obj) {
       return obj.__obj
     }

     // 如果是数组,递归处理
     if (obj instanceof Array) {
        var ret = []
        obj.forEach(function(o){
          ret.push(_formatJSToOC(o))
        })
        return ret
     }

     // 如果是 JS 对象,则遍历全部属性值,进行类型转换
     if (obj instanceof Object) {
        var ret = {}
        for (var key in obj) {
          if (obj.hasOwnProperty(key)) {
            ret[key] = _formatJSToOC(obj[key])
          }
        }
        return ret
     }

     // 其他类型,则不处理
     return obj
  }
  var _formatLocalMethods = function(methods, ocMethods, isInst) {
    ocMethods.forEach(function(methodName){
      delete methods[methodName]
    })
    for (var methodName in methods) {
      (function(){
        var originMethod = methods[methodName]
        methods[methodName] = function() {
          var args = Array.prototype.slice.call(arguments)
          if (isInst) global.self = this
          var ret = originMethod.apply(this, args)
          if (isInst) global.self = null
          return ret
        }
      })()
    }
  }
  // 给全部 JS 对象加上 __c 方法,调用 __c 方法之后返回的还是方法
  Object.prototype.__c = function(methodName) {
    // 如果不是 JSClass 对象,则直接调用方法,表示调用的是 JS 原生方法,绑定作用域
    if (!this.__obj && !this.__clsName) return this[methodName].bind(this);

    // 从定义的类的 JS 方法里查找,如果存在则是 JS 定义方法,绑定作用域
    var customMethod = _getCustomMethod(this.__clsName, methodName, !!this.__obj)
    if (customMethod) {
      return customMethod.bind(this)
    }

    var self = this
    // 返回包装方法,方法里代理到对应的 OC 类的实例方法或类方法
    return function() {
      var args = Array.prototype.slice.call(arguments)
      return _methodFunc(self.__obj, self.__clsName, methodName, args, self.__isSuper)
    }
  }
  // 命名不是很直观,看了多遍之后才反应过来,method 对应的 OC 方法,func 对应的 JS 方法,只能说作者很用心,我领悟能力有限
  var _methodFunc = function(instance, clsName, methodName, args, isSuper) {
    var args = _formatJSToOC(args)

    // 方法名替换, indexPathForRow_inSection 到 indexPathForRow:inSection: 格式
    var selectorName = methodName.replace(/_/g, ":")
    var marchArr = selectorName.match(/:/g)
    var numOfArgs = marchArr ? marchArr.length : 0
    // 判断是否是无参方法,如果不是补全冒号
    if (args.length > numOfArgs) {
      selectorName += ":"
    }
    // 代理调用 OC 方法
    var ret = instance ? _OC_callI(instance, selectorName, args, isSuper):
                         _OC_callC(clsName, selectorName, args)

    return _formatOCToJS(ret)
  }

阅读感受:

我是先看的源码,再仔细看的 bang 的文章,文章写的很通俗易懂。在没有看详解文章之前,每个方法基本都能看懂,但是方法之间的关联和部分方法为什么要这么处理还不能很好的反向理解。另外,看的过程中觉得能写出这么多闭包和作用域绑定的 JavaScript 代码看起来很牛逼,普通的 iOS 程序员肯定是达不到这个水平的,看了博客之后才发现 bang 原来做过前端,好吧。。。跨界才是 iOS 的未来。

v0.0.1 的单元测试还仅仅覆盖通过 JS 创建和传递不同类型的对象,通过 JS 调用 OC 方法等。测试用例的写法比较独特,都是通过对象 BOOL 属性标识来判断单个测试是否正确执行,需要按顺序来阅读测试用例,测试代码和断言代码分布在不同文件,需要跳跃着来阅读。

另外,觉得 v0.0.1 代码整体缺乏优雅的结构,看起来很累,代码本身也没有太多注释,代码中还遗留了不少错误。不过更新很快,而且越来越多的代码贡献都来自社区。总而言之,JSPatch 的出现给社区提供了一个很不错的平台基础,给沉寂已久的 iOS 社区注入了一股清流。

v0.0.2

版本动态

  • 支持添加新的 OC 方法,https://github.com/bang590/JSPatch/commit/653074ff50639c57703265c4824b604c7bc80127
  • 方法名包含下划线 ‘_’,JS 用 ‘__’ 表示
  • https://github.com/bang590/JSPatch/pull/17
  • _objc_msgForward,https://github.com/bang590/JSPatch/commit/e67740fc386dd205d254da141d4480c4396b0c7d,https://github.com/bang590/JSPatch/commit/770d4efaf7d4ab1194d756dff65b0bfc79908c7c
  • 支持 NSNull 和 nil 参数,https://github.com/bang590/JSPatch/commit/5f2f081d9b75a3719e53d9c40eae48e2252a0934

关于 _objc_msgForward 用途,进一步阅读 http://www.jianshu.com/p/3c8ce231576c

支持添加新的 OC 方法,addNewMethod

static void addNewMethod(Class cls, NSString *selectorName, JSValue *function, int argCount, BOOL isClassMethod) {
    NSString *clsName = NSStringFromClass(cls);
    // 重构 methods 存储方式,不再区分类方法和实例方法,按类名和方法名存储为二维数组
    _initJPOverideMethods(clsName);
    _JSOverideMethods[clsName][selectorName] = function;

    SEL selector = NSSelectorFromString(selectorName);

    // 跟进参数格式指定不同的 IMP,IMP 的第一个参数是 id self, 第二个参数是  SEL selector,后续才是调用参数
    IMP JPImplementation = (IMP)JPMETHOD_NEW_IMPLEMENTATION_NAME(0);
    switch (argCount) {
    #define JPMETHOD_NEW_CASE(_argCount) \
        case _argCount: \
            JPImplementation = (IMP)JPMETHOD_NEW_IMPLEMENTATION_NAME(_argCount);    \
            break;

        JPMETHOD_NEW_CASE(0)
        JPMETHOD_NEW_CASE(1)
        JPMETHOD_NEW_CASE(2)
        JPMETHOD_NEW_CASE(3)
        JPMETHOD_NEW_CASE(4)
        JPMETHOD_NEW_CASE(5)
    }

    // 根据参数个数设置 type encoding
    NSMutableString *typeDescStr = [@"@@:" mutableCopy];
    for (int i = 0; i < argCount; i ++) {
        [typeDescStr appendString:@"@"];
    }

    // 调用 class_addMethod 方法添加新的方法,class_addMethod 会重写父类同名实现,但是不会替换类已有实现
    class_addMethod(cls, selector, JPImplementation, [typeDescStr cStringUsingEncoding:NSUTF8StringEncoding]);
}

由于统一了对象和方法的存储方式,在对象层次结构上查找定义的 JS 方法变得更简单。

static JSValue* getJSFunctionInObjectHierarchy(id slf, SEL selector) {
    NSString *selectorName = NSStringFromSelector(selector);
    Class cls = [slf class];
    NSString *clsName = NSStringFromClass(cls);
    JSValue *func = _JSOverideMethods[clsName][selectorName];
    while (!func) {
        cls = class_getSuperclass(cls);
        if (!cls) {
            NSCAssert(NO, @"warning can not find selector %@", selectorName);
            return nil;
        }
        clsName = NSStringFromClass(cls);
        func = _JSOverideMethods[clsName][selectorName];
    }
    return func;
}

- _objc_msgForward 替换 __JPNONImplementSelector

在 forwardInvocation 逻辑里,把它指向了一个不存在的 IMP,之前的实现是指定一个不存在的 selector,而 class_getMethodImplementation 函数在找不到 selector 对应的 IMP 时,会返回 _objc_msgForward 这个 IMP,所以优化成直接指向这个 IMP。

另外,一个 bugfix 还不太看得懂代码,就是处理非 64 位处理器 IMP 返回一个 struct 时的崩溃,具体原理解释参看 http://blog.cnbang.net/tech/2855/。另外,在 Aspects 这个项目里也找到了同样的处理,不过代码更加清晰。https://github.com/steipete/Aspects/blob/master/Aspects.m#L243

支持 NSNull 和 nil 参数

把 nil 参数在内部处理成 NSNull,而 NSNull 在转成 JS 对象是增加 __isNull 属性。

v0.0.3

版本动态

  • 修复多线程调用的问题,通过同步锁解决,https://github.com/bang590/JSPatch/issues/21
  • 进一步支持参数传入 null, undefined, nsnull, https://github.com/bang590/JSPatch/issues/31
  • 支持调用 NSArray / NSDictionary / NSString 的方法,https://github.com/bang590/JSPatch/commit/e6b121db3feacb22e7bbf81d2be67811e4558fd3,https://github.com/bang590/JSPatch/commit/73569a1041242bb1d88b7b0ee1c69d69548ad98c
  • 支持 Protocol
  • 这个版本开始单元测试逐步覆盖比较完整了

支持调用 NSArray / NSDictionary / NSString 的方法

API changes: 1.now the NSArray/NSDictionary/NSString will use as NSObject in JS instead of JS type. 2.use .toJS() to transfer NSArray/NSDictionary/NSString to JS array/object/string. 3.change API .super to .super()

Code changes: 1.box/unbox for NSArray/NSDictionary/NSString 2.remove JSClass 3.move formatJSToOC() to Objective-C

目的是为了在写 Patch 的时候一直保留 OC API,将方法调用的返回值都返回包装类型,避免 JS 和 OC 的 API 混用。另一方面,优化了 JS 调用 OC 方法时参数装箱,调用返回时返回值拆箱的处理,提升了性能。 装箱拆箱的设计很多语言里都有,这里的设计是返回 JSBoxing,移除了原有的 JSClass 设计,在 JS 层面就变得更轻巧,而且将原本在 JS 处理的 _formatJSToOC 方法实现移至了 OC 实现,而保留在 JS 的 _formatOCToJS 方法,仅仅是递归拆包处理。另外,JS 里也移除了 localMethods 的解析和保存。 装箱支持 id,void *,Class,SEL 类型,void * 指针类型支持 _OC_free / free 方法释放。

支持 Protocol

这样做的作用是,当添加 Protocol 里定义的方法,而类里没有实现的方法时,参数和返回值类型不再全是 id,而是自动转为 Protocol 里定义的类型。 具体实现上,差别就在于 overrideMethod 方法里传入匹配的 typeDescription。原本 typeDescription 获取是通过 selectorName -> SEL -> Method -> method type encoding 方式,methodSignature 获取是通过 instanceMethodSignatureForSelector 方法。现在,有了 typeDescription 之后,直接通过 signatureWithObjCTypes 方法就能获取 methodSignature。 我的理解是,代码里混用了 typeDescption 和 methodSignature,比较乱。

static char *methodTypesInProtocol(NSString *protocolName, NSString *selectorName, BOOL isInstanceMethod, BOOL isRequired) {
    Protocol *protocol = objc_getProtocol([trim(protocolName) cStringUsingEncoding:NSUTF8StringEncoding]);
    unsigned int selCount = 0;
    // 获取到接口里定义的方法描述
    struct objc_method_description *methods = protocol_copyMethodDescriptionList(protocol, isRequired, isInstanceMethod, &selCount);
    for (int i = 0; i < selCount; i ++) {
        if ([selectorName isEqualToString:NSStringFromSelector(methods[i].name)]) {
            return methods[i].types;
        }
    }
    // FIXME: 缺少 free(methods) 调用
    return NULL;
}

v0.1

版本动态

  • 取消了 addNewMethod 方法,合并到 overrideMethod 方法
  • 支持链式调用,通过返回 false 实现
  • 使用 JSValue 原生方法转换 CGRect/CGSize/CGPoint/NSRange struct
  • 规范定义了 Extension 机制
  • 增加了 C 接口扩展,这一个版本 albert438 贡献不少代码,他的博客里也有不少介绍 JSPatch 原理的文章
  • 增加了 JPMemory 扩展,用于 JS 里直接操作内存和结构体分配

判断 selector 是否被替换的方法

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

取消了 addNewMethod 方法,合并到 overrideMethod 方法

因为 class_replaceMethod 有两个表现,如 API 文档描述:

This function behaves in two different ways:

If the method identified by name does not yet exist, it is added as if class_addMethod were called. The type encoding specified by types is used as given.

If the method identified by name does exist, its IMP is replaced as if method_setImplementation were called. The type encoding specified by types is ignored.

只要传入合适的 types,即可作为 class_addMethod 使用。

支持链式调用,通过返回 false 实现

https://github.com/bang590/JSPatch/commit/cb7688ee74c3bbc88f0a80c72b1864a93e918f66

这个 bang 的文章里有详细解释,实现上来讲也很巧妙。

同时,支持了 nil 类型,实现方式上同 NULL 类型。

使用 JSValue 原生方法转换 CGRect/CGSize/CGPoint/NSRange struct

https://github.com/bang590/JSPatch/commit/3228c85b68ec675b65ace95119d6f7b3df5e2243

这个 commit 也挺有意思的,bang 发现了 JSValue 原生方法就支持转换 CGRect/CGSize/CGPoint/NSRange struct,免去了额外的扩展支持。从侧面也说明对于 Apple API 越了解越能发现简单的解决方式,上述的 class_replaceMethod 同理。

规范定义了 Extension 机制

Extension 扩展机制保证了核心代码的精简和稳定。

bang 的文章里解释了 Extension 接口设计的目标:

  • 接口清晰
  • 每个扩展独立存在,互不影响
  • 不影响JPEngine的正常使用,尽量少暴露JPEngine的接口
  • 扩展的接口可扩展,以后有其他扩展需求可以在其基础上添加
  • 动态加载,扩展可能会给JS全局变量添加很多接口,最好能在真正使用到时才加载
@protocol JPExtensionProtocol <NSObject>
@optional
- (void)main:(JSContext *)context;

- (size_t)sizeOfStructWithTypeName:(NSString *)typeName;
- (NSDictionary *)dictOfStruct:(void *)structData typeName:(NSString *)typeName;
- (void)structData:(void *)structData ofDict:(NSDictionary *)dict typeName:(NSString *)typeName;
@end

@interface JPExtension : NSObject <JPExtensionProtocol>
+ (instancetype)instance;
- (void *)formatPointerJSToOC:(JSValue *)val;
- (id)formatPointerOCToJS:(void *)pointer;
- (id)formatJSToOC:(JSValue *)val;
- (id)formatOCToJS:(id)obj;
id formatJSToOC(JSValue *val);
id formatOCToJS(id obj);

@implementation JPExtension

+ (instancetype)instance {
    return [[self alloc] init];
}

- (void *)formatPointerJSToOC:(JSValue *)val {
    id obj = [val toObject];
    if ([obj isKindOfClass:[NSDictionary class]]) { // 包装对象
        if (obj[@"__obj"] && [obj[@"__obj"] isKindOfClass:[JPBoxing class]]) {
            return [(JPBoxing *)(obj[@"__obj"]) unboxPointer];
        } else {
            return NULL;
        }
    } else if (![val toBool]) { // false
        return NULL;
    } else{ // JPBoxing
        return [((JPBoxing *)[val toObject]) unboxPointer];
    }
}

- (id)formatPointerOCToJS:(void *)pointer {
    return formatOCToJS([JPBoxing boxPointer:pointer]);
}

- (id)formatJSToOC:(JSValue *)val {
    if (![val toBool]) { // false
        return nil;
    }
    return formatJSToOC(val);
}

- (id)formatOCToJS:(id)obj {
    // 疑问,为什么代理给 JS 方法去执行?
    // Unified the return value of C API
Extensions and Objective-C method.
    return [[JSContext currentContext][@"_formatOCToJS"] callWithArguments:@[formatOCToJS(obj)]];
}

@end

在实现中区分了 extensions 和 structExtensions。

增加了 JPMemory 扩展,用于 JS 里直接操作内存和结构体分配

context[@"getPointer"] = ^id(JSValue *jsVal) {
    void **p = malloc(sizeof(void *));
    void *pointer = [self formatPointerJSToOC:jsVal];
    if (pointer != NULL) {
        *p = pointer;
    } else {
        id obj = [self formatJSToOC:jsVal];
        *p = (__bridge void*)obj;
    }
    return [self formatPointerOCToJS:p];
};

存在一个没看懂的代码,为什么不是 void *p = malloc(sizeof(void));,而需要定义成 void **

v0.1.1

版本动态

  • 性能提升,调用 Patch 方法时,直接处理 invocation
  • 增加 __weak() 和 __strong() 接口
  • 增加 JPStructPointer,支持定义结构体,根据结构体指针和 key 获取 value 的方法,以及 struct 和 NSDictionry 直接的互转方法
  • 增加 performSelector() 接口,直接调用 OC 方法

性能提升,调用 Patch 方法时,直接处理 invocation

https://github.com/bang590/JSPatch/commit/60266cb3456585b4a9a3e4b7bd83b2955b988595

在 overrideMethod 阶段,将 JPSelector 方法也指向 msgForwardIMP,不再指向自定义方法。

在 JPForwardInvocation 阶段,如果调用的是 JPSelector 方法,则转发到 ORIGforwardInovation。返回时,不再调用指向 JPSelector 方法的 inovation,而是直接组装好 setReturnValue。

增加 __weak() 和 __strong() 接口

通过 JPBoxing 里增加 weak 类型的 weakObj 引用来保存。 至于实现细节还没有理清楚。

增加 JPStructPointer,支持定义结构体,根据结构体指针和 key 获取 value 的方法,以及 struct 和 NSDictionry 直接的互转方法

增加 performSelector() 接口,直接调用 OC 方法

给 MethodSignature 增加 Cache

Caching the MethodSignature when calling Objective-C methods in JavaScript. Make the performance 25% faster than before.

https://github.com/bang590/JSPatch/commit/370b8d3b62639de21750f4b9a9668f0abd842591

性能的瓶颈在于 methodSignature = [cls instanceMethodSignatureForSelector:selector]; 方法调用?怎么定位的性能问题?

v0.1.3

版本动态

  • 完善 console.log 方法

完善 console.log 方法

  if (global.console) {
    var jsLogger = console.log;
    // 重新包装 console.log 方法,既执行 _OC_log 用于 NSLog 输出,又执行 log 方法用于控制台输出
    global.console.log = function() {
      global._OC_log.apply(global, arguments);
      if (jsLogger) {
        jsLogger.apply(global.console, arguments);
      }
    }
  } else {
    global.console = {
      log: global._OC_log
    }
  }

对比最初的版本:

  global.console = {
    log: global._OC_log
  }

v0.1.4

版本动态

  • 增加性能测试 Case,采用 measureBlock: 方法,测试各种替换,执行,包括 JS 方法和 OC 方法
  • 性能提升,通过 NSRecursiveLock 和 NSLock 替换 @synchronized
  • 增加了 __bridge_id() 和 CFRelease() 接口
  • 修复内存泄漏
  • 支持 JS 自定义结构体作为参数和返回值使用

增加性能测试 Case

性能 Case 在执行 10000 次的基准下,基本在 0.2s 以内

性能提升,通过 NSRecursiveLock 和 NSLock 替换 @synchronized

https://github.com/bang590/JSPatch/commit/72717d81b556df80bc4c84ddb303da063b584bc2

NSRecursiveLock 和 NSLock 在使用选择上有什么差别?

增加了 __bridge_id() 和 CFRelease() 接口

context[@"__bridge_id"] = ^id(JSValue *jsVal) {
    void *p = [self formatPointerJSToOC:jsVal];
    id obj = (__bridge id)p;
    return [self formatOCToJS:obj];
};

context[@"CFRelease"] = ^void(JSValue *jsVal) {
    CFRelease([self formatPointerJSToOC:jsVal]);
};

为什么 __weak 不像 __bridge_id 一样实现?

修复内存泄漏

终于修复了在 0.0.1 就引入的内存泄漏问题,也取消了 typeDescription 和 methodSignature 混用。

https://github.com/bang590/JSPatch/commit/799d70c5a973265790fecf623a6639be39a59459

https://github.com/bang590/JSPatch/commit/5d5208668b9d287df971966034680426572aae96

支持自定义结构体作为参数和返回值使用

将传入的自定义结构体包装成 JSValue

[argList addObject:[JSValue valueWithObject:dict inContext:_context]];

设置返回值时,解析类型,创建 void * 指针,指向结构体。

@synchronized (_context) {
    NSDictionary *structDefine = registeredStruct[typeString];
    if (structDefine) {
        size_t size = sizeOfStructTypes(structDefine[@"types"]);
        JP_FWD_RET_CALL_JS
        void *ret = malloc(size);
        NSDictionary *dict = formatJSToOC(jsval);
        getStructDataWithDict(ret, dict, structDefine);
        [invocation setReturnValue:ret];
    }
}

v0.1.5

版本动态

  • 增加 performSelectorInOC(sel, args, cb) 接口,在 JSContext 之外执行 OC 方法,避免 JavaScriptCore context lock,能够并发执行 OC 方法
  • 增加 JPLoader
  • 代码优化和问题修复

增加 performSelectorInOC(sel, args, cb) 接口,在 JSContext 之外执行 OC 方法,避免 JavaScriptCore context lock,能够并发执行 OC 方法

https://github.com/bang590/JSPatch/commit/6e2ab4f5eb750d364134f2eaac5483373d289972

实现还是比较简单的,就是在 __c 方法里区分 performSelectorInOC,返回一个配置参数对象,然后真正调用时,传递到 JPforwardInvocation 里识别特殊配置参数,单独执行 callSelector。

另外,想吐槽的是代码提交还是不够规范,没有说明地改动一些东西,单元测试里找不到对应的修改。

v0.2

版本动态

  • 增加 addProtocol() 接口
  • 增加对可变参数的支持
  • 修复在父类forwardInvocation被重写时可能引发的bug
  • 增加 JPCleaner() 接口

v1.0

版本动态

  • 增加在 defineClass 增加属性
  • 增加 JPLocker 和 JPSpecialInit
  • 增加 defineJSClass 方法,支持以 OC 方式定义 JS 方法

v1.1

版本动态

  • 引入 libffi,支持动态调用 C 函数

v1.1.1

版本动态

  • 增加 po() 和 bt() 方法,用于 Safari 调试

总结

从架构上理解

从流程上理解

从 TestCase 上理解