iOS 统计打点那些事

1、统计代码埋点 so easy?

统计打点是 App 开发里很重要的一个环节,App 的运行状态、改版后的效果、用户的各种行为等都需要打点,市面上也有不少可供选择的第三方库。 假设产品有这么个需求:当用户在详情页点击购买按钮时,记录一下事件。我们实现起来大概会是这样

// DetailViewController.m

- (void)onBuyButtonTapped:(UIButton *)button
{
    // do some stuff, maybe send a request to server
    [XXXAnalytics event:kSomeEventYouDefined];
}

这个需求就这样轻松搞定了,但细细想想还是有不少问题的:

  • 页面上会有其他的 Button,可能每个 Button 都要放上这么一段代码。
  • 这些统计其实跟具体的业务无关,没必要跟业务代码混杂在一起,不优雅。
  • 当改版或者重构时,有可能忘了把相应的打点代码迁移过去。

2、AOP 实现业务代码与统计代码解耦

所以需要一种更好的方式来做这件事,这就是使用 AOP(Aspect-Oriented-Programming),翻译过来就是「面向切面编程」

通过预编译方式和运行期动态代理实现在不修改源代码的情况下给程序动态统一添加功能的一种技术。

简单来说,就是可以动态的在函数调用的前后插一段代码。iOS 可以使用 Pete Steinberger 开发的 Aspects 这个库,大致原理是在 runtime 层,通过 swizzle method 来实现的。

来看一个小 Demo

[UIViewController aspect_hookSelector:@selector(viewWillAppear:) withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> aspectInfo, BOOL animated) {
    NSLog(@"View Controller %@ will appear animated: %tu", aspectInfo.instance, animated);
} error:NULL];

这样在 UIViewController 的 viewWillAppear: 被调用后,还会再调一下我们定义的 Block,这段日志就会被输出。而打点正好符合这种场景:正事干完之后,额外干一些跟业务无关的事情。

上面的例子,我们通过 AOP 来做的话,大概就是这样

// DetailViewController.m
- (void)onBuyButtonTapped:(UIButton *)button
{
    // do some stuff, maybe send a request to server
    // no need to call [XXXAnalytics event:]
}

// AppDelegate.m
- (void)setupAnalytics
{
    [DetailViewController aspect_hookSelector:@selector(onBuyButtonTapped:) withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> aspectInfo, BOOL animated) {
        [XXXAnalytics event:kSomeEventYouDefined];
    } error:NULL];
}

3、多个 Button 事件怎么办?

这样统计代码就从业务代码中剥离出来了。但是又产生了一个新问题,多个 Button Event,岂不是要写很多行这样的代码,「重复」这样的事情,作为一个程序员怎么能忍,简单,造一个方法

- (void)trackEventWithClass:(Class)klass selector:(SEL)selector event:(NSString *)event
{
    [klass aspect_hookSelector:@selector(selector) withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> aspectInfo, BOOL animated) {
        [XXXAnalytics event:event];
    } error:NULL];
}

使用起来就像这样

- (void)setupAnalytics
{
    [self trackEventWithClass:DetailViewController selector:@seletor(onBuyButtonTapped:) event:kSomeEventYouDefined];
    [self trackEventWithClass:ListViewController selector:@seletor(followButtonTapped:) event:kAnotherEventYouDefined];
    // ...
}

4、统计事件有选择逻辑呢?

看起来又干净了些。这时,产品经理又提了个需求:当这个按钮点击时,如果已经登录了,发送 EventA,如果没有登录则发送 EventB,也就是说,不再只是 [XXXAnalytics event:] 这么简单了,还需要加上额外的逻辑,这也难不倒我们,加上一个 block 即可。

- (void)trackEventWithClass:(Class)klass
                   selector:(SEL)selector
               eventHandler:(void (^)(id<AspectInfo> aspectInfo))eventHandler
{
    [klass aspect_hookSelector:@selector(selector) withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> aspectInfo, BOOL animated) {
        if (eventHandler) {
            eventHandler(aspectInfo);
        }
    } error:NULL];
}

// 使用
[self trackEventWithClass:DetailViewController selector:@seletor(onBuyButtonTapped:) eventHandler:^(id<AspectInfo> aspectInfo){
    user.loggedIn ? [XXXAnalytics event:EventA] : [XXXAnalytics event:EventB];
}];

