JOBridge之二JS注册类和访问所有Native方法(可用代替JSPatch)

简述

在上篇文章: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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,026评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,655评论 1 296
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,726评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,204评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,558评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,731评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,944评论 2 314
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,698评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,438评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,633评论 2 247
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,125评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,444评论 3 255
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,137评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,103评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,888评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,772评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,669评论 2 271

推荐阅读更多精彩内容

  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,037评论 1 32
  • 1.设计模式是什么? 你知道哪些设计模式,并简要叙述?设计模式是一种编码经验,就是用比较成熟的逻辑去处理某一种类型...
    龍飝阅读 2,058评论 0 12
  • 1.项目经验 2.基础问题 3.指南认识 4.解决思路 ios开发三大块: 1.Oc基础 2.CocoaTouch...
    阳光的大男孩儿阅读 4,909评论 0 13
  • 1.OC里用到集合类是什么? 基本类型为:NSArray,NSSet以及NSDictionary 可变类型为:NS...
    轻皱眉头浅忧思阅读 1,295评论 0 3
  • 身在千山顶上头,突岩深缝妙香稠。 非无脚下浮云闹,来不相知去不留。 墨蘭幽香浓,惹蝶寻芳踪。...
    周墨_de19阅读 296评论 0 0