如何使用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的打印方式,打印出参数的数值,本例中参数打印结果为:

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

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

推荐阅读更多精彩内容