如何使用libffi库实现OC方法替换和调用 OC 函数

最近在学习JS语言,走大前端的路子,学了两天后,感觉要小试牛刀。突然想到JSPatch中JSPatch.js是JS写的,正好之前有一些细节还没完全了解,就决定重看了一遍,不看不知道,一看发现JSPatch里比我一年前看的升级了好多,并且被JPBlock的实现方式惊呆了~
结合之前逆向滴滴的DYC的实现方式,发现利用滴滴的JS脚本替换的函数,函数有不同的地址(如果用JSPatch的实现方式,被替换的函数应该有相同的函数地址~)。
然后就突发其想,如何动态的给OC函数绑定替换函数,而不利用消息转发的方式。
有了这么多的想法,就差实践了...
由于之前完全不了解libffi,遂在github上搜索相关的技术资料,但是发现能参考的文档比较少,不过还是发现了一个有100多stars的工程:
https://github.com/mikeash/MABlockClosure
原作者利用自定义的Block结构体模拟OC的block结构,进而获取到block的函数签名。
举个例子:

id block = ^(int x, int y) { return x + y; };
    closure = [[MABlockClosure alloc] initWithBlock: block];
    int ret = ((int (*)(int, int))[closure fptr])(5, 10);
    NSLog(@"%d", ret);

block的函数签名如下:
"i12@?0i4i8"
其中i12中的i代表返回值类型是int,12代码这个block参数的总长度是12字节(不确定,感觉是这个意思),@?0标识block的第一参数类型是Block类型(类似函调调用的self),后面的0代表了第一个参数从函数调用栈偏移0字节开始,i4代表了第二个参数是int类型,其中4代码表了参数的起始位置从从函数调用栈偏移4字节开始,i8代表了第三个参数是int类型,其中8参数的起始位置从从函数调用栈偏移8字节开始。
分析出OC数据类型信息后(并没有使用类型偏移信息~,只用到了类型信息),根据每种类型生成对应的ffi_type,解析过程如下:

- (ffi_type *)_ffiArgForEncode: (const char *)str
{
    #define SINT(type) do { \
        if(str[0] == @encode(type)[0]) \
        { \
           if(sizeof(type) == 1) \
               return &ffi_type_sint8; \
           else if(sizeof(type) == 2) \
               return &ffi_type_sint16; \
           else if(sizeof(type) == 4) \
               return &ffi_type_sint32; \
           else if(sizeof(type) == 8) \
               return &ffi_type_sint64; \
           else \
           { \
               NSLog(@"Unknown size for type %s", #type); \
               abort(); \
           } \
        } \
    } while(0)
    
    #define UINT(type) do { \
        if(str[0] == @encode(type)[0]) \
        { \
           if(sizeof(type) == 1) \
               return &ffi_type_uint8; \
           else if(sizeof(type) == 2) \
               return &ffi_type_uint16; \
           else if(sizeof(type) == 4) \
               return &ffi_type_uint32; \
           else if(sizeof(type) == 8) \
               return &ffi_type_uint64; \
           else \
           { \
               NSLog(@"Unknown size for type %s", #type); \
               abort(); \
           } \
        } \
    } while(0)
    
    #define INT(type) do { \
        SINT(type); \
        UINT(unsigned type); \
    } while(0)
    
    #define COND(type, name) do { \
        if(str[0] == @encode(type)[0]) \
            return &ffi_type_ ## name; \
    } while(0)
    
    #define PTR(type) COND(type, pointer)
    
    #define STRUCT(structType, ...) do { \
        if(strncmp(str, @encode(structType), strlen(@encode(structType))) == 0) \
        { \
           ffi_type *elementsLocal[] = { __VA_ARGS__, NULL }; \
           ffi_type **elements = [self _allocate: sizeof(elementsLocal)]; \
           memcpy(elements, elementsLocal, sizeof(elementsLocal)); \
            \
           ffi_type *structType = [self _allocate: sizeof(*structType)]; \
           structType->type = FFI_TYPE_STRUCT; \
           structType->elements = elements; \
           return structType; \
        } \
    } while(0)
    
    SINT(_Bool);
    SINT(signed char);
    UINT(unsigned char);
    INT(short);
    INT(int);
    INT(long);
    INT(long long);
    
    PTR(id);
    PTR(Class);
    PTR(SEL);
    PTR(void *);
    PTR(char *);
    PTR(void (*)(void));
    
    COND(float, float);
    COND(double, double);
    
    COND(void, void);
    
    ffi_type *CGFloatFFI = sizeof(CGFloat) == sizeof(float) ? &ffi_type_float : &ffi_type_double;
    STRUCT(CGRect, CGFloatFFI, CGFloatFFI, CGFloatFFI, CGFloatFFI);
    STRUCT(CGPoint, CGFloatFFI, CGFloatFFI);
    STRUCT(CGSize, CGFloatFFI, CGFloatFFI);
    