好了,现在只要不是太复杂的打点逻辑(那些需要方法上下文变量的)我们都能应付了,接下来就该等产品来验收了。产品搬了个凳子坐在身边,然后点一下 Button,看一下 Console,被几轮蹂躏后,产品也慢慢地接受了这种验收方式。后来某一天,忽然发现某一项或某几项数据有异常,然后找到开发,瞄了一眼:哦,这个方法被重构了。或者新加的方法忘了加统计了。只能等到下个版本再加上了,如果只是一般的统计数据倒还好,跟钱相关的就麻烦了。

5、如何避免统计代码被遗忘?

那么有没有一种直观的验证方式呢?当然,程序员是万能的呀。一个理想的状况是,产品打开 App 后,开启某个开关就能看到所有会发送 Event 的按钮,就像这样

其中数字代表了 EventID。如何实现呢?还记得注册事件时,我们有传入 class 和 selector 么,一般我们都会有一个 BaseViewController,那么就可以在 BaseViewController 的 viewDidAppear: 里做点文章了。

// BaseViewController.m

- (void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];
    // 获取已经注册过的 classes
    NSDictionary *registeredClasses = [OurAnalytics sharedInstance].registeredClasses;

    [registeredClasses enumerateKeysAndObjectsUsingBlock:^(NSString *className, NSArray *selectors, BOOL *stop) {
        if ([self isKindOfClass:NSClassFromString(className)]) {
            // 如何根据 selector 找到它的宿主?
        }
    }];
}

所以现在问题就剩下,如何根据 selector 找到对应的 Button,这里要注意,有些 Button 可能要等网络请求完成才会出现,比如 TableViewCell 里的 Button。

没有想到太方便的方法,简单粗暴点就是设置个 Timer 每隔一段时间扫一下 subviews,如果是 button 或 包含 tapGesture 的,就拿它们的 action 对比一下,如果 match 就可以高亮那个 button / view 了。

EventID 也一样,之前在注册时也会传一个 EventID 过来,这里直接显示出来即可。对于那些传 eventHandler 的就不行了。

所以理论上是可行的,性能上会稍微有点损耗,尤其是当 subViews 的结构比较复杂时,不过只是内部用来做验证,所以这也不是什么问题。

6、如何解决 APP 埋码要发版的问题?

看起来效果已经不错了,有没有可能让这套体系再灵活一些?比如可以从后端制定打点规则?客户端只是读取一个配置文件,就像这样

- (void)setupAnalytics
{
    // analyticsRules 是从配置文件中读取出来的
    [analyticsRules enumerateObjectsUsingBlock:^(NSDictionary *rules, NSUInteger idx, BOOL *stop) {
        Class klass = NSClassFromString(rules[@"class"]);
        SEL selector = NSSelectorFromString(rules[@"selector"]);
        NSString *eventID = rules[@"eventID"];
        [self trackEventWithClass:klass seletor:seletor event: eventID];
    }];
}

那如果在后台的时候填错了 Class 或 Selector 怎么办?还好有 objc_getClassList 和 class_copyMethodList 这两个运行时方法,有了它们就可以在 App 启动时扫一遍已注册的类(过滤掉 UI / NS 开头的),然后将它们的 seletor 也一并保存下来发送给服务端,当然这种操作只需在适当的时机做一下就可以了,比如集成打包时。

现在,这套体系就比较完整了。当然这只是我的一些构想,并没有在实践中尝试过,所以肯定会踩到各种各样的坑,不过至少看起来是个可行的方案。

7、Refer:

[1] iOS 统计打点那些事

http://limboy.me/ios/2015/09/09/ios-analytics.html

时间: 2024-10-12 03:31:46

iOS 统计打点那些事的相关文章

iOS统计项目的代码总行数

很多在麦子学院学习ios的学员在开班会时问老师,在ios开发的时候,不知道怎样可以统计出写了多少行代码,如何处理这个问题呢,下面跟着麦子学院的ios开发老师来探讨下. 如果要统计ios开发代码,包括头文件的,CD到项目目录下,命令如下 ① 列出每个文件的行数 复制代码 代码如下: find . -name "*.m" -or -name "*.h" -or -name "*.xib" -or -name "*.c" |xarg

ios统计代码行数

