IOS切面编程Hook

最近项目需要统计用户操作App的一些行为。我分析了一下,这里可以使用hook操作,把所有的事件都hook到一个方法中,然后在方法中进行统一进行处理。这样对原代码的入侵是最小的。

具体做法,我这里不细讲(网上有很多介绍这方面的方案)。只分析下其中遇到的问题。

一、简单介绍下Hook。

我以Hook的UIViewController-viewDidLoad为例:
目的:UIViewController的实例方法viewDidLoad与另外的一个方法比如ZX_viewDidLoad进行交换。之后,我在ZX_viewDidLoad中就可以监听到原来的viewDidLoad操作。

1、我们得为UIViewController添加ZX_viewDidLoad方法。这里我们可以通过类别的方式实现。

2、在类别中的load进行方法交换。load方法,只在加载(编译)此类时候调用一次,所以非常适合在这里进行操作。

#import <objc/runtime.h>
@implementation UIViewController (Analysis)
+(void)load
{
    Method originalMethod = class_getInstanceMethod([self class], @selector(viewWillAppear:));
    Method swizzingMethod = class_getInstanceMethod([self class], @selector(ZX_viewWillAppear:));
    method_exchangeImplementations(originalMethod, swizzingMethod);
}
-(void)ZX_viewDidLoad
{

    [self ZX_viewDidLoad];//这里调用的是原来的实现,所以不会导致死循环
    //Action_identifier=BCUserSettingVC_ViewDidLoad
}
@end

3、上面已经基本实现了Hook。但需要注意的是:我们只是Hook了UIViewController的viewDidLoad方法。并不是Hook子类的viewDidLoad方法。但UIViewController都是被继承使用的。所以子类VC必须调用[super viewDidLoad]才能触发。比如:

//子类BCUserSettingVC
- (void)viewDidLoad{
    NSLog(@"BCUserSettingVC--->super before");
    [super viewDidLoad];
    NSLog(@"BCUserSettingVC--->super after");
}

结果如下:

BCUserSettingVC--->super before
Action_identifier=BCUserSettingVC_ViewDidLoad
BCUserSettingVC--->super after

Action_identifier=BCUserSettingVC_ViewDidLoad是我在Hook方法中的输出。可见,调用[super viewDidLoad]才生效。

二、遇到的问题分析:

1)、对于Hook tableView的点击事件,网上基本都是这样实现的:
@implementation UITableView (Analysis)

+(void)load
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        
        SEL originalAppearSelector = @selector(setDelegate:);
        SEL swizzingAppearSelector = @selector(ZX_setDelegate:);
        [MethodSwizzingTool swizzingForClass:[self class] originalSel:originalAppearSelector swizzingSel:swizzingAppearSelector];
    });
}
-(void)ZX_setDelegate:(id<UITableViewDelegate>)delegate
{
    [self ZX_setDelegate:delegate];
    
    SEL sel = @selector(tableView:didSelectRowAtIndexPath:);
    SEL sel_t = @selector(ZX_tableView:didSelectRowAtIndexPath:);

    //如果没实现tableView:didSelectRowAtIndexPath:就不需要hook
    if (![delegate respondsToSelector:sel]){
        return;
    }
    BOOL addsuccess = class_addMethod([delegate class],
                                      sel_t,
                                      method_getImplementation(class_getInstanceMethod([self class], sel_t)),
                                      nil);

    //如果添加成功了就直接交换实现, 如果没有添加成功,说明之前已经添加过并交换过实现了
    if (addsuccess) {
        Method selMethod = class_getInstanceMethod([delegate class], sel);
        Method sel_Method = class_getInstanceMethod([delegate class], sel_t);
        method_exchangeImplementations(selMethod, sel_Method);
    }
}

// 由于我们交换了方法, 所以在tableview的 didselected 被调用的时候, 实质调用的是以下方法:
-(void)ZX_tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
    [self ZX_tableView:tableView didSelectRowAtIndexPath:indexPath];

    NSString * identifier = [NSString stringWithFormat:@"%@/%@/%ld", [self class],[tableView class], tableView.tag];
    NSLog(@"tableView_identifier=%@",identifier);
    
}

