简述
在上篇文章:JOBridge之一任意方法的Swizzle(链接地址https://www.jianshu.com/p/905e06eeda7b),介绍了Swizzle任意方法(暂不包含有变长参数中的匿名参数,例如stringWithFormat:的第二个以上的参数,对于这种情况的处理较为麻烦,以后再处理)的详细方案,其做法是定义一个无参数无返回值的C桩函数,将被替换方法替换到该桩函数,在桩函数内部暂存参数,调用解析函数根据方法签名解析出所有参数放入数组,然后进入JOCallJsFunction调用对应的JS方法。本篇博客继续讲根据JS调用class注册方法,如何注册一个类以及其属性和方法;以及向JS上下文开放所有的原生方法,并完成参数转换、调用和返回值转换等。此篇完成后JOBridge基本就可以使用了,利用其可以重新一个原生的页面,实现动态更新代码,甚至于实时更新代码(不过这种方式有一定风险),下一篇讲如何比较简单的开放c函数调用入口和其他一些零散的东西和优化点。废话不多说,直接进入正题。
注册类
Native通过js上下文向js开放注册接口,js通过该接口注册类。
jsContext[@"interface"] = ^(JSValue *className, JSValue *properties, JSValue *classMethods, JSValue *metaClassMethods) {
[JOClass classWithName:className properties:properties classMethods:classMethods metaClassMethods:metaClassMethods];
};
注册时传入类名字,继承的父类,实现的协议,属性列表,类方法,元类方法等参数,然后调用JOClass的方法来注册,该方法会依次调用parseClass,parseMethods,parseProperties,addDealloc来完成类的注册,整个过程不难,简单说明即可。
parseClass传入的参className是包含类名,父类名和协议的字符串,解析出名字后调用OC的运行时方法创建类和添加协议。
- (void)parseClass:(NSString *)className {
NSString *aClassName = JOTools.trim([self getClassName:className]);
NSString *superClassName = JOTools.trim([self getSuperClassName:className]);
NSArray *protocolsName = [self getProtocolName:className];
self.class = objc_getClass(aClassName.UTF8String);
if (!self.class) {
self.isNewClass = YES;
self.superClass = objc_getClass(superClassName.UTF8String);
if (!self.superClass) {
self.class = NULL;
return;
}
self.class = objc_allocateClassPair(self.superClass, aClassName.UTF8String, 2);
}
if (!self.protocols) self.protocols = [NSMutableArray array];
for (NSString *obj in protocolsName) {
Protocol *pro = objc_getProtocol(JOTools.trim(obj).UTF8String);
class_addProtocol(self.class, pro);
[self.protocols addObject:pro];
}
}
注册属性和成员变量,
将js对象转成字典,枚举字典,调用JOAddPropertyAttribute具体添加属性和成员变量,完成后调用objc_registerClassPair注册类,此后类模板固定不能再添加成员变量,但属性和方法不受限制。
- (void)parseProperties:(JSValue *)jsValue {
NSDictionary *propertyList = [jsValue toDictionary];
for (NSString *obj in propertyList.allKeys) {//这里全部使用关联对象也可以,这不过先实现了class_addProperty
JOAddPropertyAttribute(self.class, obj, propertyList[obj], self.isNewClass);
}
if (self.isNewClass) objc_registerClassPair(self.class);
}
JOAddPropertyAttribute方法如下:
//目前只支持OC类型,基础类型暂时不支持,基础类型由NSNumber代替,而NSNumber和js中的Number等价
void JOAddPropertyAttribute(Class class, NSString *name, NSArray *att, BOOL isNewClass) {
if (!isNewClass) goto JOAssociatedTag;//使用关联对象只需要添加方法,关联对象目前只支持retain
objc_property_attribute_t nonatomic = {"N", ""};
objc_property_attribute_t ownership = {"&", ""};
objc_property_attribute_t type = {"T", @encode(id)};
if ([att.lastObject isEqualToString:@"weak"]) {
ownership = (objc_property_attribute_t){"W",""};
} else if ([att.lastObject isEqualToString:@"copy"]) {
ownership = (objc_property_attribute_t){"C",""};
}
// else if ([att.lastObject isEqualToString:@"assign"]) {
// type = (objc_property_attribute_t){"T", [[NSString stringWithFormat:@"@\"%@\"",att.firstObject] UTF8String]};
// }
objc_property_attribute_t attribute[] = { ownership, nonatomic, type};
BOOL success = class_addProperty(class, [name UTF8String], attribute, 3);
if (success) {
//这里似乎要手动调用class_addIvar才能将变量描述进去,仅用class_addProperty似乎不奏效。
class_addIvar(class, [[NSString stringWithFormat:@"_%@",name] UTF8String], sizeof(id), log2(sizeof(id)), @encode(id));
JOAssociatedTag:
class_addMethod(class, NSSelectorFromString(name), (IMP)JOGetter, "@@:");
NSString *head = [[name substringToIndex:1] uppercaseString];
NSString *set = [NSString stringWithFormat:@"set%@%@:", head, [name substringFromIndex:1]];
class_addMethod(class, NSSelectorFromString(set), (IMP)JOSetter, "v@:@");
}
}
这里为了简单,暂时仅支持OC类型的属性添加。如果不是JS动态创建类,则只能使用关联对象,直接goto到JOAssociatedTag,仅添加getter和setter方法,这里要小心的是编译器会添加release操作,goto别跳过去了,这会导致内存泄露。
如果是新创建的类,则可以添加成员变量。创建三个objc_property_attribute_t属性,调用class_addProperty添加属性,如果成功则调用class_addIvar添加成员变量,比较重要的是内存大小的计算log2(sizeof(id))
,其头文件有说明,对齐大小将以1左移align,所以只可能是1,2,4,8...,任何指针类型都传log2(sizeof(pointer_type))也就是8。
接下来通过动态方法添加的方式添加一组getter和setter,默认实现为JOGetter和JOSetter。
id JOGetter(id obj, SEL sel) {
NSString *key = [NSString stringWithFormat:@"_%@",NSStringFromSelector(sel)];
Ivar ivar = class_getInstanceVariable([obj class], [key UTF8String]);
if (ivar) {
return object_getIvar(obj, ivar);
} else {
return objc_getAssociatedObject(obj, JOAssociatedKey(key));
}
}
/* object_setIvar不会retain对象,而object_setIvarWithStrongDefault在iOS10之后才有效,
所以需要手动调用retain,并在父对象dealloc的时候调用release
*/
void JOSetter(id obj, SEL sel, id newValue) {
NSString *selStr = [NSStringFromSelector(sel) substringFromIndex:3];
NSString *head = [[selStr substringToIndex:1] lowercaseString];
NSString *tail = [selStr substringFromIndex:1];
tail = [tail substringToIndex:tail.length - 1];
NSString *key = [NSString stringWithFormat:@"_%@%@", head, tail];
Ivar ivar = class_getInstanceVariable([obj class], [key UTF8String]);
if (ivar) {
id value = object_getIvar(obj, ivar);
JOTools.release(value);
object_setIvar(obj, ivar, newValue);
JOTools.retain(newValue);
} else {
objc_setAssociatedObject(obj, JOAssociatedKey(key), newValue, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
}
JOGetter先通过class_getInstanceVariable
在类中查找实例描述Ivar,如果找到了则表示其为成员变量,调用对象的object_getIvar
方法获取成员变量值,否则就调用关联对象方法获取关联对象。
JOSetter套路类似,先获取描述,如果有描述则先调用object_setIvar
存储数据,需要注意的是其不会retain对象,而会retain对象的方法object_setIvarWithStrongDefault
在iOS10才可用,所以这里需要手动调用JOTools.retain()
。如果是关联对象就简单了,直接调用相关方法就好。需要注意的是这里的Key必须是唯一变量地址,这里将key放入全局字典,。
JOTools.retain实现如下,就是汇编调用_objc_retain
(当然也可以用别的实现方案),注意参数和堆栈,需要注意的是这里其实是有一个参数的存在x0中,但在该retain函数中我并不使用,所以不用在栈上存储,也就不用手动修改栈大小(不过stp, ldp命令会导致sp增减),直接调用_objc_retain
就可以了,不过retain这个桩函数会默认插入汇编指令ret
,因此这里需要将x29, x30入栈,如果不想入栈优化性能,可以把retain定义为宏函数,直接调用_objc_retain
。release同理。
OS_ALWAYS_INLINE void retain() {
asm volatile("stp x29, x30, [sp, #-0x10]!");
asm volatile("mov x29, sp");
asm volatile("bl _objc_retain");
asm volatile("mov sp, x29");
asm volatile("ldp x29, x30, [sp], #0x10");
}
OS_ALWAYS_INLINE void release() {
asm volatile("stp x29, x30, [sp, #-0x10]!");
asm volatile("mov x29, sp");
asm volatile("bl _objc_release");
asm volatile("mov sp, x29");
asm volatile("ldp x29, x30, [sp], #0x10");
}
注册方法
接下来解析JS方法,根据方法列表创建对应的Method。
- (void)parseMethods:(JSValue *)jsMethods isMeta:(BOOL)isMeta {
NSDictionary *methods = [jsMethods toDictionary];
for (NSString *method in methods) {
JSValue *jsMethod = [jsMethods valueForProperty:method];
SEL sel = [self getSelectorWithString:method];
//这里使用class_copyMethodList,其只会获取当前类方法,不会获取父类方法,而class_getInstanceMethod等会获取父类方法
Method ocMethod = isMeta ? JOGetMethodWithSelector(object_getClass(self.class), sel)
: JOGetMethodWithSelector(self.class, sel);
if (ocMethod) {
method_setImplementation(ocMethod, JOGlobalSwizzle);
JOAddJsMethod(self.class, NSStringFromSelector(sel), jsMethod);
continue;
}
Method ocSuperMethod = isMeta ? class_getClassMethod([self.class superclass], sel)
: class_getInstanceMethod([self.class superclass], sel);
if (ocSuperMethod) {
const char *type = method_getTypeEncoding(ocSuperMethod);
class_addMethod(self.class, sel, JOGlobalSwizzle, type);
JOAddJsMethod(self.class, NSStringFromSelector(sel), jsMethod);
continue;
}
char *type = NULL;
for (Protocol *p in self.protocols) {
type = protocol_getMethodDescription(p, sel, YES, !isMeta).types;
if (!type) type = protocol_getMethodDescription(p, sel, NO, !isMeta).types;
if (type) break;
}
//如果协议中也没有此方法签名,表明是由js新创建的方法,则获取js提供的签名
if (type) {
class_addMethod(isMeta ? object_getClass(self.class) : self.class, sel, JOGlobalSwizzle, type);
} else {
NSArray *array = [jsMethod toObject];
if ([array isKindOfClass:[NSArray class]]
&& array.count > 1
&& [array.firstObject isKindOfClass:[NSString class]]) {
const char *type = [array.firstObject UTF8String];
class_addMethod(isMeta ? object_getClass(self.class) : self.class, sel, JOGlobalSwizzle, type);
jsMethod = jsMethod[1];
} else {
continue;
}
}
JOAddJsMethod(self.class, NSStringFromSelector(sel), jsMethod);
}
}
获取JS中的SEL名字构建SEL,在当前类中获取对应的Method,注意使用class_copyMethodList。如果有对应的Method,则只需替换其实现为JOGlobalSwizzle,同时将对应的JS实现以ClassName和SEL为Key存入全局字典。这里就和上篇博客关联起来了,调用链为ClassA的MethodA->JOGlobalSwizzle->...->JOCallJsFunction->JS->(Native)。
如果当前类没有该方法,则跳过该类,去父类中循环获取,如果找到方法,则调用class_addMethod添加方法,相当于当前类重写该方法,调用JOAddJsMethod添加全局字典,并跳到下一次循环。
接下来去协议中查找方法签名,如果找到了则添加方法,如果没有找到,则表明这是个JS新定义的方法,获取JS提供签名后向类添加方法,最后把JS方法添加到全局字典。
目前class方法等注册和方法调用没有加锁,所以在调用时重新注册很可能会凉凉,也就是说最好不要实时跟新代码,注册最好在app启动时完成。
注册dealloc
前面讲到所有的成员变量会调用retain来持有变量,带来的结果就是内存泄露,所有需要在注册类的dealloc中来release一次。
//对JOSetter中retain的对象一次调用release
void JORelease(__unsafe_unretained id obj, SEL sel) {
unsigned int count;
Ivar *v = class_copyIvarList([obj class], &count);
for (int i = 0; i < count; ++i) {
const char *name = ivar_getName(v[i]);
Ivar ivar = class_getInstanceVariable([obj class],name);
__unsafe_unretained id value = object_getIvar(obj, ivar);
object_setIvar(obj, ivar, nil);
JOTools.release(value);
}
free(v);
//#pragma clang diagnostic push
//#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
//#pragma clang diagnostic ignored "-Wundeclared-selector"
// if ([obj respondsToSelector:@selector(JODealloc)]) {
// [obj performSelector:@selector(JODealloc)];
// }
//
//#pragma clang diagnostic pop
//调用父类dealloc实现,重写dealloc后,编译器会默认插入父类dealloc的调用,但这里修改其实现后,必须手动调用
IMP imp = class_getMethodImplementation([[obj class] superclass], sel);
imp ? ((void(*)(id, SEL))imp)(obj, sel) : nil;
}
这里将class所有的Ivar枚举出来,获取所有的成员对象,依次调用release,注意free IvarList(这里我一直忘了free,这么个小错误导致的内存泄露查了好久才查出来,😓)。
本来我是打算让JS通过重写JODealloc来实现自定义的dealloc过程,后来发现会导致crash,原因在于调用JODealloc会让js通过共享对象也持有当前对象,这里调用完成后当前对象已经没了,而js却还持有共享对象。目前我还没有想到好的解决方案,只能暂时不提供该功能,自己能砍需求就是牛逼啊!😎
我们平常重写dealloc方法,Xcode编译器会帮我们插入调用父类dealloc的代码,所以这里需要手动调用一下父类的dealloc,另外需要注意的是在这个函数实现中最好不要retain当前对象,weak也不能用,因为在调用了父类的dealloc后,对象已经释放了,这时候编译器帮你插入的release再调用就会crash。weak对象的问题在于其调用函数时会被retain,调用完会立即release,这也很可能crash。
JS访问所有的Native方法(JS->OC通用调用桥)
JSC提供共享对象,JS和Native可以相互传递参数,同时有了类似于JOGlobalSwizzle可以用于解析参数和转发消息函数之后,JS动态调用OC方法就有了可能,只需要将OC对象和SEL传给Native,就可以动态调用所有OC方法了,若不处理过多(种)的参数,通过performSelector动态调用,甚至只需要少量的代码就可以完成。如果在JSContext中注入一个方法来实现,那么js语法将会很复杂,同时不能使用“.”运算符,为了调用简单,我还是借鉴JSPatch类似的语法(没法子js不精通,玩不出新花样),只不过我这里实现就完全不一样了。
JS根类添加_oc_
属性
通过JSContext给js的根类Object的prototype原型添加一个_oc_
属性,其描述信息为@{JSPropertyDescriptorValueKey:^{...},JSPropertyDescriptorConfigurableKey:@(NO), JSPropertyDescriptorEnumerableKey:@(NO)}。这里使用JSPropertyDescriptorValueKey,并提供一个block,js就可以直接调用该属性了。具体的玩法,注释里面有描述。
/* 给js的根类添加一个_oc_方法(或者说一个属性吧),其返回了一个block,block第一个参数是selector名称,同时在上下文获取currentThis,从中取出obj,余下的参数则在后面,这里我只写了12个,添加也容易,补上去就行(当然还有其他麻烦一点的办法,比如根据selector动态获取,也可以通过预处理js将参数打包成数组,都可以支持任意长度了)。
js调用会被预处理,例如tableView.setDelegate_(self),会被转成tableView._oc_('setDelegate_',self),当然也可以使用其他的语法。在调用原生方法的时候我偷了一下懒,使用了NSInvocation。也可以像获取任意参数一样,强制写入参数到寄存器和栈来调用objc_msgSend(当然也可以直接调用其实现,但可能没有objc_msgSend的缓存效果),就是有点麻烦,后续有时间再改。
*/
[self.jsContext[@"Object"][@"prototype"] defineProperty:@"_oc_" descriptor:@{JSPropertyDescriptorValueKey:^id (JSValue *selName, JSValue *p0, JSValue *p1, JSValue *p2, JSValue *p3, JSValue *p4, JSValue *p5, JSValue *p6, JSValue *p7, JSValue *p8, JSValue *p9, JSValue *p10, JSValue *p11, JSValue *p12) {
NSString *tmp = [[selName toString] stringByReplacingOccurrencesOfString:@"__" withString:@"-"];
tmp = [tmp stringByReplacingOccurrencesOfString:@"_" withString:@":"];
SEL sel = NSSelectorFromString([tmp stringByReplacingOccurrencesOfString:@"-" withString:@"_"]);
id obj = [[JSContext currentThis] toObject];
NSAssert(obj, @"调用者容器不能为空");
if (![obj obj]) return [NSNull null];
obj = [obj obj];
NSMethodSignature *sign = [obj methodSignatureForSelector:sel];
NSAssert(sign, @"签名不能为空,请先检测JS调用函数是否书写正确,再检查OC是否提供此函数");
id params[] = {p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12};//创建c数组
NSInvocation *invoke = [NSInvocation invocationWithMethodSignature:sign];
[invoke setTarget:obj];
[invoke setSelector:sel];
NSUInteger num = [sign numberOfArguments];
for (int i = 2; i < num; ++i) {
const char* type = [sign getArgumentTypeAtIndex:i];
while (*type == 'r' || // const
*type == 'n' || // in
*type == 'N' || // inout
*type == 'o' || // out
*type == 'O' || // bycopy
*type == 'R' || // byref
*type == 'V') { // oneway
type++; // cut off useless prefix
}
switch (type[0]) {
case 'B': {BOOL v = [params[i-2] toBool]; [invoke setArgument:&v atIndex:i]; break;}
case 'c': {char v = [[params[i-2] toNumber] charValue]; [invoke setArgument:&v atIndex:i]; break;}
case 'C': {Byte v = [[params[i-2] toNumber] unsignedCharValue]; [invoke setArgument:&v atIndex:i]; break;}
case 's': {short v = [[params[i-2] toNumber] shortValue]; [invoke setArgument:&v atIndex:i]; break;}
case 'S': {unsigned short v = [[params[i-2] toNumber] unsignedShortValue]; [invoke setArgument:&v atIndex:i]; break;}
case 'i': {int v = [params[i-2] toInt32]; [invoke setArgument:&v atIndex:i]; break;}
case 'I': {unsigned int v = [params[i-2] toUInt32]; [invoke setArgument:&v atIndex:i]; break;}
case 'l': {long v = [[params[i-2] toNumber] longValue]; [invoke setArgument:&v atIndex:i]; break;}
case 'L': {unsigned long v = [[params[i-2] toNumber] unsignedLongValue]; [invoke setArgument:&v atIndex:i]; break;}
case 'q': {long long v = [[params[i-2] toNumber] longLongValue]; [invoke setArgument:&v atIndex:i]; break;}
case 'Q': {unsigned long long v = [[params[i-2] toNumber] unsignedLongLongValue]; [invoke setArgument:&v atIndex:i]; break;}
case 'f': {float v = [[params[i-2] toNumber] floatValue]; [invoke setArgument:&v atIndex:i]; break;}
case 'd': {double v = [params[i-2] toDouble]; [invoke setArgument:&v atIndex:i]; break;}
case '#':
case '@': {
__autoreleasing id v = [params[i-2] toObject];
if ([v isKindOfClass:[NSArray class]]
&& [v count] == 2
&& [v[0] isKindOfClass:[NSString class]]
&& [params[i-2][1] isInstanceOf:[JSContext currentContext][@"Function"] ]) {
//签名字符串不能放在栈上,否则很可能被覆盖掉
NSString *signString = v[0];
char *blockSgin = JOCopySgin(signString);
id jsBlocks = params[i-2][1];
/* 原生方法需要的参数是block,js传过来的参数是function,这时需要构造一个block(无参数无返回),强制修改这个block的签名。然后在block回调的时候才能根据签名解析出参数(Method或者说selector的签名在定义的时候,就由编译器搞定了,protocol也会生成签名,但如果仅仅是在类中声明而不实现是没有签名的,而block的签名则是在定义的时候才有,仅声明也没有,js没有类型信息,所以这里需要在js传funtion的时候手动签名)。JOGlobalParamsResolver只能根据第一个参数也就是block本身,如果不拷贝,则每次都是相同的block,无法获取正确的签名,如果强制每次修改签名,那么在异步执行的情况下,签名会冲突,所以需要拷贝整个block,并重新签名。
*/
v = ^() {
JOGlobalBlockGetParams();
//从x0中获取已经解析出的参数列表,是个NSArray,其中第一个参数是Block本身是隐藏参数
asm volatile("mov x1, x0");
uintptr_t *array = NULL;
void **arrayptr = (void **)&array;
asm volatile("ldr x0, %0": "=m"(arrayptr));
asm volatile("str x1, [x0]");
__autoreleasing NSArray *arr = (__bridge NSArray *)(void *)(*arrayptr);
__autoreleasing JSValue *ret = [jsBlocks callWithArguments:[arr subarrayWithRange:(NSRange){1, arr.count - 1}]];
char returnType = blockSgin[0];
//构建返回值和方法的返回值构建是一样的注意使用__autoreleasing变量以防止x0被破坏
JOConstructReturnValue(ret, returnType);//此句最好在方法的最末
};
v = (__bridge id)JOCopyBlock(v, blockSgin);
//修改block签名,JOGlobalParamsResolver解析参数时会获取该签名
// asm volatile("ldr x0, %0" : "=m"(v));
// asm volatile("ldr x8, [x0, 0x18]");
// asm volatile("add x1, x8, 0x10");
// asm volatile("add x2, x8, 0x20");
// asm volatile("ldr w3, [x0, 0x8]");
// asm volatile("tst w3, #0x2000000");
// asm volatile("csel x2, x1, x2, eq");
// asm volatile("ldr x0, %0": "=m"(blockSgin));
// asm volatile("str x0, [x2]" );
}
if ([v isKindOfClass:JOSelObj.class]) {
SEL s = [v sel];
[invoke setArgument:&s atIndex:i]; break;
} else if ([v isKindOfClass:JOObj.class]) {
v = [v obj];
[invoke setArgument:&v atIndex:i]; break;
} else if ([v isKindOfClass:JOPointerObj.class]) {
void *p = [v ptr];
[invoke setArgument:&p atIndex:i]; break;
} else if ([v isKindOfClass:JOWeakObj.class]) {
v = [v obj];
[invoke setArgument:&v atIndex:i]; break;
} else if ([v isKindOfClass:[NSNull class]]) {
v = nil;
[invoke setArgument:&v atIndex:i];
} else {
[invoke setArgument:&v atIndex:i];
}
break;
}
case '{': {
if (!strcmp(type, @encode(CGRect))) {
CGRect v = [params[i-2] toRect];
[invoke setArgument:&v atIndex:i];
} else if (!strcmp(type, @encode(CGPoint))) {
CGPoint v = [params[i-2] toPoint];
[invoke setArgument:&v atIndex:i];
} else if (!strcmp(type, @encode(CGSize))) {
CGSize v = [params[i-2] toSize];
[invoke setArgument:&v atIndex:i];
} else if (!strcmp(type, @encode(NSRange))) {
NSRange v = [params[i-2] toRange];
[invoke setArgument:&v atIndex:i];
}
//MARK:解析结构体
break;
}
case '^':
case '*': {
__autoreleasing id v = [params[i-2] toObject];
void *p = NULL;
if ([v isKindOfClass:[JOPointerObj class]]) {
p = [v ptr];
}
[invoke setArgument:&p atIndex:i];
break;
}
case ':': {
__autoreleasing id v = [params[i-2] toObject];
void *p = NULL;
if ([v isKindOfClass:[JOSelObj class]]) {
p = [v sel];
}
[invoke setArgument:&p atIndex:i];
break;
}
}
}
[invoke invoke];
const char *type = [sign methodReturnType];
while (*type == 'r' || // const
*type == 'n' || // in
*type == 'N' || // inout
*type == 'o' || // out
*type == 'O' || // bycopy
*type == 'R' || // byref
*type == 'V') { // oneway
type++; // cut off useless prefix
}
switch (type[0]) {
case 'v': return [NSNull null];
case 'B': {BOOL value; [invoke getReturnValue:&value]; return @(value);}
case 'c': {char value; [invoke getReturnValue:&value]; return @(value);}
case 'C': {Byte value; [invoke getReturnValue:&value]; return @(value);}
case 's': {short value; [invoke getReturnValue:&value]; return @(value);}
case 'S': {unsigned short value; [invoke getReturnValue:&value]; return @(value);}
case 'i': {int value; [invoke getReturnValue:&value]; return @(value);}
case 'I': {unsigned int value; [invoke getReturnValue:&value]; return @(value);}
case 'l': {long value; [invoke getReturnValue:&value]; return @(value);}
case 'L': {unsigned long value; [invoke getReturnValue:&value]; return @(value);}
case 'q': {long long value; [invoke getReturnValue:&value];
return @(value);}
case 'Q': {unsigned long long value; [invoke getReturnValue:&value];
return @(value);}
case 'f': {float value; [invoke getReturnValue:&value]; return @(value);}
case 'd': {double value; [invoke getReturnValue:&value]; return @(value);}
case '#': {
id retrunValue = nil;
[invoke getReturnValue:&retrunValue];
return MakeObj(retrunValue) ?: [NSNull null] ;
}
case '@': {
__unsafe_unretained id retrunValue = nil;
[invoke getReturnValue:&retrunValue];
JOObj *o = MakeObj(retrunValue);
if (sel == @selector(alloc) || sel == @selector(new)
|| sel == @selector(copy) || sel == @selector(mutableCopy)
|| (!strncmp(sel_getName(sel), "init", 4) && invoke.target != retrunValue)) {
JOTools.release(retrunValue);
}
return o ?: [NSNull null] ;
}
case '^':
case '*': {
void *retrunValue = NULL;
[invoke getReturnValue:&retrunValue];
return MakePointerObj(retrunValue) ?: [NSNull null] ;
}
case ':': {
void *retrunValue = NULL;
[invoke getReturnValue:&retrunValue];
return MakeSelObj(retrunValue) ?: [NSNull null];
}
case '{': {
if (!strcmp(type, @encode(CGRect))) {
CGRect v; [invoke getReturnValue:&v];
return [JSValue valueWithRect:v inContext:[JSContext currentContext]];
} else if (!strcmp(type, @encode(CGPoint))) {
CGPoint v; [invoke getReturnValue:&v];
return [JSValue valueWithPoint:v inContext:[JSContext currentContext]];
} else if (!strcmp(type, @encode(CGSize))) {
CGSize v; [invoke getReturnValue:&v];
return [JSValue valueWithSize:v inContext:[JSContext currentContext]];
} else if (!strcmp(type, @encode(NSRange))) {
NSRange v; [invoke getReturnValue:&v];
return [JSValue valueWithRange:v inContext:[JSContext currentContext]];
}
//MARK:更完善的结构体解析以后再想办法
}
default : break;
}
return [NSNull null];
}, JSPropertyDescriptorConfigurableKey:@(NO), JSPropertyDescriptorEnumerableKey:@(NO)}];
在这里通过[JSContext currentThis]获取this,就是打包OC中的self,解包得到得到真正的self对象,然后将SEL解析出来,通过创建NSMethodSignature和NSInvocation来调用。这里有个优化点,_oc_
调用很频繁,可以缓存这两个对象,因为type字符串确定后这两个对象也就确定了,缓存可以有一些性能提升,之前给忘了。当然也可以通过method_getArgumentType获取参数签名。
使用NSInvocation转发消息,能够比较好的处理参数和返回值。这两个switch我一直想简化,想来想去除了宏似乎也没有别的法子,只是目前为了方便阅读和调试,暂不用宏简化代码。用了NSInvocation,常规参数处理,需要说明一下的是基础数据类型会被封装成NSNumber(不打包成JOObj对象),其和js的Number对应,可以直接被js使用。其他也就没有太多需要介绍的了,JSC有数据转换方法,根据签名转就可以了。
block参数处理
重点说明Block参数,例如:当JS调用Native的enumerateObjectsUsingBlock:方法,需要传递传递一个Function,而这个Function明显不能被Native直接调用,一种朴素的想法就是给enumerateObjectsUsingBlock传一个Block,再在这个Block中调用JS Function。呵呵,想法是没错的,但是实现起来确实很麻烦。
因为Block参数的多样性,这里定义一种的通用Block似乎不够用?如果根据参数和返回值预先定义多个Block,可参数种类有十几种,排列后就需要定义几十上百个Block,😓,就算整型类型合并,只保留id,double,NSInteger,指针还是要定义很多个,想想还是算了吧。定义一种Block真的不够么?有了JOGlobalSwizzle成功思路,似乎也不是不可行。Block本质上也是OC对象,方法也是C语法,符合ARM64 PCSAA调用协议,那么解析参数就和OC完全一致,唯一不一样的是签名,不过只要有签名就可以解析参数。但有一个问题是Block对象在定义的时候编译器才会生成,如果仅仅是声明是没有的,没有Block对象也就没有签名,而Method的也类似,不过协议也会生成Method,但没有实现。因此在JS传入Function的时候需要给出签名,OK,思路有了,下面来看具体实现。
说一下这里的原理,block的签名实际是其包装函数指针的签名,捕获的参数跟签名没有关系,仅需要将签名copy给Block就行(如果每次都是串行调用这个通用Block,实际上不需要copy整个Block,只需要强制修改Block的签名就行了,也就是说block实际上就一个,只是每次调用传参时修改签名,回调是串行,所以签名也就不存在覆盖的情况,这也是我最开始的做法)。但异步调用这种非串行的方式,就不能简单的修改签名了,需要将对应签名Block与JS函数绑定在一起,因此需要将block拷贝一份,然后手动签名。
这里需要先定义一下对应的Block的数据结构,这是一个具体的Block结构,带两个捕获参数。
通用的block定义如下:
typedef struct _JOBlockDescriptor {
void *reserved;
unsigned long int size;
void (*copy)(void *dst, const void *src);
void *dispose;
const char *signature;//目前使size,copy,signatrue字段,其他占位即可
void *layout;
} _JOBlockDescriptor;
typedef struct _JOBlock {
Class isa;
short int retainCount;
short int flag;
int token;
void *BlockFunctionPtr;
_JOBlockDescriptor *descriptor;//目前仅用retainCount,descriptor,其他占位即可
//捕获的参数
JSValue *jsFunction;
char *sign;
} _JOBlock;
JOCopySgin会将签名拷贝到堆上,同时缓存到字典,同一个签名就不用拷贝了。
/* 拷贝一个签名到堆上并返回 */
char *JOCopySgin(NSString *sign) {
[_JOBlockLock lock];
JOPointerObj *obj = _JOBlockSigns[sign];
[_JOBlockLock unlock];
if (obj) return [obj ptr];
const char *type = [sign UTF8String];
void *blockSgin = malloc(strlen(type) + 1);
memcpy(blockSgin, type, strlen(type) + 1);
[_JOBlockLock lock];
_JOBlockSigns[sign] = MakePointerObj(blockSgin);
[_JOBlockLock unlock];
return blockSgin;
}
新分配一个block和descriptor空间,拷贝descriptor和signature。
/*
关注JOBridge中处理JS传过来的block(js function)参数,其都被一个无参数无返回值的block处理,这是一个原型block,其原始签名是固定的,但该block会调用各种不同的js function,也就意味着要处理不同的参数和返回值。如果串行调用,每次根据传入js function的签名修改即可,但如果存在异步操作,就会出问题,所以签名必须和js function对应,而实际上我这里只处理参数,所以只需要关注签名是否一致,不需要关注捕获参数。
为了确保在执行过程中JOSwizzle的JOGlobalParamsResolver取到正确的签名,需要将block拷贝一份,调用copy或者_Block_copy都不行,其检查flags时跳过了拷贝。所以这里只能构造相同的结构体来手动拷贝,_JOBlock与原型block对应,拷贝block,descriptor和sign。最后将修改descriptor和sign。
*/
id JOCopyBlock(id block, _JOBlock *blockCopy, _JOBlockDescriptor *blockDescriptor, char *blockSgin) {
//_Block_copy;
_JOBlock *blockPtr = (__bridge void *)block;
_JOBlockDescriptor *blockDescriptor = malloc(sizeof(_JOBlockDescriptor));
_JOBlock *blockCopy = malloc(sizeof(_JOBlock));
memcpy((void*)blockCopy, (void*)blockPtr, sizeof(_JOBlock));
memcpy(blockDescriptor, blockPtr->descriptor, sizeof(_JOBlockDescriptor));
// blockDescriptor->copy = (void *)_JOCopyHelper;
blockDescriptor->copy(blockCopy, blockPtr);
blockDescriptor->dispose = (void *)_JODisposeHelper;
blockCopy->retainCount = 0x0;
blockCopy->descriptor = blockDescriptor;
blockCopy->descriptor->signature = blockSgin;
return blockCopy;
}
手动调用block的copy函数,拷贝一下捕获的参数,说是copy其实是retain一下jsFunction。这里的copy实现可以不用替换。
替换dispose的实现代码,主要是为了释放descriptor,同时手动release一下jsFunction。
void _JODisposeHelper(_JOBlock *src) {
JOTools.release(src->jsFunction);
free(src->descriptor);
}
将retainCount设置为0,v = (__bridge id)JOCopyBlock(v, blockSgin);
赋值时候,会自动将其+1,就懒得前前后后的调用retain和release。ARM64位isa定义中,后19bit为引用计数器,但block似乎不太一样,引用计数器不在isa中,而在下一个4Byte,可能占用了32bit,但也可能不是,所有我这里定义retainCount为short int,16bit,一般也足够用了。
目前block是手动完成copy的,其实挺麻烦的,研究_Block_copy
汇编码时,发现其拷贝前检查了一个flag,然后跳过了拷贝,目前这个flag情况不明所以我没有通过强行修改flag的方式去让_Block_copy
生效,如果研究清楚了这个flag或许可以调用该方法copy了。
返回值内存管理
其他的情况没有什么特别的(更完善的结构体解析暂不实现),这里需要说明一下这种对象返回值的情况。
case '@': {
__unsafe_unretained id retrunValue = nil;
[invoke getReturnValue:&retrunValue];
JOObj *o = MakeObj(retrunValue);
if (sel == @selector(alloc) || sel == @selector(new)
|| sel == @selector(copy) || sel == @selector(mutableCopy)
|| (!strncmp(sel_getName(sel), "init", 4) && invoke.target != retrunValue)) {
JOTools.release(retrunValue);
}
return o ?: [NSNull null] ;
}
通过getReturnValue获取到返回值retrunValue,然后将其封装到JOObj中。
对于alloc,new,copy,mutableCopy需要调用一次release。为什么呢?因为这几种调用返回的是非autoreleasing对象,其retainCount默认为1,而且与外部声明的获取指针__unsafe_unretained id retrunValue
无关,但是返回JOObj给JS持有也会导致其retainCount+1,也就是说调用一次alloc导致retainCount+2了,那就需要release一次。使用MakeObj封装一次后再调用release可以将retrunValue转换成autoreleasing对象,同时返回给JS后交给js管理。
(!strncmp(sel_getName(sel), "init", 4) && invoke.target != retrunValue)
又是另外一种情况,当通过alloc和init创建类簇对象的时候,比如[[NSMutableArray alloc] init]
,alloc和init调用完后返回的不是同一个对象,alloc返回的是一个__NSPlaceholderArray
占位对象,调用init时,这个对象会被销毁再新建一个对应的对象返回。所以判断条件为sel为init打头时,并且self不等于返回值也release一下。
iOS的内存管理说简单也简单,说复杂还是很复杂的,比如在这里JS,OC,汇编,手动的MRC,编译器的ARC,Block拷贝的内存管理(retain和release捕获变量,block自身的内存管理),这些问题糅合在一起,就会让内存管理复杂很多,需要一点一点梳理才能解决,如果对内存管理理解不深刻的话,很容易出现各种问题,而且不知道问题出在什么地方,怎么解决,有兴趣的话可以去看我之前的ARC和block的相关博客,对很多问题都有详细描述和分析。
NULL处理
JSC头文件有说明,js的null和OC的[NSNull null]是可以等价转换的,所以遇到nil可以转成[NSNull null]返给JS。众所周知OC中对nil调用方法是合法的,因为调用都会被转成objc_msgSend的形式,但是js的对null调用方法就会报错。所以对于调用链JOC.class('ZWConfigure').sharedInstance().loginUser().userID()
,如果中间的sharedInstance或者loginUser返回了null,js就会报错,但如果每次都需要判断是否为null再调用会有些麻烦。
一种解决方案是返回一个空的JOObj(单独创建一个单例也行),遇到nil要传给js时就使用这个空的JOObj,但这样的话js就不能只检查null还得检查是否为空的JOObj,但这需要单独的语法支持,比如if(JOC.class('ZWConfigure').sharedInstance().loginUser().userID() != JOObjNil)
或者if(JOC.class('ZWConfigure').sharedInstance().loginUser().userID().isNil())
。
但是如果是浮点判断怎么处理,比如if(JOC.class('ZWConfigure').sharedInstance().loginUser().userAccountMoney() > 0)
。
目前我还没有比较好的办法来解决这个问题,所以暂时我还是使用[NSNull null]来作为nil返回值,JS就麻烦点,调用前先判断是否为null再调用。
最后
虽然实现了功能,也作了一些优化,但依旧不少可以改进和优化的地方,大概列了几个:
1、NSMethodSignature和NSInvocation的复用。
2、传递给JS封装对象的复用,特别是后两者。JS代码只是作为一个桥, 实现功能全靠JS调用的Native,因此需要频繁调用Native方法,每次调用会创建好几个临时对象,而缓存复用就成了提高性能的最有效的方式。
3、Block不使用拷贝的方式处理签名,目前js传递一个function给原生调用时,会被参数解析函数封装成一个特殊的block,在block中再调用该function,但是给block解析参数需要签名。目前的做法是写一个手动拷贝block的函数将block拷贝一份,同时强制替换其签名,这样做的好处是该特殊block可以像普通的block一样调用,但实际上可以在block中捕获签名作为成员变量,这样可以避免拷贝block,但是调用的时候需要和普通的block区别对待。
目来还有不少语法功能未支持,目前以下这些语法都在酝酿中(其实基本已经实现,只是还需要调试),当然还有其他一些细节功能
1、调用父类方法,比如重写init方法,必然要调用父类init。
2、再比如最开始说的变长参数中的匿名参数,最常用的就是stringWithFormat。
3、不通过setter和getter,获取和修改对象的成员变量,比如懒加载重写getter的时候。
4、再比如Masonry的链式语法的支持(我自己也有一个UI创建的链式语法库,主要是为了支持它,😂😂😂)。
最后给一个ARM64程序调用标准的链接:Procedure Call Standard for the ARM 64-bit Architecture
网友评论