在上一篇文章中已经简单介绍了JSPatch的简单使用过程,可见JSPatch是一个威力无比强大神器,可以通过中间转化,让原生运行动态下发的JS代码,从而实现应用行为的动态更新,虽然现在该框架被apple封掉了,但是不影响通过这个框架进行技术分析,汲取营养的作用,尤其是对于熟悉动态注册类,远行时交换方法时技术的来讲是非常好的源代码研究材料.
require
JS和原生之间实现调用,面临的第一个问题就是原生的类如何能够出现在JS的调用中被识别.在JS中虽然一切皆对象,但是对象和对象之间并不完全一样,OC中的对象在JS中就都只是个"普通"对象而已,可以在JS和原生环境之间传递却不能直接调用相关的方法.那么如果在JS中实现原生方法的编译正常通过呢?这就要从作者定义require方法说起.
在使用相关的类之前,通过自定义的require方法将OC中的类字符串引用进来,这样就会生成一个全局的JS对象,对象的名称就是注册字符串,包含了一个key为"__clsName",value为clsName的属性.这样这个字符串就代表了一个新的全局JS的对象,可以正常编译JS代码.
var _require = function(clsName) {
if (!global[clsName]) {
//根据类名生成只有一个属性的Object,并对全局可见
global[clsName] = {
__clsName: clsName
}
}
return global[clsName]
}
//require方法是定义在全局的,所以在整个JS环境中都是可见的
global.require = function() {
var lastRequire
for (var i = 0; i < arguments.length; i ++) {
//从这个实现中可以看出,不仅可以使用require("UIView", "UIImageView")这种方式,还可以直接使用require("UIView, UIImageView")这种方式都是合法的
arguments[i].split(',').forEach(function(clsName) {
lastRequire = _require(clsName.trim())
})
}
return lastRequire
}
全局的require方法实现:
- 定义lastRequire变量;
- 遍历arguments中传入的参数,调用_require参数,并将返回值赋值给lastRequire;
- 返回lastRequire.所以可以在执行方法之前调用require,也可以在使用到某个类时直接调用require.
self
在OC中的方法是默认带有self和cmd两个隐藏参数的,所以可以直接使用self来进行方法调用,而在JS中this关键字用来替代这一功能,但是JS中的this只是方法的调用者(一定是方法所属的对象,this指向比较复杂,可能是一个方法也可能啥都不是),所以在JSPatch中,同样使用了将方法调用者绑定在全局对象的self属性上的做法,这样在方法执行期间,self指向方法调用者(在JSPatch实现中其实并不是真正的方法调用者,而是包含了方法调用者信息的JPBoxing对象),在方法执行结束之后将全局的self的对象进行改变.
var args = _formatOCToJS(Array.prototype.slice.call(arguments))
var lastSelf = global.self
//在调用JS方法时,将方法"调用者"(其实并不是真正的方法调用对象而是封装之后的包含了调用者相关信息的新对象)绑定在全局对象上
global.self = args[0]
if (global.self) global.self.__realClsName = realClsName
args.splice(0,1)
var ret = originMethod.apply(originMethod, args)
//方法执行完成之后将self重新赋值:所以只有在方法执行期间self才是方法的"调用者"
global.self = lastSelf
return ret
原生调用JS方法时,截取第一个参数并挂载在全局对象上,这样对于在执行的方法就可以识别self变量,并执行相应的方法调用.
property
在JSPatch中,允许为类定义/添加属性.而在OC中如果想要动态添加属性只能使用运行时提供的objc_setAssociatedObject方法来完成,JS借用了这一实现技术,但是并没有wan q依靠这一技术.在JSPatch中,为属性生成了单独的setter和getter方法,并指向了OC的_OC_setCustomProps和_OC_getCustomProps实现,
context[@"_OC_getCustomProps"] = ^id(JSValue *obj) { id realObj = formatJSToOC(obj); return objc_getAssociatedObject(realObj, kPropAssociatedObjectKey); }; context[@"_OC_setCustomProps"] = ^(JSValue *obj, JSValue *val) { id realObj = formatJSToOC(obj); objc_setAssociatedObject(realObj, kPropAssociatedObjectKey, val, OBJC_ASSOCIATION_RETAIN_NONATOMIC); };
而每一个需要动态绑定属性的对象,都只绑定了一个集合(一个类似于OC中NSDictionary的JSValue对象)对象,而这个集合为了该对象上绑定的所有属性,其中key为属性名,而value为属性的值.
var _propertiesGetFun = function(name){
return function(){
var slf = this;
if (!slf.__ocProps) {
var props = _OC_getCustomProps(slf.__obj)
if (!props) {
//当属性值为空时初始化props并调用_OC_setCustomProps进行存储
props = {}
_OC_setCustomProps(slf.__obj, props)
}
slf.__ocProps = props;
}
return slf.__ocProps[name];
};
}
var _propertiesSetFun = function(name){
return function(jval){
var slf = this;
if (!slf.__ocProps) {
var props = _OC_getCustomProps(slf.__obj)
if (!props) {
props = {}
_OC_setCustomProps(slf.__obj, props)
}
slf.__ocProps = props;
}
//可以看出添加新的变量并没有进行对象的OC存储(没有调用_OC_setCustomProps方法)
slf.__ocProps[name] = jval;
};
}
JS方法调用
定义在JS中的方法大概会有两种方式的调用:一种是供原生调用;另一种是JS内部调用.
原生调用JS方法
定义在JS中的方法要么需要动态添加到对应类中,要么对原始的方法实现进行替换.但是无论哪种方式都会在原生需要的时候进行方法调用,而原生调用的实际上是被封装成JSValue对象的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)
}
}]
})()
}
}
_formatDefineMethods方法主要实现将定义的原始方法进行重新封装之后给原生使用:最主要的实现就是将原始的JS方法实现封装成了数组,数组中的第一个元素是方法中参数的个数,第二个是进行修改之后的原始方法.之所以在这里加入了方法的参数个数,有至少有两个方面的意义:
- 在OC中使用运行时进行动态方法添加/替换时,需要用到方法的编码类型(为了简化实现在JSPatch中JS中自定义的方法直接受id类型参数)使用参数个数就可以自己生成方法编码类型进行方法注册;
- 由于方法命名规范的局限,在OC方法名中的":"在JS方法定义时只能使用"_"进行表示.这样会引入方法最后一个位置有没有参数的问题.而有个方法参数个数这个参数可以修正这一问题.
JS内部调用
在其他JS函数中也有可能会调用JS中函数,由于JS函数本身就存在于当前JS执行环境中,没有必要再经过原生传递调用可以直接使用JS进行调用.在解析JS定义的方法时,使用了全局的对象进行存储,以方便JS内部进行调用
_ocCls[className] = {
instMethods: {},
clsMethods: {},
}
if (superCls.length && _ocCls[superCls]) {
for (var funcName in _ocCls[superCls]['instMethods']) {
_ocCls[className]['instMethods'][funcName] = _ocCls[superCls]['instMethods'][funcName]
}
for (var funcName in _ocCls[superCls]['clsMethods']) {
_ocCls[className]['clsMethods'][funcName] = _ocCls[superCls]['clsMethods'][funcName]
}
}
_setupJSMethod(className, instMethods, 1, realClsName)
_setupJSMethod(className, clsMethods, 0, realClsName)
内部调用时由于原来的调用者已经是调用方法的"OC对象",只需要将方法调用者挂载到全局对象上,在方法内部可以识别self即可.
var _wrapLocalMethod = function(methodName, func, realClsName) {
return function() {
var lastSelf = global.self
global.self = this
this.__realClsName = realClsName
var ret = func.apply(this, arguments)
global.self = lastSelf
return ret
}
}
var _setupJSMethod = function(className, methods, isInst, realClsName) {
for (var name in methods) {
var key = isInst ? 'instMethods': 'clsMethods',
func = methods[name]
_ocCls[className][key][name] = _wrapLocalMethod(name, func, realClsName)
}
}
defineClass
JS中的defineClass方法用于定义需要注册/替换到OC中的函数实现:如果原始类中已经存在对应的方法,就会将原始的方法名称前添加"ORIG"作为新方法名保存实现,然后将原始的方法名实现指向msgForwardIMP;如果原始类中不存在对应的方法,则直接将方法名指向msgForwardIMP.这样在调用到方法时,就会触发消息转发流程,在替换之后的forwardingInvocation:中处理方法实现.
global.defineClass = function(declaration, properties, instMethods, clsMethods) {
var newInstMethods = {}, newClsMethods = {}
if (!(properties instanceof Array)) {
//属性是可选项
clsMethods = instMethods
instMethods = properties
properties = null
}
if (properties) {
//单独定义属性的get和set方法
properties.forEach(function(name){
if (!instMethods[name]) {
instMethods[name] = _propertiesGetFun(name);
}
var nameOfSet = "set"+ name.substr(0,1).toUpperCase() + name.substr(1);
if (!instMethods[nameOfSet]) {
instMethods[nameOfSet] = _propertiesSetFun(name);
}
});
}
//分离类名
var realClsName = declaration.split(':')[0].trim()
//封装JS方法,将原始的方法名对应的JS函数实现转化为参数个数+实现
//原始实现{functionName: function(){}} ==> functionName: [countOfArguments, function(){}]
_formatDefineMethods(instMethods, newInstMethods, realClsName)
_formatDefineMethods(clsMethods, newClsMethods, realClsName)
//在OC中注册/替换方法实现
var ret = _OC_defineClass(declaration, newInstMethods, newClsMethods)
var className = ret['cls']
var superCls = ret['superCls']
_ocCls[className] = {
instMethods: {},
clsMethods: {},
}
if (superCls.length && _ocCls[superCls]) {
for (var funcName in _ocCls[superCls]['instMethods']) {
_ocCls[className]['instMethods'][funcName] = _ocCls[superCls]['instMethods'][funcName]
}
for (var funcName in _ocCls[superCls]['clsMethods']) {
_ocCls[className]['clsMethods'][funcName] = _ocCls[superCls]['clsMethods'][funcName]
}
}
//供JS内部相互调用
_setupJSMethod(className, instMethods, 1, realClsName)
_setupJSMethod(className, clsMethods, 0, realClsName)
//将“类对象”挂载到全局对象,所以使用defineClass之后使用类时,可以不用再require(className)
return require(className)
}
- 处理properties是可选项:如果没有properties项,需要将instMethods,clsMethods进行重新赋值;
- 单独定义属性的get和set方法:属性的存储并没有直接绑定在OC中的对象上,而是在JS内部针为每一个需要绑定属性的对象单独维护了一个key-value对象,所以需要将属相的get和set属性做单独实现;
- 分离类名并在OC中注册/替换方法:分离出真正的类名并将JS中定义的方法实现注册/替换到OC类系统中;
- 处理JS内部方法相互调用细节:将JS中定义的方法通过全局维护的对象进行存储在JS进行方法调用时判断是否可以直接调用JS中的方法,如果可以直接调用;否则继续进行方法查找;
- 将注册的类名挂载到全局对象上并进行返回.
Block支持
Block是OC中经常用到的一个结构,对于处理方法回调实现响应式等都具有重要的意义.在JSPatch中也对Block做了支持:通过使用block方法将block封装为一个JS对象,block的实现通过Function进行替代.
global.block = function(args, cb) {
var that = this
var slf = global.self
if (args instanceof Function) {
cb = args
args = ''
}
var callback = function() {
var args = Array.prototype.slice.call(arguments)
global.self = slf
return cb.apply(that, _formatOCToJS(args))
}
var ret = {args: args, cb: callback, argCount: cb.length, __isBlock: 1}
return ret
}
而OC中的block在传入到JS运行环境时会自动转化为Function类型可以直接进行参与执行.