众所周知,使用runtime的提供的接口,咱们能够设定原方法的IMP
,或交换原方法和目标方法的IMP
,以彻底代替原方法的实现,或为原实现先后至关于加一段额外的代码。html
@interface ClassA: NSObject
- (void)methodA;
+ (void)methodB;
@end
...
@implementation ClassA (Swizzle)
+ (void)load {
Method originalMethod = class_getInstanceMethod(self, @selector(methodA));
Method swizzledMethod = class_getInstanceMethod(self, @selector(swizzled_methodA));
method_exchangeImplementations(originalMethod, swizzledMethod);
}
- (void)swizzled_methodA {
...
[self swizzled_methodA];
...
}
@end
复制代码
使用知名的AOP库 Aspects ,能够更便捷地为原方法实现先后增长(代替)额外的执行。git
// hook instance method
[ClassA aspect_hookSelector:@selector(methodA)
withOptions:AspectPositionAfter
usingBlock:^{...}
error:nil];
// hook class method
[object_getClass(ClassA) aspect_hookSelector:@selector(methodB)
withOptions:AspectPositionAfter
usingBlock:^{...}
error:nil];
复制代码
另外,Aspects 支持屡次hook同一个方法,支持从hook返回的id<AspectToken>
对象删除对应的hook。
IMP
即函数指针,Aspects 的大体原理:替换原方法的IMP
为 消息转发函数指针 _objc_msgForward
或_objc_msgForward_stret
,把原方法IMP
添加并对应到SEL
aspects_originalSelector
,将forwardInvocation:
的IMP
替换为参数对齐的C函数__ASPECTS_ARE_BEING_CALLED__(NSObject *self, SEL selector, NSInvocation *invocation)
的指针。在__ASPECTS_ARE_BEING_CALLED__
函数中,替换invocation
的selector
为aspects_originalSelector
,至关于要发送调用原始方法实现的消息。对于插入位置在前面,替换,后面的多个block,构建新的blockInvocation,从invocation中提取参数,最后经过invokeWithTarget:block
来完成依次调用。有关消息转发的介绍,能够参考笔者的另外一篇文章用代码理解ObjC中的发送消息和消息转发。
Aspects 实现代码里的不少细节处理是很使人称道的,且支持hook类的单个实例对象的方法(相似于KVO的isa-swizzlling
)。但因为对原方法调用直接进行了消息转发,到真正的IMP
对应的函数被执行前,经历了对其余多个消息的处理,invoke block也须要额外的invocation构建开销。做者也在注释中写道,不适合对每秒钟超过1000次的方法增长切面代码。此外,使用其余方式对Aspect hook过的方法进行hook时,如直接替换为新的IMP
,新hook获得的原始实现是_objc_msgForward
,以前的aspect_hook会失效,新的hook也将执行异常。
那么不由要思考,有没有一种方式能够替换原方法的IMP
为一个和原方法参数相同(type encoding)的方法的函数指针,做为壳,处理消息时,在这个壳内部拿到全部参数,最后经过函数指针直接执行“前”、“原始/替换”,“后”的多个代码块。使人惊喜的是,libffi 能够帮咱们作到这一切。github
libffi 能够认为是实现了C语言上的runtime,简单来讲,libffi 可根据 参数类型(ffi_type
),参数个数 生成一个 模板(ffi_cif
);能够输入 模板、函数指针 和 参数地址 来直接完成 函数调用(ffi_call
); 模板 也能够生成一个所谓的 闭包(ffi_closure
),并获得指针,当执行到这个地址时,会执行到自定义的void function(ffi_cif *cif, void *ret, void **args, void *userdata)
函数,在这里,咱们能够得到全部参数的地址(包括返回值),以及自定义数据userdata
。固然,在这个函数里咱们能够作一些额外的操做。
数组
根据参数个数和参数类型生成的各自的ffi_type。安全
int fun1 (int a, int b) {
return a + b;
}
int fun2 (int a, int b) {
return 2 * a + b;
}
...
ffi_type **types; // 参数类型
types = malloc(sizeof(ffi_type *) * 2) ;
types[0] = &ffi_type_sint;
types[1] = &ffi_type_sint;
ffi_type *retType = &ffi_type_sint;
复制代码
根据ffi_type生成特定cif,输入cif、 函数指针、参数地址动态调用函数。bash
void **args = malloc(sizeof(void *) * 2);
int x = 1, y = 2;
args[0] = &x;
args[1] = &y;
int ret;
ffi_cif cif;
// 生成模板
ffi_prep_cif(&cif, FFI_DEFAULT_ABI, 2, retType, types);
// 动态调用fun1
ffi_call(&cif, fun1, &ret, args);
...
// 输出: ret = 3;
复制代码
生成closure,并产生一个函数指针imp,当执行到imp时,得到全部输入参数, 后续将执行ffi_function。数据结构
void ffi_function(ffi_cif *cif, void *ret, void **args, void *userdata) {
...
// args为全部参数的内存地址
}
ffi_cif cif;
// 生成模板
ffi_prep_cif(&cif, FFI_DEFAULT_ABI, 2, returnType, types);
ffi_prep_closure_loc(_closure, &_cif, ffi_function, (__bridge void *)(self), imp);
void *imp = NULL;
ffi_closure *closure = ffi_closure_alloc(sizeof(ffi_closure), (void **)&imp);
//生成ffi_closure
ffi_prep_closure_loc(closure, &cif, ffi_function, (__bridge void *)(self), stingerIMP);
复制代码
libffi 能调用任意 C 函数的原理与objc_msgSend
的原理相似,其底层是用汇编实现的,ffi_call
根据模板cif和参数值,把参数都按规则塞到栈/寄存器里,调用的函数能够按规则获得参数,调用完再获取返回值,清理数据。经过其余方式调用上文中的imp,ffi_closure
可根据栈/寄存器、模板cif拿到全部的参数,接着执行自定义函数ffi_function
里的代码。JPBlock的实现正是利用了后一种方式,更多细节介绍能够参考 bang: 如何动态调用 C 函数。
到这里,对于如何hook ObjC方法和实现AOP,想必你们已经有了一些思路,咱们能够将ffi_closure
关联的指针替换原方法的IMP,当对象收到该方法的消息时objc_msgSend(id self, SEL sel, ...)
,将最终执行自定义函数void ffi_function(ffi_cif *cif, void *ret, void **args, void *userdata)
。而实现这一切的主要工做是:设计可行的结构,存储类的多个hook信息;根据包含不一样参数的方法和切面block,生成包含匹配ffi_type
的cif;替换类某个方法的实现为ffi_closure
关联的imp,记录hook;在ffi_function
里,根据得到的参数,动态调用原始imp和block。
闭包
#import <Foundation/Foundation.h>
#import "StingerParams.h"
typedef NSString *STIdentifier;
typedef NS_ENUM(NSInteger, STOption) {
STOptionAfter = 0,
STOptionInstead = 1,
STOptionBefore = 2,
};
@interface NSObject (Stinger)
+ (BOOL)st_hookInstanceMethod:(SEL)sel option:(STOption)option usingIdentifier:(STIdentifier)identifier withBlock:(id)block;
+ (BOOL)st_hookClassMethod:(SEL)sel option:(STOption)option usingIdentifier:(STIdentifier)identifier withBlock:(id)block;
+ (NSArray<STIdentifier> *)st_allIdentifiersForKey:(SEL)key;
+ (BOOL)st_removeHookWithIdentifier:(STIdentifier)identifier forKey:(SEL)key;
@end
复制代码
下文将围绕一些重要的点来介绍下笔者的实现。Stingerapp
对于方法的签名和type encoding,笔者在 用代码理解ObjC中的发送消息和消息转发 一文中已经有了很多介绍。简而言之,type encoding 字符串与方法的返回类型及参数类型是一一对应的。例如:- (void)print1:(NSString *)s;
的type encoding为v24@0:8@16
。v
对应void
,@
对应id
(这里是self),:
对应SEL
,@
对应id
(这里是NSString *),另外一方面,每一种参数类型都对应一种ffi_type
,如v
对应ffi_type_void
, @
对应ffi_type_pointer
。能够用type encoding生成一个NSMethodSignature
实例对象,利用numberOfArguments
和 - (const char *)getArgumentTypeAtIndex:(NSUInteger)idx;
方法获取每个位置上的参数类型。固然,也能够过滤掉数字来分隔字符串v24@0:8@16
(@?为block),获得参数类型数组(JSPatch中使用了这一方式)。接着,咱们对字符和ffi_type
作一一对应便可完成从方法签名到ffi_type的转换。ide
_args = malloc(sizeof(ffi_type *) * argumentCount) ;
for (int i = 0; i < argumentCount; i++) {
ffi_type* current_ffi_type = ffiTypeWithType(self.signature.argumentTypes[i]);
NSAssert(current_ffi_type, @"can't find a ffi_type of %@", self.signature.argumentTypes[i]);
_args[i] = current_ffi_type;
}
复制代码
void (^block)(id<StingerParams> params, NSString *s) = ^(id<StingerParams> params, NSString *s) {
NSLog(@"---after2 print1: %@", s);
}
复制代码
block是一个ObjC对象,能够认为几种block类型都继承于NSBlock。block很特殊,从表面来看包含了持有了数据和对象(暂不讨论变量捕获),并拥有可执行的代码,调用方式相似于调用C函数,等同于数据加函数。Block类型很神秘,但咱们从 opensource-apple/objc4 和 oclang/docs/block 中看到Block 完整的数据结构。
enum {
BLOCK_DEALLOCATING = (0x0001), // runtime
BLOCK_REFCOUNT_MASK = (0xfffe), // runtime
BLOCK_NEEDS_FREE = (1 << 24), // runtime
BLOCK_HAS_COPY_DISPOSE = (1 << 25), // compiler
BLOCK_HAS_CTOR = (1 << 26), // compiler: helpers have C++ code
BLOCK_IS_GC = (1 << 27), // runtime
BLOCK_IS_GLOBAL = (1 << 28), // compiler
BLOCK_USE_STRET = (1 << 29), // compiler: undefined if !BLOCK_HAS_SIGNATURE
BLOCK_HAS_SIGNATURE = (1 << 30) // compiler
};
// revised new layout
#define BLOCK_DESCRIPTOR_1 1
struct Block_descriptor_1 {
unsigned long int reserved;
unsigned long int size;
};
#define BLOCK_DESCRIPTOR_2 1
struct Block_descriptor_2 {
// requires BLOCK_HAS_COPY_DISPOSE
void (*copy)(void *dst, const void *src);
void (*dispose)(const void *);
};
#define BLOCK_DESCRIPTOR_3 1
struct Block_descriptor_3 {
// requires BLOCK_HAS_SIGNATURE
const char *signature;
const char *layout;
};
struct Block_layout {
void *isa;
volatile int flags; // contains ref count
int reserved;
void (*invoke)(void *, ...);
struct Block_descriptor_1 *descriptor;
// imported variables
};
复制代码
不少人大概已经看过BlocksKit
的代码,了解到Block对象能够强转为Block_layout
类型,经过标识符和内存地址偏移获取block的签名signature
。
NSString *signatureForBlock(id block) {
struct Block_layout *layout = (__bridge void *)block;
if (!(layout->flags & BLOCK_HAS_SIGNATURE))
return nil;
void *descRef = layout->descriptor;
descRef += 2 * sizeof(unsigned long int);
if (layout->flags & BLOCK_HAS_COPY_DISPOSE)
descRef += 2 * sizeof(void *);
if (!descRef)
return nil;
const char *signature = (*(const char **)descRef);
return [NSString stringWithUTF8String:signature];
}
复制代码
NSString *signature = signatureForBlock(block)
// 输出 NSString:@"v24@?0@\"<StingerParams>\"8@\"NSString\"16"
复制代码
对于Block对象的的最简签名,咱们仍然能够构建NSMethodSignature
来逐一获取,也能够经过过滤掉数字及'\"'来得到字符数组。
_argumentTypes = [[NSMutableArray alloc] init];
NSInteger descNum = 0; // num of '\"' in block signature type encoding
for (int i = 0; i < _types.length; i ++) {
unichar c = [_types characterAtIndex:i];
NSString *arg;
if (c == '\"') ++descNum;
if ((descNum % 2) != 0 || (c == '\"' || isdigit(c))) {
continue;
}
...
}
/*@"v24@?0@\"<StingerParams>\"8@\"NSString\"16"
*/ -> v,@?,@,@
复制代码
能够看到,签名的第一位是"@?",意味着第一个参数为blcok本身,后面的才是blcok的参数类型。同理,咱们依然能够经过type encoding匹配到对应的ffi_type
。
此外,咱们能够直接获取到Block对象的函数指针。
BlockIMP impForBlock(id block) {
struct Block_layout *layout = (__bridge void *)block;
return layout->invoke;
}
复制代码
作一个简单的尝试,直接调用Block对象的包含的函数。
void (^block2)(NSString *s) = ^(NSString *s) {
NSLog(@"---after2 print1: %@", s);
};
void (*blockIMP) (id block, NSString *s) = (void (*) (id block, NSString *s))impForBlock(block2);
blockIMP(block2, @"tt");
// 输出:---after2 print1: tt
复制代码
此外,实测经过
IMP _Nonnull imp_implementationWithBlock(id _Nonnull block)
得到的函数指针对应的参数并不包含Block对象自身,意味着签名发生了变化。
经过一些方式,咱们能够以为Block对象拥有了新的实例方法。
NSString *signature = [block signature];
void *blockIMP = [block blockIMP];
复制代码
作法是在STBlock
里为NSBlock
类增长实例方法。
typedef void *BlockIMP;
@interface STBlock : NSObject
+ (instancetype)new NS_UNAVAILABLE;
- (instancetype)init NS_UNAVAILABLE;
- (NSString *)signature;
- (BlockIMP)blockIMP;
NSString *signatureForBlock(id block);
BlockIMP impForBlock(id block);
@end
复制代码
#define NSBlock NSClassFromString(@"NSBlock")
void addInstanceMethodForBlock(SEL sel) {
Method m = class_getInstanceMethod(STBlock.class, sel);
if (!m) return;
IMP imp = method_getImplementation(m);
const char *typeEncoding = method_getTypeEncoding(m);
class_addMethod(NSBlock, sel, imp, typeEncoding);
}
@implementation STBlock
+ (void)load {
addInstanceMethodForBlock(@selector(signature));
addInstanceMethodForBlock(@selector(blockIMP));
}
...
@end
复制代码
这样作,能够为Block对象增长可处理的消息。但若是在其余类的load方法里尝试调用,可能会遇到STBlock类里load方法未加载的问题。
这里使用简单的对象来存储单个hook信息。
@protocol StingerInfo <NSObject>
@required
@property (nonatomic, copy) id block;
@property (nonatomic, assign) STOption option;
@property (nonatomic, copy) STIdentifier identifier;
@optional
+ (instancetype)infoWithOption:(STOption)option withIdentifier:(STIdentifier)identifier withBlock:(id)block;
@end
@interface StingerInfo : NSObject <StingerInfo>
@end
复制代码
typedef void *StingerIMP;
@protocol StingerInfoPool <NSObject>
@required
@property (nonatomic, strong, readonly) NSMutableArray<id<StingerInfo>> *beforeInfos;
@property (nonatomic, strong, readonly) NSMutableArray<id<StingerInfo>> *insteadInfos;
@property (nonatomic, strong, readonly) NSMutableArray<id<StingerInfo>> *afterInfos;
@property (nonatomic, strong, readonly) NSMutableArray<NSString *> *identifiers;
@property (nonatomic, copy) NSString *typeEncoding;
@property (nonatomic) IMP originalIMP;
@property (nonatomic) SEL sel;
- (StingerIMP)stingerIMP;
- (BOOL)addInfo:(id<StingerInfo>)info;
- (BOOL)removeInfoForIdentifier:(STIdentifier)identifier;
@optional
@property (nonatomic, weak) Class cls;
+ (instancetype)poolWithTypeEncoding:(NSString *)typeEncoding originalIMP:(IMP)imp selector:(SEL)sel;
@end
@interface StingerInfoPool : NSObject <StingerInfoPool>
@end
复制代码
这里利用三个数组来存储某个类hook位置在原实现前、替换、实现后的id<StingerInfo>
对象,并保存了原始imp。添加和删除id<StingerInfo>
对象的操做是线程安全的。
根据原始方法提供的type encoding,生成各个参数对应的ffi_type,继而生成cif对象,最后调用ffi_prep_closure_loc
至关于生成空壳函数StingerIMP
。调用StingerIMP
将最终执行到自定义的static void ffi_function(ffi_cif *cif, void *ret, void **args, void *userdata)
函数,此函数可得到调用StingerIMP
时得到的全部参数。
- (StingerIMP)stingerIMP {
ffi_type *returnType = ffiTypeWithType(self.signature.returnType);
NSAssert(returnType, @"can't find a ffi_type of %@", self.signature.returnType);
NSUInteger argumentCount = self.signature.argumentTypes.count;
StingerIMP stingerIMP = NULL;
_args = malloc(sizeof(ffi_type *) * argumentCount) ;
for (int i = 0; i < argumentCount; i++) {
ffi_type* current_ffi_type = ffiTypeWithType(self.signature.argumentTypes[i]);
NSAssert(current_ffi_type, @"can't find a ffi_type of %@", self.signature.argumentTypes[i]);
_args[i] = current_ffi_type;
}
_closure = ffi_closure_alloc(sizeof(ffi_closure), (void **)&stingerIMP);
if(ffi_prep_cif(&_cif, FFI_DEFAULT_ABI, (unsigned int)argumentCount, returnType, _args) == FFI_OK) {
if (ffi_prep_closure_loc(_closure, &_cif, ffi_function, (__bridge void *)(self), stingerIMP) != FFI_OK) {
NSAssert(NO, @"genarate IMP failed");
}
} else {
NSAssert(NO, @"FUCK");
}
[self _genarateBlockCif];
return stingerIMP;
}
复制代码
与前面生成方法调用模板cif相似,只不过这里没有生成壳子ffi_closure
。值得注意的是,这里把原始方法type encoing
的第0位(@ self
)和第1位(: SEL
)替换为(@?block
)和(@ id<StingerParams>
)。意味着,限定了切面Block对象的签名类型。
- (void)_genarateBlockCif {
ffi_type *returnType = ffiTypeWithType(self.signature.returnType);
NSUInteger argumentCount = self.signature.argumentTypes.count;
_blockArgs = malloc(sizeof(ffi_type *) *argumentCount);
ffi_type *current_ffi_type_0 = ffiTypeWithType(@"@?");
_blockArgs[0] = current_ffi_type_0;
ffi_type *current_ffi_type_1 = ffiTypeWithType(@"@");
_blockArgs[1] = current_ffi_type_1;
for (int i = 2; i < argumentCount; i++){
ffi_type* current_ffi_type = ffiTypeWithType(self.signature.argumentTypes[i]);
_blockArgs[i] = current_ffi_type;
}
if(ffi_prep_cif(&_blockCif, FFI_DEFAULT_ABI, (unsigned int)argumentCount, returnType, _blockArgs) != FFI_OK) {
NSAssert(NO, @"FUCK");
}
}
复制代码
在非instead位置,block的返回值能够为任意;写block时,block的第0位(不考虑block自身)参数类型应该为id,后面接的是与原方法对应的参数。
在这个函数里,获取到了调用原始方法时的全部入参的内存地址,先是根据block_cif
模板生成新的参数集innerArgs
,第0位留给Block
对象,第1位留给StingerParams
对象,从第2位开始复制原始的参数。
如下是完成切面代码和原始imp执行的过程:
1. 利用ffi_call(&(self->_blockCif), impForBlock(block), NULL, innerArgs);
完成全部切面位置在前block的调用。使用block模板blockCif和innerArgs。
2. 利用ffi_call(cif, (void (*)(void))self.originalIMP / impForBlock(block), ret, args);
完成对原始IMP或替换位置block imp的调用。使用原始模板cif和原始参数args,并可能产生返回值。
3. 利用ffi_call(&(self->_blockCif), impForBlock(block), NULL, innerArgs);
完成全部切面位置在后的block的调用。使用block模板blockCif和innerArgs。
static void ffi_function(ffi_cif *cif, void *ret, void **args, void *userdata) {
StingerInfoPool *self = (__bridge StingerInfoPool *)userdata;
NSUInteger count = self.signature.argumentTypes.count;
void **innerArgs = malloc(count * sizeof(*innerArgs));
StingerParams *params = [[StingerParams alloc] init];
void **slf = args[0];
params.slf = (__bridge id)(*slf);
params.sel = self.sel;
[params addOriginalIMP:self.originalIMP];
NSInvocation *originalInvocation = [NSInvocation invocationWithMethodSignature:self.ns_signature];
for (int i = 0; i < count; i ++) {
[originalInvocation setArgument:args[i] atIndex:i];
}
[params addOriginalInvocation:originalInvocation];
innerArgs[1] = ¶ms;
memcpy(innerArgs + 2, args + 2, (count - 2) * sizeof(*args));
#define ffi_call_infos(infos) \
for (id<StingerInfo> info in infos) { \
id block = info.block; \
innerArgs[0] = █ \
ffi_call(&(self->_blockCif), impForBlock(block), NULL, innerArgs); \
} \
// before hooks
ffi_call_infos(self.beforeInfos);
// instead hooks
if (self.insteadInfos.count) {
id <StingerInfo> info = self.insteadInfos[0];
id block = info.block;
innerArgs[0] = █
ffi_call(&(self->_blockCif), impForBlock(block), ret, innerArgs);
} else {
// original IMP
ffi_call(cif, (void (*)(void))self.originalIMP, ret, args);
}
// after hooks
ffi_call_infos(self.afterInfos);
free(innerArgs);
}
复制代码
注:StingerParams 对象包含了消息接收者slf,当前消息的selector sel, 还包含了可调用原始方法的invocation(使用invokeUsingIMP:完成调用),该invocation仅适合在替换方法且须要原始返回值做参数时调用。其余hook直接使用optionBefore或after便可, 不用关注该invocation。
#import <Foundation/Foundation.h>
#define ST_NO_RET NULL
@protocol StingerParams
@required
@property (nonatomic, unsafe_unretained) id slf;
@property (nonatomic) SEL sel;
- (void)invokeAndGetOriginalRetValue:(void *)retLoc;
@end
@interface StingerParams : NSObject <StingerParams>
- (void)addOriginalInvocation:(NSInvocation *)invocation;
- (void)addOriginalIMP:(IMP)imp;
@end
复制代码
思路是对某个类以SEL sel为键关联一个id<StingerInfoPool>
对象,第一次hook,新建该对象,尝试替换原方法实现为ffi_prep_closure_loc
关联的IMP,后续hook时,将直接添加hook info到关联的id<StingerInfoPool>
对象中。
关于条件,最主要的就是两点,第一点就是对于某个类中(父类)的某个SEL sel要能找到对应Method m及IMP imp;第二点即切面block与原方法的签名是匹配的,且切面block的签名是符合要求的(isMatched方法)。
#import "Stinger.h"
#import <objc/runtime.h>
#import "StingerInfo.h"
#import "StingerInfoPool.h"
#import "STBlock.h"
#import "STMethodSignature.h"
@implementation NSObject (Stinger)
#pragma - public
+ (BOOL)st_hookInstanceMethod:(SEL)sel option:(STOption)option usingIdentifier:(STIdentifier)identifier withBlock:(id)block {
return hook(self, sel, option, identifier, block);
}
+ (BOOL)st_hookClassMethod:(SEL)sel option:(STOption)option usingIdentifier:(STIdentifier)identifier withBlock:(id)block {
return hook(object_getClass(self), sel, option, identifier, block);
}
+ (NSArray<STIdentifier> *)st_allIdentifiersForKey:(SEL)key {
NSMutableArray *mArray = [[NSMutableArray alloc] init];
@synchronized(self) {
[mArray addObjectsFromArray:getAllIdentifiers(self, key)];
[mArray addObjectsFromArray:getAllIdentifiers(object_getClass(self), key)];
}
return [mArray copy];
}
+ (BOOL)st_removeHookWithIdentifier:(STIdentifier)identifier forKey:(SEL)key {
BOOL hasRemoved = NO;
@synchronized(self) {
id<StingerInfoPool> infoPool = getStingerInfoPool(self, key);
if ([infoPool removeInfoForIdentifier:identifier]) {
hasRemoved = YES;
}
infoPool = getStingerInfoPool(object_getClass(self), key);
if ([infoPool removeInfoForIdentifier:identifier]) {
hasRemoved = YES;
}
}
return hasRemoved;
}
#pragma - inline functions
NS_INLINE BOOL hook(Class cls, SEL sel, STOption option, STIdentifier identifier, id block) {
NSCParameterAssert(cls);
NSCParameterAssert(sel);
NSCParameterAssert(option == 0 || option == 1 || option == 2);
NSCParameterAssert(identifier);
NSCParameterAssert(block);
Method m = class_getInstanceMethod(cls, sel);
NSCAssert(m, @"SEL (%@) doesn't has a imp in Class (%@) originally", NSStringFromSelector(sel), cls);
if (!m) return NO;
const char * typeEncoding = method_getTypeEncoding(m);
STMethodSignature *methodSignature = [[STMethodSignature alloc] initWithObjCTypes:[NSString stringWithUTF8String:typeEncoding]];
STMethodSignature *blockSignature = [[STMethodSignature alloc] initWithObjCTypes:signatureForBlock(block)];
if (! isMatched(methodSignature, blockSignature, option, cls, sel, identifier)) {
return NO;
}
IMP originalImp = method_getImplementation(m);
@synchronized(cls) {
StingerInfo *info = [StingerInfo infoWithOption:option withIdentifier:identifier withBlock:block];
id<StingerInfoPool> infoPool = getStingerInfoPool(cls, sel);
if (infoPool) {
return [infoPool addInfo:info];
}
infoPool = [StingerInfoPool poolWithTypeEncoding:[NSString stringWithUTF8String:typeEncoding] originalIMP:originalImp selector:sel];
infoPool.cls = cls;
IMP stingerIMP = [infoPool stingerIMP];
if (!(class_addMethod(cls, sel, stingerIMP, typeEncoding))) {
class_replaceMethod(cls, sel, stingerIMP, typeEncoding);
}
const char * st_original_SelName = [[@"st_original_" stringByAppendingString:NSStringFromSelector(sel)] UTF8String];
class_addMethod(cls, sel_registerName(st_original_SelName), originalImp, typeEncoding);
setStingerInfoPool(cls, sel, infoPool);
return [infoPool addInfo:info];
}
}
NS_INLINE id<StingerInfoPool> getStingerInfoPool(Class cls, SEL key) {
NSCParameterAssert(cls);
NSCParameterAssert(key);
return objc_getAssociatedObject(cls, key);
}
NS_INLINE void setStingerInfoPool(Class cls, SEL key, id<StingerInfoPool> infoPool) {
NSCParameterAssert(cls);
NSCParameterAssert(key);
objc_setAssociatedObject(cls, key, infoPool, OBJC_ASSOCIATION_RETAIN);
}
NS_INLINE NSArray<STIdentifier> * getAllIdentifiers(Class cls, SEL key) {
NSCParameterAssert(cls);
NSCParameterAssert(key);
id<StingerInfoPool> infoPool = getStingerInfoPool(cls, key);
return infoPool.identifiers;
}
NS_INLINE BOOL isMatched(STMethodSignature *methodSignature, STMethodSignature *blockSignature, STOption option, Class cls, SEL sel, NSString *identifier) {
//argument count
if (methodSignature.argumentTypes.count != blockSignature.argumentTypes.count) {
NSCAssert(NO, @"count of arguments isn't equal. Class: (%@), SEL: (%@), Identifier: (%@)", cls, NSStringFromSelector(sel), identifier);
return NO;
};
// loc 1 should be id<StingerParams>.
if (![blockSignature.argumentTypes[1] isEqualToString:@"@"]) {
NSCAssert(NO, @"argument 1 should be object type. Class: (%@), SEL: (%@), Identifier: (%@)", cls, NSStringFromSelector(sel), identifier);
return NO;
}
// from loc 2.
for (NSInteger i = 2; i < methodSignature.argumentTypes.count; i++) {
if (![blockSignature.argumentTypes[i] isEqualToString:methodSignature.argumentTypes[i]]) {
NSCAssert(NO, @"argument (%zd) type isn't equal. Class: (%@), SEL: (%@), Identifier: (%@)", i, cls, NSStringFromSelector(sel), identifier);
return NO;
}
}
// when STOptionInstead, returnType
if (option == STOptionInstead && ![blockSignature.returnType isEqualToString:methodSignature.returnType]) {
NSCAssert(NO, @"return type isn't equal. Class: (%@), SEL: (%@), Identifier: (%@)", cls, NSStringFromSelector(sel), identifier);
return NO;
}
return YES;
}
@end
复制代码
import UIKit;
@interface ASViewController : UIViewController
- (void)print1:(NSString *)s;
- (NSString *)print2:(NSString *)s;
@end
复制代码
#import "ASViewController+hook.h"
@implementation ASViewController (hook)
+ (void)load {
/*
* hook @selector(print1:)
*/
[self st_hookInstanceMethod:@selector(print1:) option:STOptionBefore usingIdentifier:@"hook_print1_before1" withBlock:^(id<StingerParams> params, NSString *s) {
NSLog(@"---before1 print1: %@", s);
}];
[self st_hookInstanceMethod:@selector(print1:) option:STOptionBefore usingIdentifier:@"hook_print1_before2" withBlock:^(id<StingerParams> params, NSString *s) {
NSLog(@"---before2 print1: %@", s);
}];
[self st_hookInstanceMethod:@selector(print1:) option:STOptionAfter usingIdentifier:@"hook_print1_after1" withBlock:^(id<StingerParams> params, NSString *s) {
NSLog(@"---after1 print1: %@", s);
}];
[self st_hookInstanceMethod:@selector(print1:) option:STOptionAfter usingIdentifier:@"hook_print1_after2" withBlock:^(id<StingerParams> params, NSString *s) {
NSLog(@"---after2 print1: %@", s);
}];
/*
* hook @selector(print2:)
*/
__block NSString *oldRet, *newRet;
[self st_hookInstanceMethod:@selector(print2:) option:STOptionInstead usingIdentifier:@"hook_print2_instead" withBlock:^NSString * (id<StingerParams> params, NSString *s) {
[params invokeAndGetOriginalRetValue:&oldRet];
newRet = [oldRet stringByAppendingString:@" ++ new-st_instead"];
NSLog(@"---instead print2 old ret: (%@) / new ret: (%@)", oldRet, newRet);
return newRet;
}];
[self st_hookInstanceMethod:@selector(print2:) option:STOptionAfter usingIdentifier:@"hook_print2_after1" withBlock:^(id<StingerParams> params, NSString *s) {
NSLog(@"---after1 print2 self:%@ SEL: %@ p: %@",[params slf], NSStringFromSelector([params sel]), s);
}];
}
@end
复制代码
Stinger用法与Aspects很类似,但收到消息后,因为block和原始IMP直接使用函数指针进行调用,不处理额外的消息,不用实例化诸多NSInvocation对象,两个lib_cif对象在hook后也即准备好,相比aspects,实测从收到消息到执行到切面信息(例如before block)这个过程, 花费时间下降一个数量级。使用其余方式hook时,仍能保证st_hook的有效性。
github.com/opensource-…
blog.cnbang.net/tech/3219/
juejin.im/post/5a308f…
github.com/mikeash/MAB…
若有任何知识产权、版权问题或理论错误,还请指正。
转载请注明原做者及以上信息。