JOBridge之三C函数OC化和语法增加以及优化(可用代替JSPatch)

简述

前两篇博客已经将JOBridge 基本实现原理和实现方案以及大部分代码都写好了 ,它向JS开放了绝大部分的OC方法,JS可以很容易替换方法,新增方法,构建回调方法,让JS和OC相互协作运行实现功能。不过iOS编程可不仅仅只调用OC方法,调用C方法也是经常的事情,比如经常用的GCD就是C接口。那么对于这部分C接口怎么处理呢?也许只能通过预先挖坑的方式来解决(至于地址预测?iOS有ASLR技术,函数入口地址每次加载都不一样,系统函数也一样。虽然iOS C函数也有中转符号表,facebook出的fishhook就是针对它做手脚来替换C函数的,这种平时玩玩还可以,但是极有可能被App Store审核给毙掉,还是还是老老实实挖坑吧)。坑怎么埋呢?最简单的方法就是通过向JSContext注册关键字+回调Block,然后转换参数,调用,再返回值,这么一个一个的写。要是这么挖坑肯定得把人累死,这肯定不是我的性格,那么有没有简单一点的办法呢?确实有,这个方案我把它叫做C函数OC化。

C函数OC化

之前聊过OC方法Method=SEL+type+IMP,IMP就是个C函数指针。所以可以简单认为OC方法就是给C函数配置个ID,也就是SEL,通过SEL可以找到实现IMP,type就是调用IMP需要的参数类型。唯一不一样的是OC函数第一二的参数类型是固定的@:,所以只要给C函数类似配置信息就可以让其变成OC函数。比如:CGRectMake,存储一个“CGRectMake”->IMP的映射即可,但是如果想要调用能处理参数就要同时给出签名"{CGRect={CGPoint=dd}{CGSize=dd}}dddd"。至于映射具体存储在哪里?我这里是通过class_addMethod给类添加了一个新的Method,和默认的IMP,同时给出签名,真正的IMP存储到字典,通过SEL查找到默认IMP,再在默认的IMP函数中搜索调用真正的IMP,这样做的好处是可以直接复用JS->OC通用调用桥(通用调用桥见之前的"JOBridge之二JS注册类和访问所有Native方法"的第二部分),如果不想用class_addMethod,可以直接存字典,只不过通用调用桥就的兼容处理,改动会大一些。

转变成OC方法后,就可以很方便的动态调用。我们利用OC Runtime机制,给对象发送消息,就可以调用对应的C函数,而使用NSInvocation动态调用就是最容易实现的方法,但这里有一个问题:NSInvocation调用第一二个参数固定是NSObject和SEL类型,用于查找方法。所以我们在签名的时候就不得不使用@:来占位,这样一来签名就会发生变化:CGRectMake签名变为"{CGRect={CGPoint=dd}{CGSize=dd}}@:dddd",但这破坏了原始参数个数,当参数大于6个时候存储位置也会发生改变。C函数的第9个参数才会存储到栈上,但被@:占用后,第7的参数就需要存储在栈了,所以目前这种方案只适用于6个以下的整型参数情况。至于6个以上的整型参数的处理我们后面再说。

有了上面的思路,实现起来倒并不困难。

我们新建一个OC类型JOCFunction,将需要映射的C方法添加到该类的类方法上,我这里以GCD的部分方法为例子。

static NSMutableDictionary *_JOCFunctions;

void *JOGetCFunction(SEL cmd) {
    JOPointerObj *p = [_JOCFunctions objectForKey:NSStringFromSelector(cmd)];
    return p.ptr;
}


@implementation JOCFunction
+ (void)load {
    [JOBridge registerPlugin:[self class]];
}