#if !TARGET_OS_IPHONE
    STRUCT(NSRect, CGFloatFFI, CGFloatFFI, CGFloatFFI, CGFloatFFI);
    STRUCT(NSPoint, CGFloatFFI, CGFloatFFI);
    STRUCT(NSSize, CGFloatFFI, CGFloatFFI);
#endif
    
    NSLog(@"Unknown encode string %s", str);
    abort();
}

准备就绪后

ffi_status status = ffi_prep_closure_loc(_closure, &_closureCIF, BlockClosure, self, _closureFptr);

根据类型信息(存储在_closureCIF中),生成_closureFptr函数指针,函数指针的回调是BlockClosure方法。
基于以上的思路,既然OC 的 Block可以通过函数签名实现,那么OC中普通的函数也具有函数签名,能否移植过去呢?带着这种想法,我们准备进行进一步的尝试。
首先创建测试类MyObject

@interface MyObject : NSObject
- (CGRect)stringParam:(NSString *)str rectParam:(CGRect)rect charParam:(char)charValue intParam:(int)intValue pointParam:(CGPoint)point voidParam:(void *)voidP  endParam:(NSString *)end;
@end

@implementation MyObject
- (CGRect)stringParam:(NSString *)str rectParam:(CGRect)rect charParam:(char)charValue intParam:(int)intValue pointParam:(CGPoint)point voidParam:(void *)voidP  endParam:(NSString *)end{
    return CGRectMake(12, 13, 14, 15);
}
@end

编写测试代码:

Method method = class_getInstanceMethod(MyObject.class, @selector(stringParam:rectParam:charParam:intParam:pointParam:voidParam:endParam:));
    NSString *typeDescription = @((char *)method_getTypeEncoding(method));
    MABlockClosure *closure = [[MABlockClosure alloc] initWithSignature: typeDescription];
    class_replaceMethod(MyObject.class, @selector(stringParam:rectParam:charParam:intParam:pointParam:voidParam:endParam:), [closure fptr], [typeDescription UTF8String]);
    MyObject *object = [MyObject new];
    void *poiner = malloc(10);
    CGRect returnRect = [object stringParam:@"test1" rectParam:CGRectMake(12, 33, 54, 76)  charParam:'c' intParam:1986 pointParam:CGPointMake(-20, 25) voidParam:poiner endParam:@"hello world"];
    NSLog(@"returnRect 的值是%@",NSStringFromRect(returnRect));

首先,在MABlockClosure扩展一个初始化方法,根据OC函数签名初始化:

- (id)initWithSignature: (NSString *)signature {
    _allocations = [[NSMutableArray alloc] init];
    _signature = signature;
    _closure = AllocateClosure(&_closureFptr);
    [self _prepClosureCIF];
    [self _prepClosure];
    return self;
}

将BlockSig升级成FunctionSig

static const char *FunctionSig(id blockObj)
{
    if ([blockObj isKindOfClass:NSString.class]) {
        NSString *sign = blockObj;
        return [sign UTF8String];
    }
    
    
    struct Block *block = (void *)blockObj;
    struct BlockDescriptor *descriptor = block->descriptor;
    
    int copyDisposeFlag = 1 << 25;
    int signatureFlag = 1 << 30;
    
    assert(block->flags & signatureFlag);
    
    int index = 0;
    if(block->flags & copyDisposeFlag)
        index += 2;
    
    return descriptor->rest[index];
}

FunctionSig的作用是如果传进来的是字符串函数签名,则直接使用,否则,解析block中的函数签名。
重写_prepClosureCIF函数

- (void)_prepClosureCIF
{
    _closureArgCount = [self _prepCIF: &_closureCIF withEncodeString: FunctionSig(_block?:_signature) skipArg: _block?YES:NO];
}

其中,skipArg是如果是block的类型,则忽略第一个参数,如果是OC函数,则不忽略第一个函数。(具体原因,需要进一步研究...)
改造完毕后,我们尝试在BlockClosure中获取到OC中所有的参数,并能设置返回值,就说明实现了目的。
我们看一下BlockClosure的实现

MABlockClosure *self = userdata;
    if (self->_signature.length > 0 ) {
        const char *str = [self->_signature UTF8String];
        int i = -1;
        while(str && *str)
        {
            const char *next = SizeAndAlignment(str, NULL, NULL, NULL);
            if(i >= 0)
                [self _ffiValueForEncode:str argumentPtr:args[i]];
            i++;
            str = next;
        }
        
        //!!!!!!!!这里先写死,后面根据返回值的描述进一步写返回值的类型
        *(CGRect *)ret = CGRectMake(321, 123, 10, 10);
    }
    if (self->_block) {
        int count = self->_closureArgCount;
        void **innerArgs = malloc((count + 1) * sizeof(*innerArgs));
        innerArgs[0] = &self->_block;
        memcpy(innerArgs + 1, args, count * sizeof(*args));
        ffi_call(&self->_innerCIF, BlockImpl(self->_block), ret, innerArgs);
        free(innerArgs);
    }

