[关闭]
@MicroCai 2016-05-20T12:08:50.000000Z 字数 5199 阅读 6578

问题记录:iOS 用户行为统计代码的剥离

Archives iOS


这两天在搞一个统计模块,把碰到的问题和一些讨论记录下来,所以本文没有答案,没有解决方案,仅是讨论而已。

我要做什么?

我现在做的是一个 app 里面的用户行为统计,简单来说就是记录下用户从哪个页面跳转到哪个页面,在页面上都点击了哪些按钮,点击了几次等等之类的东西。

统计工具用的是现成的 Google Analytics,Flurry,MixPanel,我要做的就是将他们集成进我的统计模块代码,并进行各种业务事件的统计。

以 Flurry 为例,当点击登录按钮 clickShootButtonAction 被调用要做一条用户行为统计,统计代码是这样的

  1. - (IBAction)clickShootButtonAction
  2. {
  3. // 执行下面这句话,在 Flurry 的后台就能看到这个事件的记录
  4. [Flurry logEvent:@"点击拍照按钮"];
  5. }

或者

  1. - (IBAction)clickShootButtonAction
  2. {
  3. // 执行下面这句话,在 Flurry 的后台就能看到这个事件的记录
  4. // 与上面不同的是,这边记录的是用户的一条行为路径
  5. // 表示:用户拍照后,在照片分享页,将照片分享到了 Facebook
  6. [Flurry logEvent:@"保存分享照片" withParameters:{@"分享到":@"Facebook"}];
  7. }

而在应用里面记录上百个用户行为是很正常的事情。也就说类似上面的这种代码在 Controller 里面可能要出现几百次,还散落在各处。

这是正常的吗?

如果这些代码遍布我们的工程,使得统计模块和业务代码耦合度极高,造成剥离困难,无法重用等等各种的问题,写起来手累心也累。

对我们来说,最理想的情况下,Controller 里面这种代码越少越好,最好是一行都没有,包含个头文件就能自动统计那该多好。因为如果要剥离统计代码,或者更换统计方式,都是非常方便。

但实际情况不容乐观!!!

有解吗?

根据统计的事件,我们把需要统计的方法大致归类为以下三种,统计剥离的难度也逐级递增

那么问题来了,如果我们不希望在 Controller 里面直接添加统计代码,应该怎么统计上面的三种方法?

剥离统计 ViewController 生命周期的统计代码

这类方法的统计比较简单,写个 UIViewController 的 category,hook UIViewController 的中需要统计的方法,然后将头文件塞到要统计的 ViewController 即可。

剥离调用一次就统计的统计代码

这个统计代码的剥离比较麻烦,麻烦的点在于这些方法是根据业务逻辑产生的,每个 ViewController 中的方法都不一致,没法用统一的方式来处理。

关于这类代码的剥离,我查了一些资料,请教一些同学,又在一些技术群讨论了下,确实可以剥离,但方法都不太可靠,所以不建议使用,以下一一列举。

乍看之下,这个思路非常不错,写起来爽歪歪,要添加统计代码时,只要在配置文件中加一下就 OK,嗨到不行。但后来一想这种方式实际执行起来是会有问题的。

首先统计模块代码和业务代码是分散在两个地方,统计是根据具体的类名和方法名的字符串来 hook 后进行统计操作。因为统计模块比较独立,由一个单独的人来写,其他人去写业务代码。业务开发的同学随意修改个方法名还是比较正常的。当业务开发的同学改了个方法名,一般不会想到统计这边也要改;统计这边的同学也不知道业务的同学改了什么。所以这种只有调试时,碰到统计异常才会去检查这里,可维护性太差。

所以这种方法是写的人爽,维护的人非常非常的不爽!
所以这种不推荐使用!

所以这种也不推荐使用!

剥离满足条件才统计的统计代码

无解!

想过去真真是无解啊!因为我们前面所用的方法和思路基本离不开 AOP,而 AOP 本身是 hook 一整个方法,在方法前后添加一些自定义的操作。AOP 是没有办法了解方法内部是什么样的,更何谈去统计方法内满足了一定条件再统计事件。

怎么办?

老老实实的将统计代码写到相关的方法里面吧,真没辙了!

最后的忧伤

折腾了这么些来回,结果还是没法将统计代码和业务代码分开。原以为统计模块应该是一个独立的模块,结果却捅到各个 Controller 代码里面去,实在令人忧伤。

那么到底是什么原因造成了这样一个结局?

回头看看,我们一开始就默认了统计模块是一个独立的模块,应该与业务逻辑分开来。但实际上统计是和业务紧密结合的一个模块,所以是不是可以这么想。统计模块的代码也属于业务逻辑呢?

不管怎样,这么想就可以心安理得的把统计代码写进 Controller 了~


Update:写这篇文章的目的,主要是记录下我的思考过程,虽然结局很忧伤。结果在 CocoaChina 看到有同学吐槽说写了这么多内容,却并没有什么卵用,那我就再更新点内容吧。


统计过程碰到的其他问题

研究完代码该怎么写之后,就是实际动手了。在实际写代码的过程中,还有个非常容易碰到的问题 —— 并不是所有的业务逻辑都能在业务代码里面统计到。

写业务逻辑代码的同学,其实是不需要关心统计内容。所以在写业务逻辑的类时,直接将一些行为封装在较为深层的类中,在 Controller 中是没法直接接触这些行为。

这对于明确 Controller 作为控制器的职责,减轻负担,以及深层类的封装性,这么做是相当合理的。但同时带来一个问题,如果要统计深层的用户行为,怎么办?

