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,得好好思考下,因为比较绕,稍不留神可能就铸成大错。