美文网首页
IOS切面编程Hook

IOS切面编程Hook

作者: Johnny_Wu | 来源:发表于2019-07-17 15:49 被阅读0次

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

    相关文章

      网友评论

          本文标题:IOS切面编程Hook

          本文链接:https://www.haomeiwen.com/subject/lxgflctx.html