最近在學(xué)習(xí)JS語言,走大前端的路子,學(xué)了兩天后,感覺要小試牛刀。突然想到JSPatch中JSPatch.js是JS寫的,正好之前有一些細(xì)節(jié)還沒完全了解,就決定重看了一遍,不看不知道,一看發(fā)現(xiàn)JSPatch里比我一年前看的升級了好多,并且被JPBlock的實現(xiàn)方式驚呆了~
結(jié)合之前逆向滴滴的DYC的實現(xiàn)方式,發(fā)現(xiàn)利用滴滴的JS腳本替換的函數(shù),函數(shù)有不同的地址(如果用JSPatch的實現(xiàn)方式,被替換的函數(shù)應(yīng)該有相同的函數(shù)地址~)。
然后就突發(fā)其想,如何動態(tài)的給OC函數(shù)綁定替換函數(shù),而不利用消息轉(zhuǎn)發(fā)的方式。
有了這么多的想法,就差實踐了...
由于之前完全不了解libffi,遂在github上搜索相關(guān)的技術(shù)資料,但是發(fā)現(xiàn)能參考的文檔比較少,不過還是發(fā)現(xiàn)了一個有100多stars的工程:
https://github.com/mikeash/MABlockClosure
原作者利用自定義的Block結(jié)構(gòu)體模擬OC的block結(jié)構(gòu),進(jìn)而獲取到block的函數(shù)簽名。
舉個例子:
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的函數(shù)簽名如下:
"i12@?0i4i8"
其中i12中的i代表返回值類型是int,12代碼這個block參數(shù)的總長度是12字節(jié)(不確定,感覺是這個意思),@?0標(biāo)識block的第一參數(shù)類型是Block類型(類似函調(diào)調(diào)用的self),后面的0代表了第一個參數(shù)從函數(shù)調(diào)用棧偏移0字節(jié)開始,i4代表了第二個參數(shù)是int類型,其中4代碼表了參數(shù)的起始位置從從函數(shù)調(diào)用棧偏移4字節(jié)開始,i8代表了第三個參數(shù)是int類型,其中8參數(shù)的起始位置從從函數(shù)調(diào)用棧偏移8字節(jié)開始。
分析出OC數(shù)據(jù)類型信息后(并沒有使用類型偏移信息~,只用到了類型信息),根據(jù)每種類型生成對應(yīng)的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();
}
準(zhǔn)備就緒后
ffi_status status = ffi_prep_closure_loc(_closure, &_closureCIF, BlockClosure, self, _closureFptr);
根據(jù)類型信息(存儲在_closureCIF中),生成_closureFptr函數(shù)指針,函數(shù)指針的回調(diào)是BlockClosure方法。
基于以上的思路,既然OC 的 Block可以通過函數(shù)簽名實現(xiàn),那么OC中普通的函數(shù)也具有函數(shù)簽名,能否移植過去呢?帶著這種想法,我們準(zhǔn)備進(jìn)行進(jìn)一步的嘗試。
首先創(chuàng)建測試類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擴(kuò)展一個初始化方法,根據(jù)OC函數(shù)簽名初始化:
- (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的作用是如果傳進(jìn)來的是字符串函數(shù)簽名,則直接使用,否則,解析block中的函數(shù)簽名。
重寫_prepClosureCIF函數(shù)
- (void)_prepClosureCIF
{
_closureArgCount = [self _prepCIF: &_closureCIF withEncodeString: FunctionSig(_block?:_signature) skipArg: _block?YES:NO];
}
其中,skipArg是如果是block的類型,則忽略第一個參數(shù),如果是OC函數(shù),則不忽略第一個函數(shù)。(具體原因,需要進(jìn)一步研究...)
改造完畢后,我們嘗試在BlockClosure中獲取到OC中所有的參數(shù),并能設(shè)置返回值,就說明實現(xiàn)了目的。
我們看一下BlockClosure的實現(xiàn)
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;
}
//!!!!!!!!這里先寫死,后面根據(jù)返回值的描述進(jìn)一步寫返回值的類型
*(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語句是我加上去的,也是本文分析的重點。大致思路是根據(jù)獲取到函數(shù)簽名里的參數(shù)類型后,結(jié)合函數(shù)里傳遞進(jìn)來的參數(shù)數(shù)組,獲取參數(shù)值。因此核心部分在于分析 [self _ffiValueForEncode:str argumentPtr:args[i]]這個函數(shù)的實現(xiàn)。這個函數(shù),我具體的實現(xiàn)方式如下:
- (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(@"參數(shù)值是%@",[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(@"參數(shù)值是%@",NSStringFrom##_type(returnValue));\
return (NSString *)NSStringFrom##_type(returnValue); \
} \
} while(0)
#define PTROC(type) do { \
if(str[0] == @encode(type)[0]) \
{\
NSLog(@"OC對象參數(shù)值是%@",(__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對象參數(shù)值是%@",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;
}
通過不同的類型,結(jié)合OC的打印方式,打印出參數(shù)的數(shù)值,本例中參數(shù)打印結(jié)果為:

因為只寫了一個測試方法,返回值目前是寫死的,見BlockClosure的注釋。
在控制臺打印的返回值如下:
returnRect 的值是{{321, 123}, {10, 10}}。
好了簡單分析到這里~
本文的Demo地址在:
https://github.com/springwinding/MABlockClosureEx
后面,我會將本文的實現(xiàn)方式移植到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),看能否實現(xiàn)不通過消息轉(zhuǎn)發(fā),將OC的方法回掉給JS函數(shù)。
如何利用 libffi 調(diào)用 OC 函數(shù)
Demo 里新增調(diào)用的方式,沒有完善,只是提供一種調(diào)用思路,通過主動調(diào)用ffi_call 函數(shù),同時綁定 OC 的方法的 IMP 指針和調(diào)用參數(shù),詳見 Demo 中的例子,不多聊。
參考文獻(xiàn)如下:
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