Runtime使用之埋点

在需要统计用户行为的时候,传统点做法就是在需要统计的地方加入响应的代码,如viewWillAppear、viewWillDisappear和一些交互事件中。优雅一点可以自定义一个控制器让其它控制器继承。代码耦合,扩展性差,不易维护,这些都是上述的方法的一些弊端。昨天说的Runtime黑魔法可以很优雅方便地解决这些弊端。

具体实现

给UIViewController写个分类, + (void)load 这个类方法是在一个类被读到内存后,Runtime就会给它发送load消息,所以这个方法调用得很早,适合用来埋点。
下面混合每个UIViewController的viewDidAppear,也就是在执行原来的viewDidAppear之前先执行统计方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#import "UIViewController+swizzling.h"
#import <objc/runtime.h>

@implementation UIViewController (swizzling)

+ (void)load
{
SEL origSel = @selector(viewDidAppear:);
SEL swizSel = @selector(swiz_viewDidAppear:);
[UIViewController swizzleMethods:[self class] originalSelector:origSel swizzledSelector:swizSel];
}

//exchange implementation of two methods
+ (void)swizzleMethods:(Class)class originalSelector:(SEL)origSel swizzledSelector:(SEL)swizSel
{
Method origMethod = class_getInstanceMethod(class, origSel);
Method swizMethod = class_getInstanceMethod(class, swizSel);

//class_addMethod will fail if original method already exists
BOOL didAddMethod = class_addMethod(class, origSel, method_getImplementation(swizMethod), method_getTypeEncoding(swizMethod));
if (didAddMethod) {
class_replaceMethod(class, swizSel, method_getImplementation(origMethod), method_getTypeEncoding(origMethod));
} else {
//origMethod and swizMethod already exist
method_exchangeImplementations(origMethod, swizMethod);
}
}

- (void)swiz_viewDidAppear:(BOOL)animated
{
NSLog(@"I am in - [swiz_viewDidAppear:]");
//handle viewController transistion counting here, before ViewController instance calls its -[viewDidAppear:] method
//需要注入的代码写在此处
[self swiz_viewDidAppear:animated];
}

@end

如果想统计工程中UIButton的点击事件可以从UIControl的这个方法入手

1
- (void)sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event;

第三方库的封装

参考使用Github上的这个库Aspect
Aspect库是对面向切面编程(Aspect Oriented Programming)的实现,里面封装了Runtime的方法,也封装了上文的Method Swizzling方法。
代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
+ (id<AspectToken>)aspect_hookSelector:(SEL)selector
withOptions:(AspectOptions)options
usingBlock:(id)block
error:(NSError **)error {
return aspect_add((id)self, selector, options, block, error);
}

/// @return A token which allows to later deregister the aspect.
- (id<AspectToken>)aspect_hookSelector:(SEL)selector
withOptions:(AspectOptions)options
usingBlock:(id)block
error:(NSError **)error {
return aspect_add(self, selector, options, block, error);
}

参考一篇博客的做法,可以把所有埋点操作放到一个类方法中,在程序启动时调用即可

1
2
3
4
5
6
7
8
9
10
11
12
+ (void)createAllHooks
{
[UIViewController aspect_hookSelector:@selector(viewDidLoad)
withOptions:AspectPositionBefore
usingBlock:^(id<AspectInfo> info){
//用户统计代码写在此处
NSLog(@"[ASPECT] inject in class instance:%@", [info instance]);
}
error:NULL];
//other hooks ... goes here
//...
}