最近在学习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的打印方式,打印出参数的数值,本例中参数打印结果为:
因为只写了一个测试方法,返回值目前是写死的,见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