背景
为了快速认识整体框架,并且学习如何构思这个框架。
方法调用
我们希望在js实现这样的调用:
UIView.alloc().init()
- UIView哪里来?(require)
我们要用UIView,那么当然就必须创建js对象。当然require不是唯一的方式,但是也是比较好理解的方式。
var UIView = require('UIView');
var _require = function(clsName) {
if (!global[clsName]) {
global[clsName] = {
__clsName: clsName
}
}
return global[clsName]
}
- JS方法的调用
- 方法的调用
我们希望是这样的UIView.alloc()
,这样必须需要先添加alloc
方法。因为没有像OC那样的转发机制。那么js对象大致是这样的:
__clsName: "UIView",
alloc: function() {…},
beginAnimations_context: function() {…},
setAnimationsEnabled: function(){…},
...
这样的话,即使搞继承关系,也大倒难以接受。
- 为了解决上面的问题
我们能不能不暴露到对象直接调用?作者想到一个方法,就是再OC执行js前,通过正则表达式都调用一个统一的方式去处理。
UIView.alloc().init()
->
UIView.__c('alloc')().__c('init')()
这样所有对象都可以经过方法__c方法去调用。
Object.defineProperty(Object.prototype, '__c', {value: function(methodName) {
if (!this.__obj && !this.__clsName) return this[methodName].bind(this);
var self = this
return function(){
var args = Array.prototype.slice.call(arguments)
return _methodFunc(self.__obj, self.__clsName, methodName, args, self.__isSuper)
}
}})
_methodFunc() 就是把相关信息传给OC,OC用 Runtime 接口调用相应方法,返回结果值,这个调用就结束了。至此,内存消耗直降 99%
- 消息传递
我们知道了_methodFunc
方法,需要做桥接。那么就需要知道交互原理。JSContext *context = [[JSContext alloc] init]; context[@"hello"] = ^(NSString *msg) { NSLog(@"hello %@", msg); }; [_context evaluateScript:@"hello('word')"];
_methodFunc
也是一样,只是参数多几个。
- 方法调用中的问题。
类的方法调用,对象的方法调用。
- 类的方法调用相对简单。
在require('UIView')
的时候就可以添加一个属性叫__isCls
来表示类,这样直接通过反射就可以获取对象的UIView类。 - 那么实例方法的调用呢?
由于OC对象,肯定是OC返回的。作者使用了指针进行标识。但是由于需要在js里根据对象做区分,所以在OC将对象返回到js进行了转换,用来标识。
让 OC 对象作为这个 NSDictionary 的一个值,这样在 JS 里这个对象就变成:static NSDictionary *_wrapObj(id obj) { return @{@"__obj": obj}; }
那么在{__obj: [OC Object 对象指针]}
__c
中判断如果有__obj
,就取出并传回OC即可。 - 会遇到内存管理的问题
如果全局保存对象,那么会内存泄漏。如果不全局保存,可以预想到会发生野指针。作者的思路是JS持有,则引用计数+1,JS进行垃圾回收时释放。JS是有类似析构函数呢?还是有其它,还得进一步深挖。
- 类型转换
至此,方法调用过程中的坑就差不多了。接下来是OC调起具体方法的坑了。没错,用的就是NSInvocation
。这样必然会遇到类型转换的问题。
5.1 基本思路
通过defineClass
任意替换一个类的方法。实际上这样需要OC替换原方法,并且新增了-ORIGViewDidLoad
方法(例子)指向原来的方法。如果没有参数,当然就很简单,如果有参数的话,那么就会遇到转换的问题。
5.2 va_list实现(32位)
va_list结合对应的参数签名,获取精确的类型。arm64 下 va_list 的结构改变了,会有问题。具体不展开。
5.3 ForwardInvocation实现(64位)
使用这个是为了-methodSignatureForSelector:, -forwardInvocation:
中NSInvocation 对象保存了所有参数值。而class_replaceMethod
的实现变成了统一的实现方法,都是走_objc_msgForward
,并且替换-forwardInvocation:
,从而实现走转发机制的目的。这样轻松获取所有参数,但是有一个问题。如果真要用转发机制呢?
5.4 真用转发机制
如果JS有替换方法就用替换方法,没有的话,就用原来的。当然替换方法也可以调用原来的逻辑。
5.5 转换细节
假设有个方法是tool.hello(0.5)
,那么怎么转换?答案是JS到OC,会变成NSNumber,假设OC方法hello的参数其实是float
,那么实际上就是[value floatValue],从而获取正确的参数,然后传到nsinvocation
处理。其实以前自己写的框架是借助YYModel
,会简单很多,少很多事儿。 - 那么新增方法呢?
其实新增方法处理比较简单,原来有就替换,没有就新增。其它和替换方法的流程差不多。
- 协议怎么处理?
通过objc_getProtocol
和protocol_copyMethodDescriptionList
接口把 Protocol 对应的方法取出来,若匹配上,则按其方法的定义走方法替换的流程。否则新增协议,并添加到对应的类。 - 增加的js变量实现
JSPatch可以通过 -getProp:, -setProp:forKey: 这两个方法给对象动态添加成员变量。实现上用了运行时关联接口 objc_getAssociatedObject() 和 objc_setAssociatedObject() 模拟。
本来OC有 class_addIvar() 可以为类添加成员,但必须在类注册之前添加完,注册完成后无法添加,这意味着可以为在JS新增的类添加成员,但不能为OC上已存在的类添加,所以只能用上述方法模拟。
其实作者是这样说的,但是实际上,我的考虑是这样的,直接js获取就可以了,也没必要使用关联列表。其实不影响使用。
- self关键字
defineClass("JPViewController: UIViewController", {
viewDidLoad: function() {
var view = self.view()
...
},
}
JSPatch支持直接在defineClass里的实例方法里直接使用 self 关键字,跟OC一样 self 是指当前对象,这个 self 关键字是怎样实现的呢?实际上这个self是个全局变量,在 defineClass 里对实例方法进行了包装,在调用实例方法之前,会把全局变量 self 设为当前对象,调用完后设回空,就可以在执行实例方法的过程中使用 self 变量了。
- super关键字
做法是调用 self.super()时,__c函数会做特殊处理,其实是加了个标识,返回新的对象。然后新增调用方法,传到OC的统一方法,再统一处理。 - 扩展
- Struct 支持
require('JPEngine').defineStruct({
"name": "JPDemoStruct",
"types": "FldB",
"keys": ["a", "b", "c", "d"]
})
/* 上面其实相当于下面这样的结构体,结构体是个连续的内存地址
struct JPDemoStruct {
CGFloat a;
long b;
double c;
BOOL d;
}
*/
js通过上面的定义,OC与js两边都保存一份配置表,就可以根据types
的顺序去获取对应的变量参数了。
for (int i = 0; i < types.count; i ++) {
size_t size = sizeof(types[i]); //types[i] 是 float double int 等类型
void *val = malloc(size);
memcpy(val, structData + position, size);
position += size;
}
从JS到OC也是类似的,只是先生成整个结构体大小的地址,然后再按上面说的顺序塞入相应位置。
- C函数支持
没错,只能这种形式,没法通过反射去调用。由于全部都扩展会大到影响性能,所以作者分成不同的模块,可以按需接入。context[@"memcpy"] = ^(JSValue *des, JSValue *src, size_t n) { memcpy(des, src, n); };
- 关于
Special Struct
如果替换方法的返回值是某些 struct,_objc_msgForward的话,会crash。结论是非 arm64 下,是 special struct 就走 _objc_msgForward_stret,否则走 _objc_msgForward。具体看作者的文档 - 关于内存问题
- i.Double Release
涉及OC对象和指针的转换,需要明确内存管理内容的。那么Double Release是怎么发生的呢?外面用id result
作为变量,而getReturnValue
的参数为void *
。这就导致result
在当前方法结束会自动添加release
,而void *
并不会对参数进行retain
。根据内存管理的约定如果使用new
的话,需要release
,所以getReturnValue
内部是new
的调用就没问题。但是如果是其它方法,不需要release
,而是由自动释放池管理。但实际上ARC又添加了释放的方法。所以就会造成两次释放问题。 - ii.内存泄露
根据内存管理的约定如果使用new
的话,需要release。需要[invocation getReturnValue:&result];
的内部。如果是用void *obj的方式做变量的话,就会没有释放。所以需要外面释放。当然如果用了id的方式做变量的话,会自动添加release
就不会有问题的。但是对于非new
等方式,那么实际上已经加入autorelease了,自动添加的release
就会有问题,就是上面的二次释放问题。实际上getReturnValue
方法内部,并不会对象所有权进行转移。
- i.Double Release
- 关于名字转换
作者把js的两个下划线_当成OC的,用以转换,存在一定的问题,但一般不影响。 - 封箱
为了处理,OC 返回到 JS 时 JavaScriptCore 把它们转成了 JS 的 Array / Object / String,和原对象的联系脱离了关系。 - nil的处理
14.1 为了区分区分NSNull/nil,所以js搞了个特殊变量nsnull来表示NSNull,其它照常。 - 链式调用
由于js的null
会调用链式调用失效,所以最后是用false来代表nil,几乎完美的处理了所有问题。