前言
眾所周知,使用runtime的提供的接口,我們可以設(shè)定原方法的 IMP ,或交換原方法和目標(biāo)方法的 IMP ,以完全代替原方法的實(shí)現(xiàn),或?yàn)樵瓕?shí)現(xiàn)前后相當(dāng)于加一段額外的代碼。
@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庫(kù) Aspects ,可以更便捷地為原方法實(shí)現(xiàn)前后增加(代替)額外的執(zhí)行。
// 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同一個(gè)方法,支持從hook返回的 id 對(duì)象刪除對(duì)應(yīng)的hook。
IMP 即函數(shù)指針, Aspects 的大致原理:替換原方法的 IMP 為 消息轉(zhuǎn)發(fā)函數(shù)指針 _objc_msgForward 或 _objc_msgForward_stret ,把原方法 IMP 添加并對(duì)應(yīng)到 SEL aspects_originalSelector ,將 forwardInvocation: 的 IMP 替換為參數(shù)對(duì)齊的C函數(shù) __ASPECTS_ARE_BEING_CALLED__(NSObject *self, SEL selector, NSInvocation *invocation) 的指針。在 __ASPECTS_ARE_BEING_CALLED__ 函數(shù)中,替換 invocation 的 selector 為 aspects_originalSelector ,相當(dāng)于要發(fā)送調(diào)用原始方法實(shí)現(xiàn)的消息。對(duì)于插入位置在前面,替換,后面的多個(gè)block,構(gòu)建新的blockInvocation,從invocation中提取參數(shù),最后通過(guò) invokeWithTarget:block 來(lái)完成依次調(diào)用。有關(guān)消息轉(zhuǎn)發(fā)的介紹,可以參考筆者的另一篇文章 用代碼理解ObjC中的發(fā)送消息和消息轉(zhuǎn)發(fā) 。
Aspects 實(shí)現(xiàn)代碼里的很多細(xì)節(jié)處理是很令人稱道的,且支持hook類的單個(gè)實(shí)例對(duì)象的方法(類似于KVO的 isa-swizzlling )。但由于對(duì)原方法調(diào)用直接進(jìn)行了消息轉(zhuǎn)發(fā),到真正的 IMP 對(duì)應(yīng)的函數(shù)被執(zhí)行前,經(jīng)歷了對(duì)其他多個(gè)消息的處理,invoke block也需要額外的invocation構(gòu)建開(kāi)銷。作者也在注釋中寫(xiě)道,不適合對(duì)每秒鐘超過(guò)1000次的方法增加切面代碼。此外,使用其他方式對(duì)Aspect hook過(guò)的方法進(jìn)行hook時(shí),如直接替換為新的 IMP ,新hook得到的原始實(shí)現(xiàn)是 _objc_msgForward ,之前的aspect_hook會(huì)失效,新的hook也將執(zhí)行異常。
那么不禁要思考,有沒(méi)有一種方式可以替換原方法的 IMP 為一個(gè)和原方法參數(shù)相同(type encoding)的方法的函數(shù)指針,作為殼,處理消息時(shí),在這個(gè)殼內(nèi)部拿到所有參數(shù),最后通過(guò)函數(shù)指針直接執(zhí)行“前”、“原始/替換”,“后”的多個(gè)代碼塊。令人驚喜的是, libffi 可以幫我們做到這一切。
1. libffi 簡(jiǎn)介
libffi 可以認(rèn)為是實(shí)現(xiàn)了C語(yǔ)言上的runtime,簡(jiǎn)單來(lái)說(shuō), libffi 可根據(jù) 參數(shù)類型 ( ffi_type ), 參數(shù)個(gè)數(shù) 生成一個(gè) 模板 ( ffi_cif );可以輸入 模板 、 函數(shù)指針 和 參數(shù)地址 來(lái)直接完成 函數(shù)調(diào)用 ( ffi_call ); 模板也可以生成一個(gè)所謂的 閉包 ( ffi_closure ),并得到指針,當(dāng)執(zhí)行到這個(gè)地址時(shí),會(huì)執(zhí)行到自定義的 void function(ffi_cif *cif, void *ret, void **args, void *userdata) 函數(shù),在這里,我們可以獲得所有參數(shù)的地址(包括返回值),以及自定義數(shù)據(jù) userdata 。當(dāng)然,在這個(gè)函數(shù)里我們可以做一些額外的操作。
1.1 ffi_type
根據(jù)參數(shù)個(gè)數(shù)和參數(shù)類型生成的各自的ffi_type。
int fun1 (int a, int b) {
return a + b;
}
int fun2 (int a, int b) {
return 2 * a + b;
}
...
ffi_type **types; // 參數(shù)類型
types = malloc(sizeof(ffi_type *) * 2) ;
types[0] = &ffi_type_sint;
types[1] = &ffi_type_sint;
ffi_type *retType = &ffi_type_sint;
1.2 ffi_call
根據(jù)ffi_type生成特定cif,輸入cif、 函數(shù)指針、參數(shù)地址動(dòng)態(tài)調(diào)用函數(shù)。
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);
// 動(dòng)態(tài)調(diào)用fun1
ffi_call(&cif, fun1, &ret, args);
...
// 輸出: ret = 3;
1.3 ffi_prep_closure_loc
生成closure,并產(chǎn)生一個(gè)函數(shù)指針imp,當(dāng)執(zhí)行到imp時(shí),獲得所有輸入?yún)?shù), 后續(xù)將執(zhí)行ffi_function。
void ffi_function(ffi_cif *cif, void *ret, void **args, void *userdata) {
...
// args為所有參數(shù)的內(nèi)存地址
}
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 能調(diào)用任意 C 函數(shù)的原理與 objc_msgSend 的原理類似,其底層是用匯編實(shí)現(xiàn)的, ffi_call 根據(jù)模板cif和參數(shù)值,把參數(shù)都按規(guī)則塞到棧/寄存器里,調(diào)用的函數(shù)可以按規(guī)則得到參數(shù),調(diào)用完再獲取返回值,清理數(shù)據(jù)。通過(guò)其他方式調(diào)用上文中的imp, ffi_closure 可根據(jù)棧/寄存器、模板cif拿到所有的參數(shù),接著執(zhí)行自定義函數(shù) ffi_function 里的代碼。JPBlock的實(shí)現(xiàn)正是利用了后一種方式,更多細(xì)節(jié)介紹可以參考 bang: 如何動(dòng)態(tài)調(diào)用 C 函數(shù) 。
到這里,對(duì)于如何hook ObjC方法和實(shí)現(xiàn)AOP,想必大家已經(jīng)有了一些思路,我們可以將 ffi_closure 關(guān)聯(lián)的指針替換原方法的IMP,當(dāng)對(duì)象收到該方法的消息時(shí) objc_msgSend(id self, SEL sel, ...) ,將最終執(zhí)行自定義函數(shù) void ffi_function(ffi_cif *cif, void *ret, void **args, void *userdata) 。而實(shí)現(xiàn)這一切的主要工作是:設(shè)計(jì)可行的結(jié)構(gòu),存儲(chǔ)類的多個(gè)hook信息;根據(jù)包含不同參數(shù)的方法和切面block,生成包含匹配 ffi_type 的cif;替換類某個(gè)方法的實(shí)現(xiàn)為 ffi_closure 關(guān)聯(lián)的imp,記錄hook;在 ffi_function 里,根據(jù)獲得的參數(shù),動(dòng)態(tài)調(diào)用原始imp和block。
#import
#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 *)st_allIdentifiersForKey:(SEL)key;
+ (BOOL)st_removeHookWithIdentifier:(STIdentifier)identifier forKey:(SEL)key;
@end
下文將圍繞一些重要的點(diǎn)來(lái)介紹下筆者的實(shí)現(xiàn)。 Stinger
2 方法簽名 & ffi_type
2.1 方法簽名 -> ffi_type
對(duì)于方法的簽名和type encoding,筆者在 用代碼理解ObjC中的發(fā)送消息和消息轉(zhuǎn)發(fā) 一文中已經(jīng)有了不少介紹。簡(jiǎn)而言之,type encoding 字符串與方法的返回類型及參數(shù)類型是一一對(duì)應(yīng)的。例如: - (void)print1:(NSString *)s; 的type encoding為 v24@0:8@16 。 v 對(duì)應(yīng) void , @ 對(duì)應(yīng) id (這里是self), : 對(duì)應(yīng) SEL , @ 對(duì)應(yīng) id (這里是NSString *),另一方面,每一種參數(shù)類型都對(duì)應(yīng)一種 ffi_type ,如 v 對(duì)應(yīng) ffi_type_void , @ 對(duì)應(yīng) ffi_type_pointer 。可以用type encoding生成一個(gè) NSMethodSignature 實(shí)例對(duì)象,利用 numberOfArguments 和 - (const char *)getArgumentTypeAtIndex:(NSUInteger)idx; 方法獲取每一個(gè)位置上的參數(shù)類型。當(dāng)然,也可以過(guò)濾掉數(shù)字來(lái)分隔字符串 v24@0:8@16 (@?為block),得到參數(shù)類型數(shù)組(JSPatch中使用了這一方式)。接著,我們對(duì)字符和 ffi_type 做一一對(duì)應(yīng)即可完成從方法簽名到ffi_type的轉(zhuǎn)換。
_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;
}
2.2 淺談block
2.2.1 簽名 & 函數(shù)指針
void (^block)(id params, NSString *s) = ^(id params, NSString *s) {
NSLog(@"---after2 print1: %@", s);
}
block是一個(gè)ObjC對(duì)象,可以認(rèn)為幾種block類型都繼承于NSBlock。block很特殊,從表面來(lái)看包含了持有了數(shù)據(jù)和對(duì)象(暫不討論變量捕獲),并擁有可執(zhí)行的代碼,調(diào)用方式類似于調(diào)用C函數(shù),等同于數(shù)據(jù)加函數(shù)。Block類型很神秘,但我們從 opensource-apple/objc4 和 oclang/docs/block 中看到Block 完整的數(shù)據(jù)結(jié)構(gòu)。
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
};
很多人大概已經(jīng)看過(guò) BlocksKit 的代碼,了解到Block對(duì)象可以強(qiáng)轉(zhuǎn)為 Block_layout 類型,通過(guò)標(biāo)識(shí)符和內(nèi)存地址偏移獲取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@""8@"NSString"16"
對(duì)于Block對(duì)象的的最簡(jiǎn)簽名,我們?nèi)匀豢梢詷?gòu)建 NSMethodSignature 來(lái)逐一獲取,也可以通過(guò)過(guò)濾掉數(shù)字及’”’來(lái)獲得字符數(shù)組。
_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@""8@"NSString"16"
*/ -> v,@?,@,@
可以看到,簽名的第一位是”@?”,意味著第一個(gè)參數(shù)為blcok自己,后面的才是blcok的參數(shù)類型。同理,我們依然可以通過(guò)type encoding匹配到對(duì)應(yīng)的 ffi_type 。
此外,我們可以直接獲取到Block對(duì)象的函數(shù)指針。
BlockIMP impForBlock(id block) {
struct Block_layout *layout = (__bridge void *)block;
return layout->invoke;
}
做一個(gè)簡(jiǎn)單的嘗試,直接調(diào)用Block對(duì)象的包含的函數(shù)。
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
此外,實(shí)測(cè)通過(guò) IMP _Nonnull imp_implementationWithBlock(id _Nonnull block) 獲得的函數(shù)指針對(duì)應(yīng)的參數(shù)并不包含Block對(duì)象自身,意味著簽名發(fā)生了變化。
* 為block對(duì)象增加可用方法
通過(guò)一些方式,我們可以覺(jué)得Block對(duì)象擁有了新的實(shí)例方法。
NSString *signature = [block signature];
void *blockIMP = [block blockIMP];
做法是在 STBlock 里為 NSBlock 類增加實(shí)例方法。
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對(duì)象增加可處理的消息。但如果在其他類的load方法里嘗試調(diào)用,可能會(huì)遇到STBlock類里load方法未加載的問(wèn)題。
3 存儲(chǔ)hook信息 & 生成兩個(gè)ffi_cif對(duì)象
3.1 StingerInfo
這里使用簡(jiǎn)單的對(duì)象來(lái)存儲(chǔ)單個(gè)hook信息。
@protocol StingerInfo
@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
@end
3.2 StingerInfoPool
typedef void *StingerIMP;
@protocol StingerInfoPool
@required
@property (nonatomic, strong, readonly) NSMutableArray<id> *beforeInfos;
@property (nonatomic, strong, readonly) NSMutableArray<id> *insteadInfos;
@property (nonatomic, strong, readonly) NSMutableArray<id> *afterInfos;
@property (nonatomic, strong, readonly) NSMutableArray *identifiers;
@property (nonatomic, copy) NSString *typeEncoding;
@property (nonatomic) IMP originalIMP;
@property (nonatomic) SEL sel;
- (StingerIMP)stingerIMP;
- (BOOL)addInfo:(id)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
@end
3.2.1 管理StingerInfo
這里利用三個(gè)數(shù)組來(lái)存儲(chǔ)某個(gè)類hook位置在原實(shí)現(xiàn)前、替換、實(shí)現(xiàn)后的 id 對(duì)象,并保存了原始imp。添加和刪除 id 對(duì)象的操作是線程安全的。
3.2.2 生成方法調(diào)用模板 cif
根據(jù)原始方法提供的type encoding,生成各個(gè)參數(shù)對(duì)應(yīng)的ffi_type,繼而生成cif對(duì)象,最后調(diào)用 ffi_prep_closure_loc 相當(dāng)于生成空殼函數(shù) StingerIMP 。調(diào)用 StingerIMP 將最終執(zhí)行到自定義的 static void ffi_function(ffi_cif *cif, void *ret, void **args, void *userdata) 函數(shù),此函數(shù)可獲得調(diào)用 StingerIMP 時(shí)獲得的所有參數(shù)。
- (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;
}
3.2.3 生成block調(diào)用模板 blockCif
與前面生成方法調(diào)用模板cif類似,只不過(guò)這里沒(méi)有生成殼子 ffi_closure 。值得注意的是,這里把原始方法 type encoing 的第0位( @ self )和第1位( : SEL )替換為( @?block )和( @ id )。意味著,限定了切面Block對(duì)象的簽名類型。
- (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的返回值可以為任意;寫(xiě)block時(shí),block的第0位(不考慮block自身)參數(shù)類型應(yīng)該為id,后面接的是與原方法對(duì)應(yīng)的參數(shù)。
3.2.4 ffi_function !!!
在這個(gè)函數(shù)里,獲取到了調(diào)用原始方法時(shí)的所有入?yún)⒌膬?nèi)存地址,先是根據(jù) block_cif 模板生成新的參數(shù)集 innerArgs ,第0位留給 Block 對(duì)象,第1位留給 StingerParams 對(duì)象,從第2位開(kāi)始復(fù)制原始的參數(shù)。
以下是完成切面代碼和原始imp執(zhí)行的過(guò)程:
- 利用 ffi_call(&(self->_blockCif), impForBlock(block), NULL, innerArgs); 完成所有切面位置在前block的調(diào)用。使用block模板blockCif和innerArgs。
- 利用 ffi_call(cif, (void (*)(void))self.originalIMP / impForBlock(block), ret, args); 完成對(duì)原始IMP或替換位置block imp的調(diào)用。使用原始模板cif和原始參數(shù)args,并可能產(chǎn)生返回值。
- 利用 ffi_call(&(self->_blockCif), impForBlock(block), NULL, innerArgs); 完成所有切面位置在后的block的調(diào)用。使用block模板blockCif和innerArgs。
tatic 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 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 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 對(duì)象包含了消息接收者slf,當(dāng)前消息的selector sel, 還包含了可調(diào)用原始方法的invocation(使用invokeUsingIMP:完成調(diào)用),該invocation僅適合在替換方法且需要原始返回值作參數(shù)時(shí)調(diào)用。其他hook直接使用optionBefore或after即可, 不用關(guān)注該invocation。
#import
#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
- (void)addOriginalInvocation:(NSInvocation *)invocation;
- (void)addOriginalIMP:(IMP)imp;
@end
4 替換方法實(shí)現(xiàn) & 記錄HOOK
思路是對(duì)某個(gè)類以SEL sel為鍵關(guān)聯(lián)一個(gè) id 對(duì)象,第一次hook,新建該對(duì)象,嘗試替換原方法實(shí)現(xiàn)為 ffi_prep_closure_loc 關(guān)聯(lián)的IMP,后續(xù)hook時(shí),將直接添加hook info到關(guān)聯(lián)的 id 對(duì)象中。
關(guān)于條件,最主要的就是兩點(diǎn),第一點(diǎn)就是對(duì)于某個(gè)類中(父類)的某個(gè)SEL sel要能找到對(duì)應(yīng)Method m及IMP imp;第二點(diǎn)即切面block與原方法的簽名是匹配的,且切面block的簽名是符合要求的(isMatched方法)。
#import "Stinger.h"
#import
#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 *)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 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 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 getStingerInfoPool(Class cls, SEL key) {
NSCParameterAssert(cls);
NSCParameterAssert(key);
return objc_getAssociatedObject(cls, key);
}
NS_INLINE void setStingerInfoPool(Class cls, SEL key, id infoPool) {
NSCParameterAssert(cls);
NSCParameterAssert(key);
objc_setAssociatedObject(cls, key, infoPool, OBJC_ASSOCIATION_RETAIN);
}
NS_INLINE NSArray * getAllIdentifiers(Class cls, SEL key) {
NSCParameterAssert(cls);
NSCParameterAssert(key);
id 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.
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 params, NSString *s) {
NSLog(@"---before1 print1: %@", s);
}];
[self st_hookInstanceMethod:@selector(print1:) option:STOptionBefore usingIdentifier:@"hook_print1_before2" withBlock:^(id params, NSString *s) {
NSLog(@"---before2 print1: %@", s);
}];
[self st_hookInstanceMethod:@selector(print1:) option:STOptionAfter usingIdentifier:@"hook_print1_after1" withBlock:^(id params, NSString *s) {
NSLog(@"---after1 print1: %@", s);
}];
[self st_hookInstanceMethod:@selector(print1:) option:STOptionAfter usingIdentifier:@"hook_print1_after2" withBlock:^(id 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 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 params, NSString *s) {
NSLog(@"---after1 print2 self:%@ SEL: %@ p: %@",[params slf], NSStringFromSelector([params sel]), s);
}];
}
@end
Stinger用法與Aspects很相似,但收到消息后,由于block和原始IMP直接使用函數(shù)指針進(jìn)行調(diào)用,不處理額外的消息,不用實(shí)例化諸多NSInvocation對(duì)象,兩個(gè)lib_cif對(duì)象在hook后也即準(zhǔn)備好,相比aspects,實(shí)測(cè)有5%到50%左右的速度提升。使用其他方式hook時(shí),仍能保證st_hook的有效性。
謝謝觀看,水平有限,如有錯(cuò)誤,請(qǐng)指正。
https://github.com/Assuner-Lee/Stinger
參考資料
https://github.com/opensource-apple/objc4
http://blog.cnbang.net/tech/3219/
https://juejin.im/post/5a308f856fb9a0452b4937cc
https://github.com/mikeash/MABlockClosure
如有任何知識(shí)產(chǎn)權(quán)、版權(quán)問(wèn)題或理論錯(cuò)誤,還請(qǐng)指正。