答案就是:将这些已封装好的类再暴露一些统计需要的接口出来。这在一定程度上破坏了原有类的封装性,但和直接在那些类里面直接写入统计代码相比,来的更加温柔。

所以回到上面的那个问题:统计模块到底是不是属于业务逻辑?我的回答是:统计模块是强烈依赖业务模块的非业务模块,但也可以理解成是一个针对公司内部需要的特殊业务模块。

写统计代码的原则

原则一:千万不要在非业务逻辑处写统计代码

统计代码是和业务逻辑紧密相关的,所以千万不能在非业务逻辑出写统计代码。最好是将统计代码写在 Controller 中,把统计代码写在 Controller 引用的 View 等其他地方都是不太好的做法。

原则二:千万不要在业务代码中引入新的统计相关的变量

尽管统计模块已经深入到各个 Controller,但是统计模块所需的一些变量应该由统计模块自身管理。写统计代码的同学千万不能在 Controller 中引入新的变量或属性,用于记录统计信息。可以建一个类 MCUserBehavior,将需要用的变量放这里,专门用于收集用户行为。

原则三:暂时没有了
有想到的同学可以帮忙补充,谢谢~

原则写起来可能还有一些,但总的一条就是:在需要统计业务的时候,引入一行统计模块的代码就可以做到记录用户行为。

我的统计代码是怎么写的

主要有四块:

这其中最主要的是 MCUserBehavior 类,下面举一个具体的例子说明

业务背景

一款图片处理软件,当用户拍照或者从相册选中一张图片后,进入处理图片的主页,底部有四个 Tabbar 菜单(磨皮、美白、丰胸、滤镜)

主页

用户点击其中任意一个菜单就会进入一个具体的图片处理页面
磨皮

用户对图片经过一系列的操作(磨皮、美白、丰胸、滤镜),回到主页,点击 [下一步] 进入照片分享页面,或者点击 [取消] 返回上级页面。用户不需要进行所有的图片处理操作,如只做了美白,也可以点击 [下一步] 和 [取消]。

需求:如果用户在主页点击 [下一步],则统计用户进行了哪些操作;点击 [取消] 就不统计。

[用户行为] - [用户进行了哪些操作] - [磨皮]
[用户行为] - [用户进行了哪些操作] - [美白]
[用户行为] - [用户进行了哪些操作] - [丰胸]
[用户行为] - [用户进行了哪些操作] - [滤镜]

现假设

  1. /**
  2. * MCUserBehavior.h
  3. *
  4. * MCUserBehavior 用户收集用户行为
  5. */
  6. // 用户行为所在的页面
  7. typedef NS_OPTIONS(NSInteger, MCUserBehaviorContext) {
  8. MCUserBehaviorContextViewA = 1 << 0, /**< A 页面的用户行为 >**/
  9. MCUserBehaviorContextViewB = 1 << 1, /**< B 页面的用户行为 >**/
  10. MCUserBehaviorContextViewC = 1 << 2, /**< C 页面的用户行为 >**/
  11. MCUserBehaviorContextViewD = 1 << 3, /**< D 页面的用户行为 >**/
  12. };
  13. // 统计用户使用了多少种图片处理方式
  14. @property (nonatomic, assign) MCUserBehaviorContext userBehaviorTypes;
  15. /**
  16. * AAAController.m
  17. *
  18. * 磨皮页面
  19. */
  20. // 磨皮方法
  21. - (void)method1111
  22. {
  23. [[MCUserBehavior shareInstance].userBehaviorTypes = 1 << MCUserBehaviorContextViewA];
  24. }
  25. /**
  26. * BBBController.m
  27. *
  28. * 美白页面
  29. */
  30. // 美白方法
  31. - (void)method2222
  32. {
  33. [[MCUserBehavior shareInstance].userBehaviorTypes = 1 << MCUserBehaviorContextViewB];
  34. }
  35. /**
  36. * CCCController.m
  37. *
  38. * 丰胸页面
  39. */
  40. // 丰胸方法
  41. - (void)method3333
  42. {
  43. [[MCUserBehavior shareInstance].userBehaviorTypes = 1 << MCUserBehaviorContextViewC];
  44. }
  45. /**
  46. * DDDController.m
  47. *
  48. * 滤镜页面
  49. */
  50. // 滤镜方法
  51. - (void)method4444
  52. {
  53. [[MCUserBehavior shareInstance].userBehaviorTypes = 1 << MCUserBehaviorContextViewD];
  54. }
  55. /**
  56. * HHHController.m
  57. *
  58. * 主页面
  59. */
  60. // 点击 [取消],重置用户行为
  61. - (void)method6666
  62. {
  63. [[MCUserBehavior shareInstance].userBehaviorTypes = 0;
  64. }
  65. // 点击 [下一步]
  66. - (void)method5555
  67. {
  68. // recordProcessImageEvent 方法内记录用户用户后,也会重置用户行为
  69. [MCMixpanel recordProcessImageEvent:[[MCUserBehavior shareInstance].userBehaviorTypes];
  70. }

代码中的 MCUserBehaviorContext 类型的用处很大,可以用在多个地方,比如在各个处理页面(如磨皮页面)都有个 [对比] 按钮,如果要用统计各个页面是否使用了对比按钮,也可以复用这个枚举类型:

  1. // 记录用户是否点击了对比按钮
  2. @property (nonatomic, assign) MCUserBehaviorContext userBehaviorCompare;

设计跟着具体业务走的,且我技术水平还是处在较为初级的阶段,所以我的类划分和头文件划分,在其他项目中可能就不太合适了。此处只是提供一个参考和思路,希望能对各位同学有帮助!

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注