要统计ios开发代码,包括头文件的,终端命令进入项目目录下,命令如下 列出每个文件的行数: find . -name "*.m" -or -name "*.h" -or -name "*.xib" -or -name "*.c" |xargs wc -l 列出代码行数总和: find . -name "*.m" -or -name "*.h" -or -name "*.xib&

统计挖掘那些事那些情(2)-回归分析spss

前文:统计挖掘的一些事一些情(一) 实际上,无论是日常的统计学习还是挖掘学习中,回归分析都可以算是大家最早接触,也是整个体系当中相当重要的一个内容了,所以咱们这期就从回归分析说起吧. 一般来说,借助回归分析,我们希望可以量化描述预测变量与响应变量的关系,同时帮助我们进行预测.其他的例子还有:广告的投入与市场销售的关系,受教育程度与收入的关系等.而在整个回归体系当中,最基础的莫过于普通最小二乘回归(Ordinary Least Square,简称OLS) 实际上,对于回归分析来说,我们需要有两件事

iOS App开发那些事:如何选择合适的人、规范和框架?

自从做Team Leader之后,身上权责发生了变化,于是让我烦恼的不再是具体某个功能,某个界面的实现,而是如何在现有代码的基础上做渐进式的改进,创造出比较合适规范和框架,使得组内成员更快更好地完成任务.一年下来,颇有点想法,于是啰嗦几句关于iOS App开发的那些事. 合适的人 首先明确一点,合适的人是指纯技术团队的建设.一支战斗力再强的技术团队,面对一个朝三暮四,分分钟推翻自己原有想法的产品经理/项目经理,再好的戏也唱不出来.花几个月加班加点做项目,还没发布,直接推翻重做,这时候你就得去楼下

学习IOS需要知道的事

什么是iOS iOS是一款由苹果公司开发的操作系统(OS是Operating System的简称),就像平时在电脑上用的Windows XP.Windows 7,都是操作系统 那什么是操作系统呢?操作系统其实是一种软件,是直接运行在硬件(电脑.手机等)上的最基本的系统软件,任何其他软件都必须在操作系统的支持下才能运行. 按照运行系统的设备进行分类,可分为:电脑操作系统.手机操作系统. iOS与Win7等操作系统的差异 XP.Win7是PC操作系统,也就是运行在电脑上的操作系统 iOS是手持设备操

iOS 统计项目代码行数

最近去面试 对面的"他" 问我其中一个问题 是 "你的项目代码量是多少?" 当时的确有点蒙圈, 我可以从整个项目打包的角度考虑项目大小,我还真没想过到底我的项目代码量,于是我大概表达了一下 "我的确没从代码量考虑过项目,请教一下你们如何去统计的 如果统计项目代码量,用来干什么呢 " 然后对方回答的话我比较意外"用看的...就是看..." 大概是几个字略过去了 没有对我的疑问有任何价值性质的回应. 基于钻研精神,我后来还是问了我

IOS didReceiveMemoryWarning 的那些事

iOS的UIViewController 类给我们提供了处理内存不足的接口. 在iOS 3.0 之前,当系统的内存不足时,UIViewController的didReceiveMemoryWarining 方法会被调用,我们可以在didReceiveMemoryWarining 方法里释放掉部分暂时不用的资源. 从iOS3.0 开始,UIViewController增加了viewDidUnload方法. 该方法和viewDIdLoad相配对. 当系统内存不足时,首先UIViewControlle

iOS开发的一些事

1.+(void)load 和 +(id)inittialize的区别 +load是在一个类最开始加载时调用,+initialize是在应用中第一次调用该类或它的实例的方式之前调用.这两个方法都是可选的,只有实现了才会被执行.+load能够保证在类初始化的时候就会被加载,这为改变系统行为提供了一些统一性.但+initialize并不能保证在什么时候被调用——甚至也有可能永远也不会被调用,例如应用程序从未直接的给该类发送消息. 2.UIView和CALayer UIView是iOS系统中界面元素的

ios统计流量代码

#include <ifaddrs.h> #include <sys/socket.h> #include <net/if.h> 1.3G/GPRS流量统计 int getGprs3GFlowIOBytes() { struct ifaddrs *ifa_list = 0, *ifa; if (getifaddrs(&ifa_list) == -1) { return 0; } uint32_t iBytes = 0; uint32_t oBytes = 0;