美文网首页
JOBridge之二JS注册类和访问所有Native方法

JOBridge之二JS注册类和访问所有Native方法

作者: 吸血鬼de晚餐 | 来源:发表于2019-01-02 18:05 被阅读14次

    简述

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

    相关文章

      网友评论

          本文标题:JOBridge之二JS注册类和访问所有Native方法

          本文链接:https://www.haomeiwen.com/subject/lgbzlqtx.html