+ (void)addPlugin {
    
    _JOCFunctions = [NSMutableDictionary dictionary];

    //映射C函数到本类
#define JOMapCFunction(name, sign) \
({  [self mapCFunction:@#name type:sign imp:name];\
    NSLog(@"%@  %@", @#name, sign);})
    
    JOMapCFunction(dispatch_async, @"v@:@@");
    JOMapCFunction(dispatch_get_main_queue, @"@@@");
    JOMapCFunction(dispatch_time, @"Q@:Qq");
    JOMapCFunction(dispatch_after, @"v@:Q@@");
    JOMapCFunction(dispatch_sync, @"v@:@@");
    JOMapCFunction(dispatch_get_global_queue, @"v@:ii");
    
    [JOBridge addObject:JOMakeObj([JOCFunction class]) forKey:@"JC" needTransform:YES];
}

/*
    本函数做的事情是,将C方法实现映射到一个OC的类method,并给以对应签名,这样就可以通过调用OC的method去调用C函数。这样做的工作量就转变成
    对现有的C方法进行签名,并关联C函数,这个既可以手工完成,也可以通过脚本甚至bffi来完成,可以大幅减少工作量。
    这里我将方法添加到OC类中,主要是为了复用JOBridge中NSInvocation可以很好的将JS的传参解析并传递过来。当然我们也可以在JOBridge中JS对
    native的调用特殊处理,直接到_cFunctions寻找函数入口地址,不过这样的话就必须得手动传参,写入寄存器,实现起来比较麻烦,所以目前先用简单
    做法。
 */

+ (void)mapCFunction:(NSString *)name type:(NSString *)type imp:(void *)imp {
    if (name.length <= 0 || !imp) return;
    
    NSRange range = [type rangeOfString:@"@:"];
    if (range.length <= 0) return;
    
    //有参数默认实现为JOGlobalCSwizzle,没有参数就不用解析参数,直接调用即可
    if ([type substringFromIndex:range.location + range.length].length > 0) {
        [_JOCFunctions setValue:JOMakePointerObj(imp) forKey:name];
        class_addMethod(object_getClass([self class]), NSSelectorFromString(name), (IMP)JOGlobalCSwizzle, [type UTF8String]);
    } else {
        class_addMethod(object_getClass([self class]), NSSelectorFromString(name), (IMP)imp, [type UTF8String]);
    }
}
@end

JOBridge初始化后会调用addPlugin开始注册函数,创建字典用于存储函数名字(SEL)->C函数入口地址的映射,JOGetCFunction则通过SEL来查找对应的C函数入口地址。

JOMapCFunction宏调用mapCFunction:type:imp来注册OC方法。

这里分成两种情况,如果没有参数,那就简单了,直接向Class添加一个方法,IMP=C函数,不需要做多余的处理;如果有参数会麻烦一些,其IMP需要被指定为统一的JOGlobalCSwizzle,和之前谈到的JOGlobalSwizzle类似,其用于前期处理参数和调用真正的C函数,具体实现如下:

OS_ALWAYS_INLINE void JOGlobalCSwizzle(void) {
    asm volatile("stp    x29, x30, [sp, #-0x10]!");
    
    asm volatile("mov    x29, sp\n\
                 sub    sp, sp, #0xb0");

    asm volatile("str    d7, [sp, #0x88]\n\
                 str    d6, [sp, #0x80]\n\
                 str    d5, [sp, #0x78]\n\
                 str    d4, [sp, #0x70]\n\
                 str    d3, [sp, #0x68]\n\
                 str    d2, [sp, #0x60]\n\
                 str    d1, [sp, #0x58]\n\
                 str    d0, [sp, #0x50]\n\
                 str    x8, [sp, #0x40]\n\
                 str    x7, [sp, #0x38]\n\
                 str    x6, [sp, #0x30]\n\
                 str    x5, [sp, #0x28]\n\
                 str    x4, [sp, #0x20]\n\
                 str    x3, [sp, #0x18]\n\
                 str    x2, [sp, #0x10]\n\
                 str    x1, [sp, #0x8]\n\
                 str    x0, [sp]\n\
                 ");

    asm volatile("mov    x0, x1");
    asm volatile("bl    _JOGetCFunction");
    asm volatile("mov    x17, x0");

    //这里最多处理6个整型参数,多如果要处理更多的参数,就需要解析参数,比较麻烦
    asm volatile("ldr    d7, [sp, #0x88]\n\
                 ldr    d6, [sp, #0x80]\n\
                 ldr    d5, [sp, #0x78]\n\
                 ldr    d4, [sp, #0x70]\n\
                 ldr    d3, [sp, #0x68]\n\
                 ldr    d2, [sp, #0x60]\n\
                 ldr    d1, [sp, #0x58]\n\
                 ldr    d0, [sp, #0x50]\n\
//                 ldr    x8, [sp, #0xb8]\n\
//                 ldr    x7, [sp, #0xb0]\n\
                 ldr    x6, [sp, #0x40]\n\
                 ldr    x5, [sp, #0x38]\n\
                 ldr    x4, [sp, #0x30]\n\
                 ldr    x3, [sp, #0x28]\n\
                 ldr    x2, [sp, #0x20]\n\
                 ldr    x1, [sp, #0x18]\n\
                 ldr    x0, [sp, #0x10]\n\
                 ");

    asm volatile("blr    x17");

    asm volatile("mov    sp, x29");
    asm volatile("ldp    x29, x30, [sp], #0x10");
}

asm volatile("mov x0, x1"); asm volatile("bl _JOGetCFunction");这两句,将x1也就是SEL加载到x0,调用JOGetCFunction查找到C函数的入口地址,保存到x17。接下来就是将之前存储的参数加载到寄存器上,舍弃第一二个参数,从sp+0x10开始加载,跳转x17执行调用。

方案还是很简单吧,6个整型参数+8个浮点参数绝大部分情况下也够用了。如果要支持更完整的参数,有两种方法:

1、之前说到过动态调用是由NSInvocation来完成的(NSINvocation->JOGlobalCSwizzle),其默认第一二个参数为NSObject和SEL,之所以选择NSInvocation是因为其可以帮我们处理和存储参数,如果我们选择手动调用的话就需要自己根据签名来处理和存储参数,自然也就不用@:这俩实际调用发生时没啥用(前面还是有用的)的参数了。

2、JOGlobalCSwizzle会做参数调整,但调整不完善,在这里其实是有所有的参数的。如果想要完善参数调整,也就不可避免的需要根据签名重新处理和存储参数。

所以不论怎么玩都得处理参数,而这个确实比较麻烦,我暂时没实现。

宏生成签名

JOMapCFunction(dispatch_async, @"v@:@@");中需要给出签名字符串,这其实也挺麻烦的,我花了大半天时间才想出了以下优化的宏来简化这个过程。

#define ZW_PARAMS_NUM(...) ZW_PARAMS_COUNTER(-1, ##__VA_ARGS__,19,18,17,16,15,14,13,12,11,10,9,8,7,6,5,4,3,2,1,0)
#define ZW_PARAMS_COUNTER(P1,P2,P3,P4,P5,P6,P7,P8,P9,P10,P11,P12,p13,p14,p15,p16,p17,p18,p19,p20,Pn,...) Pn

#define ZW_PARAMS_INDEX_8(p1, p2, p3, p4, p5, p6, p7, p8, ...) p8
#define ZW_PARAMS_INDEX_7(p1, p2, p3, p4, p5, p6, p7, ...) p7
#define ZW_PARAMS_INDEX_6(p1, p2, p3, p4, p5, p6, ...) p6
#define ZW_PARAMS_INDEX_5(p1, p2, p3, p4, p5, ...) p5
#define ZW_PARAMS_INDEX_4(p1, p2, p3, p4, ...) p4
#define ZW_PARAMS_INDEX_3(p1, p2, p3, ...) p3
#define ZW_PARAMS_INDEX_2(p1, p2, ...) p2
#define ZW_PARAMS_INDEX_1(p1, ...) p1


#define ZWSignCat(p1, p2) [p2 stringByAppendingString:p1]
#define ZWSignParams(...)\
({  NSString *sign = @"";\
    switch(ZW_PARAMS_NUM(__VA_ARGS__)) {\
        case 8 : sign = ZWSignCat(sign, JOSignCFunction(ZW_PARAMS_INDEX_8(__VA_ARGS__,void,void,void,void,void,void,void,void)));\
        case 7 : sign = ZWSignCat(sign, JOSignCFunction(ZW_PARAMS_INDEX_7(__VA_ARGS__,void,void,void,void,void,void,void)));\
        case 6 : sign = ZWSignCat(sign, JOSignCFunction(ZW_PARAMS_INDEX_6(__VA_ARGS__,void,void,void,void,void,void)));\
        case 5 : sign = ZWSignCat(sign, JOSignCFunction(ZW_PARAMS_INDEX_5(__VA_ARGS__,void,void,void,void,void)));\
        case 4 : sign = ZWSignCat(sign, JOSignCFunction(ZW_PARAMS_INDEX_4(__VA_ARGS__,void,void,void,void)));\
        case 3 : sign = ZWSignCat(sign, JOSignCFunction(ZW_PARAMS_INDEX_3(__VA_ARGS__,void,void,void)));\
        case 2 : sign = ZWSignCat(sign, JOSignCFunction(ZW_PARAMS_INDEX_2(__VA_ARGS__,void,void)));\
        case 1 : sign = ZWSignCat(sign, JOSignCFunction(ZW_PARAMS_INDEX_1(__VA_ARGS__,void)));\
    }\
    sign;\
})

#define JOSignCFunction(type)\
    [NSString stringWithUTF8String:@encode(type)]
//将参数串生成签名字符串,至少包含一个参数
#define ZWSigns(ret, ...)\
    [ZWSignReturn(ret) stringByAppendingString:ZWSignParams(__VA_ARGS__)]
//将返回值生成签名字符串,如果函数没有参数则只能调用本宏生成签名
#define ZWSignReturn(ret) [JOSignCFunction(ret) stringByAppendingString:@"@:"]

其分为两种一种是没有参数的,使用ZWSignReturn;另一种有参数的使用ZWSigns(我是想了大半天才想到办法的,至于俩宏合并则是各种招都用了,无奈放弃了,有此道高手麻烦指点迷津,感谢)。

ZWSignReturn没啥说的,ZWSigns麻烦点,其复用宏ZWSignParams,先使用ZW_PARAMS_NUM计算参数个数的宏,使用switch的特点,不遇到return或者break则直接向下继续执行。这里最重要的是使用ZW_PARAMS_INDEX_4(__VA_ARGS__,void,void,void,void)等,后面需要使用void或者其他类型填充占位。为什么呢?因为宏是符号替换操作,必须要有个东西占位置,不然会变成@encode(),这是非法操作,我们预编译一下就好理解了,以下是CGRectMake宏展开的形式:

JOMapCFunction(CGRectMake, ZWSigns(CGRect, CGFloat, CGFloat, CGFloat, CGFloat));

展开

    [self mapCFunction:@"CGRectMake" type:[[[NSString stringWithUTF8String:@encode(CGRect)] stringByAppendingString:@"@:"] stringByAppendingString:({
        NSString *sign = @"";
        switch(4) {
            case 8 : sign = [[NSString stringWithUTF8String:@encode(void)] stringByAppendingString:sign];
            case 7 : sign = [[NSString stringWithUTF8String:@encode(void)] stringByAppendingString:sign];
            case 6 : sign = [[NSString stringWithUTF8String:@encode(void)] stringByAppendingString:sign];
            case 5 : sign = [[NSString stringWithUTF8String:@encode(void)] stringByAppendingString:sign];
            case 4 : sign = [[NSString stringWithUTF8String:@encode(CGFloat)] stringByAppendingString:sign];
            case 3 : sign = [[NSString stringWithUTF8String:@encode(CGFloat)] stringByAppendingString:sign];
            case 2 : sign = [[NSString stringWithUTF8String:@encode(CGFloat)] stringByAppendingString:sign];
            case 1 : sign = [[NSString stringWithUTF8String:@encode(CGFloat)] stringByAppendingString:sign];
                
        } 
        sign;
    })] imp:CGRectMake];

可以看到1-4参数类型都是正确,后面的5-8个参数全部填充void,其并不会被执行,只是占位而已。

接下来说点小技巧{}和()。这俩货我们单独使用的时候很多,{}可以告诉编译器这是个包含多行的代码块(并且有自己的作用域),比如if(true){},而()是改变优先级的。而({})组合在一起就更有趣了,它可以形成一个优先执行的代码块,而且还可以带一个返回值!有点类似于函数。我常用它来定义伪函数和预处理参数,这里ZWSignParams就是这种玩法。

这里使用了大量字符串的操作,所以如果需要短时间注册大量C函数的话,可以先通过这种方式把签名字符串输出来,然后通过签名字符串直接注册就可以了。至于量很多的话,可以尝试使用脚本去头文件读取函数声明把参数类型等生成ZWSigns表达式。

给出一些例子

JOMapCFunction(dispatch_async, ZWSigns(void, dispatch_queue_t, dispatch_block_t));
JOMapCFunction(dispatch_get_main_queue, ZWSignReturn(dispatch_queue_main_t));
JOMapCFunction(dispatch_time, ZWSigns(dispatch_time_t, dispatch_time_t, int64_t));
JOMapCFunction(dispatch_after, ZWSigns(void, dispatch_time_t, dispatch_queue_t, dispatch_block_t));
JOMapCFunction(dispatch_sync, ZWSigns(void, dispatch_queue_t, dispatch_block_t));
JOMapCFunction(dispatch_get_global_queue, ZWSigns(dispatch_queue_global_t, long, unsigned long));
JOMapCFunction(dispatch_source_create, ZWSigns(dispatch_source_t, dispatch_source_type_t, uintptr_t, unsigned long, dispatch_queue_t));
JOMapCFunction(dispatch_source_set_timer, ZWSigns(void, dispatch_source_t, dispatch_time_t, uint64_t, uint64_t));
JOMapCFunction(dispatch_source_set_event_handler, ZWSigns(void, dispatch_source_t, dispatch_block_t));
JOMapCFunction(dispatch_source_cancel, ZWSigns(void, dispatch_object_t));
JOMapCFunction(dispatch_resume, ZWSigns(void, dispatch_object_t));
JOMapCFunction(dispatch_walltime, ZWSigns(dispatch_time_t, const struct timespec *, int64_t));

语法增强

1、父类方法的调用

定义子类时,重写父类initXXX方法还是一个比较常见的需求,但重写initXXX不可避免的需要调用父类initXXX,所以该语法还是有一定的需求的,为此我想了若干办法来解决这个问题。

我实现过的有三种方案,一种是在NSInvocation调用invoke之前将obj的isa指针改成其父类,调用完成后再改回来,然后前后加一下锁,这是可以实现功能的,但这样其实有安全隐患的,如果在invoke期间,原生也调用该obj的方法就很可能凉凉了,这个锁个管不住来自于原生的调用。

在此基础上我给出了第二种办法,在invoke调用之前将父类的IMP(比如父类的init IMP)替换给当前调用类,调用完再还原,这样的危险性其实就大大降低了,只会在同时调用init的时候出现问题。这俩方案实现都比较简单。

第三种方案则是手动处理参数和调用,这也是我最终使用的方案,和下面的变长参数一起说明。

2、变长参数中的匿名参数

对于stringWithFormat:这种带匿名参数的,其有名参数会存在寄存器上,但匿名参数会存储在栈上,动态调用时,肯定是不知道哪些是匿名参数,并且没有匿名参数的签名,这肯定不能正确传递参数,所以需要JS在调用的时候手动进行签名。

给出包含调用父类方法和匿名参数方法的实现代码如下:

//该函数可能有返回值
void JOManualParamsResolverAndCall(__unsafe_unretained id obj, SEL sel, char *paramsSign , void *params[], int option) {
    BOOL regularCall = option == 0 || option == 1;
    BOOL superCall = option == 1;

    __autoreleasing Class class = object_getClass(obj);
    if (superCall) {
        class = [obj superclass];//不要使用class_getSuperclass,其可能会返回KVO的Class
    }
    void *imp = class_getMethodImplementation(class, sel);
    __autoreleasing NSMethodSignature *orignSign = [obj methodSignatureForSelector:sel];
    
    NSUInteger orignCount = [orignSign numberOfArguments];
    NSUInteger count = 0;
    if (regularCall) {
        count = orignCount;
        NSInteger frameLength = orignSign.frameLength;
        if (frameLength > 0xe0) {
            if ((frameLength - 0xe0)%8) {
                orignCount = count - (frameLength - 0xe0)/8 - 1;
            } else {
                orignCount = count - (frameLength - 0xe0)/8;
            }
        }
        memcpy(paramsSign, [orignSign methodReturnType], 64);
    } else {
        orignCount = orignCount > 8 ? 8 : orignCount;
        count = strlen(paramsSign) - 1;
    }
    
    void *paramArray[12] = {0};
    void *paramFloatArray[8] = {0};
    paramArray[0] = (__bridge void *)obj;
    paramArray[1] = sel;
    
    for (int i = 2, j = 0; i < count; ++i) {
        const char *types = regularCall ? [orignSign getArgumentTypeAtIndex:i] : NULL;
        char type = regularCall ? types[0] : paramsSign[i + 1];
        __unsafe_unretained id param = (__bridge id)params[regularCall ? i-2 : i-1];
        switch(type) {
            case 'B': {BOOL v = [param toBool];
                paramArray[i] = (void *)(NSUInteger)v; break;}
            case 'c': {char v = [[param toNumber] charValue];
                paramArray[i] = (void *)(NSUInteger)v; break;}
            case 'C': {Byte v = [[param toNumber] unsignedCharValue];
                paramArray[i] = (void *)(NSUInteger)v; break;}
            case 's': {short v = [[param toNumber] shortValue];
                paramArray[i] = (void *)(NSUInteger)v; break;}
            case 'S': {unsigned short v = [[param toNumber] unsignedShortValue];
                paramArray[i] = (void *)(NSUInteger)v; break;}
            case 'i': {int v = [param toInt32];
                paramArray[i] = (void *)(NSUInteger)v; break;}
            case 'I': {unsigned int v = [param toUInt32];
                paramArray[i] = (void *)(NSUInteger)v; break;}
            case 'l': {long v = [[param toNumber] longValue];
                paramArray[i] = (void *)(NSUInteger)v; break;}
            case 'L': {unsigned long v = [[param toNumber] unsignedLongValue];
                paramArray[i] = (void *)(NSUInteger)v; break;}
            case 'q': {long long v = [[param toNumber] longLongValue];
                paramArray[i] = (void *)(NSUInteger)v; break;}
            case 'Q': {unsigned long long v = [[param toNumber] unsignedLongLongValue];
                paramArray[i] = (void *)(NSUInteger)v; break;}
            case 'f': {
                float v = [[param toNumber] floatValue];
                if (regularCall) {
                    float *p = (float *)(paramFloatArray + j++); *p = v;
                } else {
                    float *p = (float *)(paramArray + i); *p = v;
                }
                break;
            }
            case 'd': {
                double v = [param toDouble];
                if (regularCall) {
                    double *p = (double *)(paramFloatArray + j++); *p = v;
                } else {
                    double *p = (double *)(paramArray + i); *p = v;
                }
                break;
            }
            case '#':
            case '@': {
                __autoreleasing id v = [param toObject];
                
                if (OS_EXPECT([v isKindOfClass:JOObj.class], 1)) {
                    v = [v obj];
                    paramArray[i] = (__bridge void *)v; break;
                } else if ([v isKindOfClass:JOSelObj.class]) {
                    SEL s = [v sel];
                    paramArray[i] = (void *)s;; break;
                } else if ([v isKindOfClass:JOPointerObj.class]) {
                    void *p = [v ptr];
                    paramArray[i] = p; break;
                } else if ([v isKindOfClass:JOWeakObj.class]) {
                    v = [v obj];
                    paramArray[i] = (__bridge void *)v; break;
                } else if ([v isKindOfClass:[NSNull class]]) {
                    v = nil;
                    paramArray[i] = NULL; ;
                } else {
                    paramArray[i] = (__bridge void *)v;
                }
                break;
            }
            case '{': {
                //MARK:解析结构体,结构体手动解析太麻烦,暂不支持
                if (OS_EXPECT(!strcmp(types, @encode(CGRect)), 1)) {
                    CGRect v = [param toRect];
                    double *p = (double *)(paramFloatArray + j++); *p = v.origin.x;
                    p = (double *)(paramFloatArray + j++); *p = v.origin.y;
                    p = (double *)(paramFloatArray + j++); *p = v.size.width;
                    p = (double *)(paramFloatArray + j++); *p = v.size.height;

                } else if (!strcmp(types, @encode(CGPoint))) {
                    CGPoint v = [param toPoint];
                    double *p = (double *)(paramFloatArray + j++); *p = v.x;
                    p = (double *)(paramFloatArray + j++); *p = v.y;

                } else if (!strcmp(types, @encode(CGSize))) {
                    CGSize v = [param toSize];
                    double *p = (double *)(paramFloatArray + j++); *p = v.width;
                    p = (double *)(paramFloatArray + j++); *p = v.height;
                } else if (!strcmp(types, @encode(NSRange))) {
                    NSRange v = [param toRange];
                    paramArray[i++] = (void *)v.location;
                    paramArray[i] = (void *)v.length;
                }
                break;
            }
            case '^':
            case '*': {
                __autoreleasing id v = [param toObject];
                void *p = NULL;
                if ([v isKindOfClass:[JOPointerObj class]]) {
                    p = [v ptr];
                }
                paramArray[i] = p;
                break;
            }
            case ':': {
                __autoreleasing id v = [param toObject];
                void *p = NULL;
                if ([v isKindOfClass:[JOSelObj class]]) {
                    p = [v sel];
                }
                paramArray[i] = p;
                break;
            }
        }
    }
    
    /* 这里的解决办法是通过建立伪栈来解决栈参数传递 */
    NSUInteger stackCount = count - orignCount;
    //16Byte对齐,原因是很多被调用函数都会使用stp来压栈x29,x30,如果sp不是16Byte对齐,就会凉凉
    NSUInteger stackOffset = (stackCount % 2 ? stackCount + 1 : stackCount);
    void *stackPtr = paramArray + orignCount;
    void **paramArrayPtr = (void **)&paramArray;
    void **paramFloatArrayPtr = (void **)&paramFloatArray;
    asm volatile("ldr    x17, %0" : "=m"(imp));
    //初始化
    asm volatile("ldr    x10, %0" : "=m"(stackCount));
    asm volatile("ldr    x11, %0" : "=m"(stackOffset));
    asm volatile("lsl    x11, x11, #0x3");//x11 * 8
    asm volatile("ldr    x12, %0" : "=m"(stackPtr));
    //循环拷贝参数到伪栈上
    asm volatile("sub    x15, sp, x11");
    asm volatile("LZW_20181202:");
    asm volatile("cbz    x10, LZW_20181203");
    asm volatile("ldr    x0, [x12]");
    asm volatile("str    x0, [x15]");
    asm volatile("add    x15, x15, #0x8");
    asm volatile("add    x12, x12, #0x8");
    asm volatile("sub    x10, x10, #0x1");
    asm volatile("cbnz   x10, LZW_20181202");
    asm volatile("LZW_20181203:");
    //加载寄存器参数,这里懒得计算寄存器参数个数,直接9+8个寄存器都加载
    asm volatile("ldr    x12, %0" : "=m"(paramArrayPtr));
    asm volatile("ldr    x13, %0" : "=m"(paramFloatArrayPtr));
    asm volatile("ldr    x0, [x12]");
    asm volatile("ldr    x1, [x12, 0x8]");
    asm volatile("ldr    x2, [x12, 0x10]");
    asm volatile("ldr    x3, [x12, 0x18]");
    asm volatile("ldr    x4, [x12, 0x20]");
    asm volatile("ldr    x5, [x12, 0x28]");
    asm volatile("ldr    x6, [x12, 0x30]");
    asm volatile("ldr    x7, [x12, 0x38]");
    asm volatile("ldr    x8, [x12, 0x40]");
    asm volatile("ldr    d0, [x13]");
    asm volatile("ldr    d1, [x13, 0x8]");
    asm volatile("ldr    d2, [x13, 0x10]");
    asm volatile("ldr    d3, [x13, 0x18]");
    asm volatile("ldr    d4, [x13, 0x20]");
    asm volatile("ldr    d5, [x13, 0x28]");
    asm volatile("ldr    d6, [x13, 0x30]");
    asm volatile("ldr    d7, [x13, 0x38]");
    

    asm volatile("sub    sp, sp, x11");
    asm volatile("blr    x17");
    //如果增删了局部变量,0x3c0可能会变,这个大小可以到函数汇编代码入口中找,其等于栈增长大小+0x10
    asm volatile("sub    sp, x29, #0x3f0");
}

option = 1调用父类方法,option=0常规调用,option=其他 匿名参数的方法调用。其中option=0暂时未使用。

最前面主要是计算栈上有多少参数,常规调用(调用父类方法也认为是这种情况,调用父类匿参函数不考虑)和匿参函数调用是不同的情况。

然后声明两C数组,分别存储整型参数和浮点参数,根据签名解析出参数存到数组中,然后通过建立伪栈来构建函数调用的参数环境,注释写的还是比较清楚的,直接看注释吧。

3、Masonry的链式语法的支持

如果发现需要返回一个block给JS,由JS来调用,比如make.centerX().equalTo()(self.backView())的equalTo,这时候需要再次封装block(blockA),因为原始block不能处理js参数,必须由blockA来预处理这些参数,其根据签名复用JOParamsResolver和JOGetReturnValue,同时通过NSInvocation来动态调用原始block。对于由js传过来的function封装成特殊的block直接返回原来的js function就行,参数处理都可以直接省略了。

id JOPackBlockValue(void (^returnBlock)()) {
//    JOTools.retain(returnValue);
    /*  JOParamsResolver中定义的特殊block的签名在捕获的参数中,原始签名无效,其他block则加载原始签名。根据其实现函数的入口地址判断是否为该特殊block,如果是特殊block也就不用再包一层block了,直接将特殊block捕获的js function返回给js调用。另:本函数只能作为给js提供block的时候使用。*/
    _JOBlock *blockPtr =  (__bridge _JOBlock *)returnBlock;
    if (_JOParamBlockIMP == blockPtr->BlockFunctionPtr) {
        return blockPtr->jsFunction;
    }
    
    return ^id (__unsafe_unretained JSValue *p0, __unsafe_unretained JSValue *p1, __unsafe_unretained JSValue *p2, __unsafe_unretained JSValue *p3, __unsafe_unretained JSValue *p4, __unsafe_unretained JSValue *p5, __unsafe_unretained JSValue *p6, __unsafe_unretained JSValue *p7) {
        __autoreleasing id obj = returnBlock;
        char *blockType = NULL;
        char **pType = &blockType;
        asm volatile("ldr    x0, %0" : "=m"(obj));
        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"(pType));
        asm volatile("ldr    x2, [x2]");
        asm volatile("str    x2, [x0]" );
        
        /*  取出block签名,手动解析起来比较麻烦,这里利用NSMethodSignature解析后,利用NSInvocationa来调用,可以复用JOParamsResolver,JOGetReturnValue俩函数 */
        NSMethodSignature *sign = [NSMethodSignature signatureWithObjCTypes:blockType];
        NSInvocation *invoke = [NSInvocation invocationWithMethodSignature:sign];
        void *params[] = {(__bridge void *)p0, (__bridge void *)p1, (__bridge void *)p2, (__bridge void *)p3,(__bridge void *)p4, (__bridge void *)p5, (__bridge void *)p6,(__bridge void *)p7};
        [invoke setTarget:obj];
        JOParamsResolver(params, sign, invoke, 1);
        
        [invoke invoke];
//        JOTools.release(obj);
        return JOGetReturnValue(sign, invoke, nil, NULL);
    };
}

4、获取对象的成员变量

目前是在Class定义的时候添加如下4个方法,复用JOBridge的JS->OC的通用调用桥就可以轻松解决问题

class_addMethod(self.class, NSSelectorFromString(@"JOGetIvar"),(IMP)JOGetIvar, "@@:@");
class_addMethod(self.class, NSSelectorFromString(@"JOSetIvar"),(IMP)JOSetIvar, "v@:@@");
class_addMethod(self.class, NSSelectorFromString(@"JOGetIvarI"),(IMP)JOGetIvarI, "q@:@");
class_addMethod(self.class, NSSelectorFromString(@"JOSetIvarI"),(IMP)JOSetIvarI, "v@:@q");

需要注意的是JOGetIvarI和JOSetIvarI调用的时候需要使用汇编去处理

//对于基础数据类型,如果直接object_setIvar,编译器会自动插入retain的代码,就会crash,所以需要用汇编来调用
void JOGetIvarI(__autoreleasing id obj, SEL sel, __autoreleasing NSString *name) {
    Ivar ivar = class_getInstanceVariable([obj class], [name UTF8String]);
    if (ivar) {
        asm volatile("ldr  x7, %0": "=m"(ivar));
        asm volatile("ldr  x0, %0": "=m"(obj));
        asm volatile("mov  x1, x7");
        asm volatile("bl  _object_getIvar");
    } else {
        asm volatile("mov  x0, #0x0");
        asm volatile("movi  d0, #0x0");
        asm volatile("movi  d1, #0x0");
        asm volatile("movi  d2, #0x0");
        asm volatile("movi  d3, #0x0");
    }
}
void JOSetIvarI(__autoreleasing id obj, SEL sel, __autoreleasing NSString *name, NSInteger newValue) {
    Ivar ivar = class_getInstanceVariable([obj class], [name UTF8String]);
    if (ivar) {
        asm volatile("ldr  x6, %0": "=m"(ivar));
        asm volatile("ldr  x7, %0": "=m"(newValue));
        asm volatile("ldr  x0, %0": "=m"(obj));
        asm volatile("mov  x1, x6");
        asm volatile("mov  x2, x7");
        asm volatile("bl  _object_setIvar");
    }
}

这个也不难理解,就是加载一下参数然后去调用Runtime方法处理,JOGetIvarI中没有找到Ivar的时候将寄存器置零。

5、参数位指针返回值

当函数有大于两个返回值时,就只能通过指针来返回,目前对于基础类型的做法是通过额外分配malloc 8Byte的堆空间来存储,然后通过JOPointerObj返回给js使用,之后再free。但对于OC对象则不太适用,因为是直接修改指针,OC下由autoreleasepool来管理,然后调用函数获取到返回值后会有强引用处理,但JS环境下在有强引用之前autoreleasepool早失效了,而主动retain又没有太好的时机,所以这种情况目前还没有处理。

另外还有一个比较棘手的问题,js端无法重写dealloc,之前尝试过添加一个JODealloc来给js重写,并在对象销毁的时候调用,但是在实际使用的时候发现会crash,分析下来是因为在js重写的JODealloc中会持有self对象,而在OC端调用玩dealloc对象就已经被销毁了,此时js引用的self对象已经无效,js在其生命周期结束时会再次调用release,然后crash了。

优化

1、NSMethodSignature和NSInvocation的复用

对于这个问题,并不困难,但有几个小技巧需要注意

首先是key的选择,我这里我选择的是对象对应class的地址和selector的地址,为啥选择这俩东西呢?因为他们是运行时常量,如果使用名字,这会导致不少的字符串创建和操作,会降低复用的意义。

OS_ALWAYS_INLINE NSString *ZWGetInvocationKey(id obj, SEL sel) {
    Class class = object_getClass(obj);
    return [NSString stringWithFormat:@"%p_%p", class, sel];
}

其次NSInvocation本身是和NSMethodSignature对应的,也有其引用,本来只存储NSInvocation就够了,我这里分开存储,主要是因为NSMethodSignature与签名(selector)对应,本来就不依赖于NSInvocation存在,而同一个方法的NSInvocation可能会并行执行,而缓存NSInvocation只有一个,因此当同时调用同一方法时,只有一个调用会复用NSInvocation,其他的调用仅仅复用NSMethodSignature,而NSInvocation则需要新创建。

最后前面也说过了,从缓存取出的NSInvocation在运行期间会出缓存中移除,等运行完了再写回去。

2、复用传递给JS的封装对象

为了保证将OC数据可以通过共享对象传给JS,同时还能回传给OC使用,需要使用JOPointerObj,JOWeakObj,JOSelObj,JOObj将对应的数据封装,但由于两者之间频繁的数据交互,需要不断的创建这些封装对象(1s可能会创建成千上万个),最多的就是JOObj,所以我对其做了复用的。

创建一个NSHashTable,作为正在使用的JOObj容器;创建NSMutableArray待复用容器存储闲置回收的JOObj。当需要创建一个JOObj对象时就去复用数组中找,看是否有闲置的,如果有就直接使用并移动到NSHashTable中;否则新建一个JOObj对象使用,并放入NSHashTable,同时强制retain一次。

创建一个timer,每个30s执行一次,遍历NSHashTable中对象,检查其引用计数器是否降到最低,如果是就释放JOObj对象同时将其转移到NSMutableArray复用数组,如果数组满了就直接调用release释放。

for (JOObj *obj in allObjects) {
    if (JOGetRetainCount(obj) == 2) {//allObjects和obj导致额外计数器增加了2
        obj.obj = nil;
        if (_JOObjectReuseArray.count < _JOObjectReuseMax) {
            [_JOObjectReuseArray addObject:obj];
        } else {
            JOTools.release(obj);
        }
        [_JOObjectInuseHashTable removeObject:obj];
    }
}

JOGetRetainCount只会获取isa中的计数器

//arm64
#define RC_Right_Shift   45
#define ISA_NonpointerBit   0x1
//这里只获取的开始和末尾bit,也就不用定义isa的解析结构体了
OS_ALWAYS_INLINE uintptr_t JOGetRetainCount(__unsafe_unretained id obj) {
    if (!obj) return NSUIntegerMax;
    void *isa_p = (__bridge void *)obj;
    uintptr_t isa = (uintptr_t)*((uintptr_t *)isa_p);
    if (OS_EXPECT((isa & ISA_NonpointerBit) == ISA_NonpointerBit, 1)) {
        return isa >> RC_Right_Shift;
    } else {
        return NSUIntegerMax;
    }
}

另外复用数组容量10240,这是因为JOObj对象其实只存储一个对象引用+isa只有16Byte,10240*16=160KB,也就10个内存页,这个量也不算多。其他JOPointerObj,JOWeakObj,JOSelObj因为使用量少可以不复用。

3、block参数签名优化

之前聊到过,为了保证定义同一种block可以当做若干种block使用(捕获参数一样,调用参数不一样),需要对block进行签名,而如果签名像普通block存储在对应的签名位置,就需要全拷贝整个block,之后就可以像普通block一样使用了,但是这比较麻烦,需要处理捕获对象的内存管理,block本身的内存管理,还要和ARC一起处理,容易出错。后来想了想,是我自己2比了,为什么非要让这种特殊的block和普通的block完全一样呢?必要的时候可以区别对待嘛,因为这种特殊block的函数入口地址是固定的,很容易就可以区分出来,而签名就在捕获参数列表中,直接用就行了,比起直接copy(又是malloc又是memcpy还得retain,release)的玩法简单多了。

4、其他优化

4.1 字符串构建selector,三次替换+多个string的创建 调用次数多了,还是有些开销的,这里可以写个for循环一次循环解决问题,最后测试了一下2000+调用可以减少一半的时间,当然其实也只有0.01秒(iPhone XS Max),只不过这些代码调用太频繁了,这种成本+效果的优化还是值得的。其他这种字符串处理也可以换成for循环

NSString *tmp = [[selName toString] stringByReplacingOccurrencesOfString:@"__" withString:@"-"];
tmp = [tmp stringByReplacingOccurrencesOfString:@"_" withString:@":"];
SEL sel = NSSelectorFromString([tmp stringByReplacingOccurrencesOfString:@"-" withString:@"_"]);

4.2 使用C数组。之前是这样,数组是C数组,但是还是会retain对象,有一定开销,毕竟每次调用都有13次retain和release操作。现在换成纯C数组和指针数据。__bridge不会带来额外的开销。

id params[] = {p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12};
//栈参的存储顺序和并非按照p0-p12顺序,所以不能复用栈来作为数组,需要单独创建一个数组。使用c数组,减少retain,release操作
void *params[] = {(__bridge void *)p0, (__bridge void *)p1, (__bridge void *)p2, (__bridge void *)p3, (__bridge void *)p4, (__bridge void *)p5, (__bridge void *)p6, (__bridge void *)p7, (__bridge void *)p8, (__bridge void *)p9, (__bridge void *)p10, (__bridge void *)p11, (__bridge void *)p12};//创建c数组,提高性能

4.3 retain和release实现优化,之前使用的是比较规矩的调用方式,就是注释的部分,后来想了想其实没必要使用“bl”跳转指令,直接使用强制跳转命令“b”就行了,编译器虽然会在后面加“ret”命令,但不会执行,因为_objc_retain中直接使用x30返回地址,本次调用都不会再走到retain和release这里,代价也就浪费了几个Byte的空间而已,绝对物超所值。

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");
    asm volatile("b _objc_retain");
}
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");
    asm volatile("b _objc_release");
}

其他优化也就不说了,当然后续还有很多优化的空间,暂时顾不上了。

安全

动态更新代码是很爽,但这种方式让原生API毫不设防的开放给JS,确实会对iOS的安全性造成比较大的影响,如果调用标准开放很容被有心人利用,即使是用常规手段加密脚本,破解加密也并非不可能,所以最好办法还是不要公开标准和源码,实际上已经公开大部分了,但要凑成完整的还是不那么容易,有这精力自己都可以另起炉灶搞一个了。如果要实用化,安全设计非常重要,这部分东西我将不作任何说明,也不会给实现方案,有需要者可以自行设计。

最后

目前这套东西已经被添加到了我们的App,并且已经毫无阻碍的通过了App Store的审核,我并没有调用任何私有API,就算有也就是用汇编调用了类似于_objc_retain,但这个应该没风险,毕竟Xcode随时随地都在插入这个代码,其他就是一下Runtime函数,敏感API更没有,之前我最大的担忧就是苹果对内联汇编的态度,目前来看还是很顺利的。苹果要堵住JSPatch不让Swizzle forwardInvocation:就行,咱这个就不那么好堵了。

到此JOBridge基本上就写完了,后续就是增强和优化,完整的代码是否开放我再想想,毕竟容易被封杀。

使用说明见“JOBridge之四使用方法”

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

推荐阅读更多精彩内容

  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,682评论 0 9
  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,089评论 1 32
  • 1.理解NSObject和元类 1.1 在OC中的对象和类是什么 对象是在objc.h中定义的 类是在runtim...
    HWenj阅读 917评论 0 3
  • 我觉得自己对所有事情都后知后觉。 朋友也说我后知后觉。 也有说我反应慢一拍的。 我自我安慰说,那也没事。 痛得慢一...
    水无名阅读 161评论 0 0
  • 随着秋雨的到来,窗外滴滴答答的声音,或是一阵凉风吹来,金黄的叶子从树上被淘气的雨点打了下来,秋天到了。 有的...
    遇见vs错过阅读 226评论 0 2