title: OC交换方法
date: 2017-08-27
tags: hook,method swizzling,埋点
OC交换方法
需求:
- 项目中要做无侵入式埋点
- 项目中要拦截某些函数,而让其执行特定代码后,不再执行原函数(如在做项目页面路由时,所有由其他app(或网页)唤起本app的页面,都用
window.rootViewController
present
一个navigationController
来呈现,nav的rootVC就是要呈现的这个界面,所以需要hook这个VC的返回按钮函数,当次VC不是nav的rootVC时,用pop返回,当是rootVC时用dismiss返回) - 让所有被hook的函数都指向同一个函数
源码和使用说明地址
初始化
hook原函数,并需要调用原函数
-(void)setRecordDic:(NSDictionary *)recordDic
andHookBlock:(void (^)(NSString *target,
NSString *action,
NSDictionary *handleDic,
NSDictionary *params))handleBlock{
_recordDic = recordDic;
self.hookBlock = handleBlock;
dispatch_async(_serialQueue, ^{
NSArray *allKeys = self.recordDic.allKeys;
//读出字典中需要hook的类
for (NSString *className in allKeys) {
NSDictionary *actionDic = self.recordDic[className];
Class classInstance = NSClassFromString(className);
NSArray *actionKeys = actionDic.allKeys;
//读出该类需要hook的方法
for (NSString *actionName in actionKeys) {
Method originMethod = class_getInstanceMethod(classInstance, NSSelectorFromString(actionName));
//hook实例方法
if (originMethod) {
Class classImplenmentThisInstanceMethod = getClassThatImplementThisMethod(classInstance, originMethod);
class_addMethod(classImplenmentThisInstanceMethod,
NSSelectorFromString([NSString stringWithFormat:@"bnrHook_%@",actionName]),
method_getImplementation(originMethod),
method_getTypeEncoding(originMethod));
method_setImplementation(originMethod, (IMP)hookFunc);
}else{
//hook类方法
originMethod = class_getClassMethod(classInstance, NSSelectorFromString(actionName));
if (originMethod) {
Class metaClass = objc_getMetaClass(class_getName(classInstance));
Class metaClassImplenmentThisClassMethod = getClassThatImplementThisMethod(metaClass,originMethod);
class_addMethod(metaClassImplenmentThisClassMethod,
NSSelectorFromString([NSString stringWithFormat:@"bnrHook_%@",actionName]),
method_getImplementation(originMethod),
method_getTypeEncoding(originMethod));
method_setImplementation(originMethod, (IMP)hookFunc);
}
}
}
}
});
}
/**
* 在subClass的原型链上找最靠近该类的父类实现了这个实例方法,
*/
Class getClassThatImplementThisMethod(Class subClass,Method m){
Class classImplenmentThisInstanceMethod = subClass;
while (classImplenmentThisInstanceMethod) {
unsigned int count = 0;
Method *methods = class_copyMethodList(classImplenmentThisInstanceMethod, &count);
//函数地址表有序排列,直接比较地址大小,不遍历
if (count >0) {
//因为hook的时候,给class加了一些以bnrHook或者hnrHookOrigin开头的函数
//要除去这些函数,地址分布才是线性的
unsigned int start = 0;
while (start < count) {
SEL mm = method_getName(methods[start]);
NSString *mmName = NSStringFromSelector(mm);
if ([mmName containsString:@"bnrHook"]||
[mmName containsString:@"bnrHookOrigin"]) {
start++;
}else{
break;
}
}
if (m >= methods[start]&&
m <= methods[count-1]) {
return classImplenmentThisInstanceMethod;
}
}
classImplenmentThisInstanceMethod = class_getSuperclass(classImplenmentThisInstanceMethod);
}
return classImplenmentThisInstanceMethod;
}
如A B C D类为依次继承关系,A C实现了
func
方法,B D未实现func
hook B的
func
方法,实际上是要是让A的func
指向hookFunc
,并给A加上一个bnrHook_func
方法,让其指向A的func
具体实现,如用代码class_addMethod([B class], NSSelectorFromString([NSString stringWithFormat:@"bnrHook_%@",actionName]), method_getImplementation(originMethod), method_getTypeEncoding(originMethod));
实际上是给B加了一个bnrHook_func
,调用A类的func
时,先调用了hookFunc
的实现,确找不到A上的bnrHook_func
的实现了;
hook C的
func
函数时,需要交换的C上的func
的实现hook D的
func
函数时,需要交换的是C上的func
的实现
getClassThatImplementThisMethod
这个函数就是处理这些情况,找出具体应该交换哪个类上的方法;
hook原函数,并不需要再调用原函数
初始化方法同上,按照这里的目的,只要让原函数指向hook后的实现就可以了,并不需要给被hook的类增加函数,让其指向原来的实现;但这里有一些情况就不能满足了,比如 A B类为依次继承关系,A实现了
func
,B未实现func
。想拦截B的func
方法,因为B没有实现该方法,那实际上hook的时候,是把A的func
指向了hookFuncWithOutCallOriginFunc
的实现了,当调用A的func
时,我们希望不拦截他,希望调用原来的实现(因为B为实现func
,不拦截他做不了,能做的是在调用时,不调用用户传入的block,而去调用func
原来的实现),基于这点,还是需要往A上去增加方法,让其指向原来的实现,并在hook后,调用它。
hook函数的具体实现
/**
* 所有需要hook并需要调用源函数的函数都hook到了这个方法
*/
void* hookFunc(id self, SEL _cmd,...){
BNRHookAction *record = [BNRHookAction shareInstance];
NSString *className = NSStringFromClass([self class]);
NSDictionary *actionDic = record.recordDic[className];
NSString *actionName = NSStringFromSelector(_cmd);
NSDictionary *actionInfo = actionDic[actionName];
//hook表里面存放的是父类名字和父类action,那么子类调用这个action时,
//需要把父类的action下的字典丢出去
if (actionInfo == nil) {
Class fatherClass = [self class];
while (fatherClass) {
Class tmpClass = class_getSuperclass(fatherClass);
NSDictionary *tmpActionDic = record.recordDic[NSStringFromClass(tmpClass)];
actionInfo = tmpActionDic[actionName];
if (actionInfo == nil) {
fatherClass = tmpClass;
}else{
break;
}
}
}
SEL hookSelect = NSSelectorFromString([NSString stringWithFormat:@"bnrHook_%@",NSStringFromSelector(_cmd)]);
NSMutableDictionary *params = [NSMutableDictionary dictionary];
params[@"self"] = self;
params[@"_cmd"] = NSStringFromSelector(_cmd);
if ([self respondsToSelector:hookSelect]) {
NSMethodSignature *methodSig = [self methodSignatureForSelector:hookSelect];
if (methodSig == nil) {
return nil;
}
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
Method originMethod = class_getInstanceMethod([self class], _cmd);
if (originMethod == nil) {
originMethod = class_getClassMethod(self, _cmd);
}
unsigned int argsCount = method_getNumberOfArguments(originMethod);
va_list args;
va_start(args, _cmd);
for (int i = 2; i < argsCount; i++) {
char *paramType = method_copyArgumentType(originMethod, i);
id param = setInvocationParam(invocation, i, args, paramType);
params[[NSString stringWithFormat:@"%@",@(i-2)]] = param;
}
va_end(args);
if (actionInfo) {
!record.hookBlock?:record.hookBlock(className,actionName,actionInfo,params.copy);
}
[invocation setTarget:self];
[invocation setSelector:hookSelect];
[invocation invoke];
char *returnType = method_copyReturnType(originMethod);
return getInvocationReturnValue(invocation,returnType);
}
return nil;
}
函数第二个(从0开始)参数是一个不定形参,函数的返回值是void *类型,被hook的函数的参数个数,类型和返回值都是不确定的,在代码实现中需解析出不定形参的个数和具体的值,并通过
NSInvocation
来调用原来的函数实现,得到返回值。
备注
不支持为父类和子类同时hook同一个函数(如A是父类,A1和A2是子类,A1实现了
viewWillAppear:
,A2未实现,同时hook A的viewWillAppear:
和A1的viewWillAppear:
会造成死循环; 同时hook A1和A2的viewWillAppear:
也会造成调用的死循环,其实A2未实现viewWillAppear:
,hook A2时,就是hook的A的viewWillAppear:
. 如果子类不调用[super method]是不会造成调用死循环。)
hook函数,并需要调用原函数时,请尽量保证hook的类有实现要hook的action,而不是父类实现了这个action.