先Hook setDelegate方法。再去Hook代理方法tableView:didSelectRowAtIndexPath。

这里有个很有意思的点,就是往代理类动态添加方法ZX_tableView:didSelectRowAtIndexPath。这个方法的实现是在UITableView中。并且这个Hook操作,在程序运行生命周期可能多次调用。不像上面Hook UIViewController一样,只调用一次。

因为tableView:didSelectRowAtIndexPath和ZX_tableView:didSelectRowAtIndexPath都是代理类的方法,所以怎么Hook也只是影响当前的代理类。所以一般情况下是可行的。

2)、但如果是以下的情况,就会出问题了:
1、UITableView的代理类为A,分别有子类B和C。因为有B和C,那么类A中self.tableView.delegate=self就会调用两次。
2、生成B的时候,调用类A中self.tableView.delegate=self后:
SEL sel = @selector(tableView:didSelectRowAtIndexPath:);
SEL sel_t = @selector(ZX_tableView:didSelectRowAtIndexPath:);

我们定义
tableView:didSelectRowAtIndexPath的SEL为->sel
ZX_tableView:didSelectRowAtIndexPath:的SEL为->sel_t

交换实现后为:
sel-->ZX_tableView:didSelectRowAtIndexPath:
sel_t-->tableView:didSelectRowAtIndexPath:
这时候是正确的。

class_addMethod这个运行时的添加方法,只对当前类实例有效。B生成了ZX_tableView:didSelectRowAtIndexPath后与父类A的方法tableView:didSelectRowAtIndexPath进行了交互。所以类A的原sel指向了ZX_tableView:didSelectRowAtIndexPath:

3、生成C的时候,又会调用类A中self.tableView.delegate=sel。此时,又得交换A类中的方法。

C生成方法ZX_tableView:didSelectRowAtIndexPath。但对于类A是公用的,所以
类A:sel-->ZX_tableView:didSelectRowAtIndexPath:
类C:sel_t-->ZX_tableView:didSelectRowAtIndexPath:
sel与sel_t指向了同一个实现,进行了交换,还是指向同一个实现。那么就会导致死循环。

4、解决办法:

使用Aspects替换自己写的交互逻辑:

-(void)ZX_setDelegate:(id<UITableViewDelegate>)delegate{
    
    [self ZX_setDelegate:delegate];
    NSObject *obg = (NSObject *)delegate;
    if(![obg isKindOfClass:[NSObject class]]){
        return;
    }
    SEL sel = @selector(tableView:didSelectRowAtIndexPath:);
    [obg aspect_hookSelector:sel withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> aspectInfo){
        NSArray *arr = aspectInfo.arguments;
//        NSLog(@"UITableViewDelegate-aspect_hookSelector");
        if(arr.count>1){
            [self ZX_tableView:arr[0] didSelectRowAtIndexPath:arr[1]];
        }
    } error:nil];
    
}

-(void)ZX_tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
    NSString *pathStr = [MethodSwizzingTool gainIdentifier:tableView];
    NSString * identifier = [NSString stringWithFormat:@"%@/(%zi_%zi)", pathStr,indexPath.section,indexPath.row];

    [ZXFireBaseManage ZX_TableViewReport:tableView didSelectRowAtIndexPath:indexPath identifier:identifier];
}

因为Aspects会生成一个新的类,然后对此类方法进行操作。所以就不会影响到公共的父类了。想引用好hook,得好好思考下,因为比较绕,稍不留神可能就铸成大错。

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

推荐阅读更多精彩内容

  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,104评论 1 32
  • 面向对象的三大特性:封装、继承、多态 OC内存管理 _strong 引用计数器来控制对象的生命周期。 _weak...
    运气不够技术凑阅读 1,106评论 0 10
  • 1.badgeVaule气泡提示 2.git终端命令方法> pwd查看全部 >cd>ls >之后桌面找到文件夹内容...
    i得深刻方得S阅读 4,667评论 1 9
  • 废话不多说,直接上干货 ---------------------------------------------...
    小小赵纸农阅读 3,361评论 0 15
  • 1、禁止手机睡眠 [UIApplication sharedApplication].idleTimerDisab...
    小小夕舞阅读 1,461评论 1 1