第一个if语句是我加上去的,也是本文分析的重点。大致思路是根据获取到函数签名里的参数类型后,结合函数里传递进来的参数数组,获取参数值。因此核心部分在于分析 [self _ffiValueForEncode:str argumentPtr:args[i]]这个函数的实现。这个函数,我具体的实现方式如下:

- (void *)_ffiValueForEncode: (const char *)str argumentPtr:(void**)argumentPtr
{
#define JP_BLOCK_PARAM_CASE(_typeString, _type, _selector) \
case _typeString: {                              \
_type returnValue = *(_type *)argumentPtr;                     \
param = [NSNumber _selector:returnValue];\
break; \
}
    
#define SINTV(_type,_selector) do { \
if(str[0] == @encode(_type)[0]) \
{ \
_type returnValue = *(_type *)argumentPtr; \
NSLog(@"参数值是%@",[NSNumber _selector:returnValue]);\
return [NSNumber _selector:returnValue]; \
} \
} while(0)

#define STRUCV(_type) do { \
if(strncmp(str, @encode(_type), strlen(@encode(_type))) == 0) \
{ \
_type returnValue = *(_type *)argumentPtr; \
NSLog(@"参数值是%@",NSStringFrom##_type(returnValue));\
return (NSString *)NSStringFrom##_type(returnValue); \
} \
} while(0)
    
#define PTROC(type) do { \
if(str[0] == @encode(type)[0]) \
{\
NSLog(@"OC对象参数值是%@",(__bridge id)(*(void**)argumentPtr));\
return (__bridge id)(*(void**)argumentPtr); \
}\
} while(0)


    

#define PTRC(type) do { \
if(str[0] == @encode(type)[0]) \
{\
NSLog(@"C指针地址是%p",*argumentPtr);\
return *argumentPtr; \
}\
} while(0)
    
    
    SINTV(_Bool, numberWithBool);
    SINTV(signed char, numberWithChar);
    SINTV(unsigned char, numberWithUnsignedChar);
    SINTV(short, numberWithShort);
    SINTV(int, numberWithInt);
    SINTV(long, numberWithLong);
    SINTV(long long, numberWithLongLong);
    
    PTROC(id);
    PTROC(Class);

    if(str[0] == @encode(SEL)[0]) {
        SEL returnValue = *(SEL *)argumentPtr;
        NSLog(@"OC对象参数值是%@",NSStringFromSelector(returnValue));\
        return NSStringFromSelector(returnValue); \
    }
    PTRC(void *);
    PTRC(char *);
    PTRC(void (*)(void));
    
    SINTV(float, numberWithFloat);
    SINTV(double, numberWithDouble);
    STRUCV(CGRect);
    STRUCV(CGPoint);
    STRUCV(CGSize);
    return (void *)0;
}

通过不同的类型,结合OC的打印方式,打印出参数的数值,本例中参数打印结果为:

如何使用libffi库实现OC方法替换和调用 OC 函数_第1张图片
BC46A297-5728-44F6-9A53-2C7CE779F8EA.png

因为只写了一个测试方法,返回值目前是写死的,见BlockClosure的注释。
在控制台打印的返回值如下:
returnRect 的值是{{321, 123}, {10, 10}}。
好了简单分析到这里~
本文的Demo地址在:
https://github.com/springwinding/MABlockClosureEx

后面,我会将本文的实现方式移植到JSPatch中,重写JPEngine中的
static void overrideMethod(Class cls, NSString *selectorName, JSValue *function, BOOL isClassMethod, const char *typeDescription)
和static void JPForwardInvocation(__unsafe_unretained id assignSlf, SEL selector, NSInvocation *invocation),看能否实现不通过消息转发,将OC的方法回掉给JS函数。


如何利用 libffi 调用 OC 函数
Demo 里新增调用的方式,没有完善,只是提供一种调用思路,通过主动调用ffi_call 函数,同时绑定 OC 的方法的 IMP 指针和调用参数,详见 Demo 中的例子,不多聊。
参考文献如下:
http://www.cocoachina.com/ios/20161220/18400.html
https://github.com/bang590/JSPatch
http://www.atmark-techno.com/~yashi/libffi.html
https://github.com/mikeash/MABlockClosure

你可能感兴趣的:(如何使用libffi库实现OC方法替换和调用 OC 函数)