OC交换方法

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.

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容

  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,690评论 0 9
  • 1、随机数 不需要随机数种子 arc4random()%N + begin:产生begin~begin+N的随机数...
    我是小胡胡123阅读 4,146评论 0 2
  • 这篇文章完全是基于南峰子老师博客的转载 这篇文章完全是基于南峰子老师博客的转载 这篇文章完全是基于南峰子老师博客的...
    西木阅读 30,548评论 33 466
  • 2014年的苹果全球开发者大会(WWDC),当Craig Federighi向全世界宣布“We have new ...
    yeshenlong520阅读 2,275评论 0 9
  • 不要乱想喔 早点睡喔
    okubyou阅读 79评论 0 0