IOS切面编程Hook

2019-07-17  本文已影响0人  Johnny_Wu

最近项目需要统计用户操作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,得好好思考下,因为比较绕,稍不留神可能就铸成大错。

上一篇下一篇

猜你喜欢

热点阅读