前言

打点统计在一个成熟的 App 中不可或缺,短期它可以让策划提的新 case 得到验证,长期来看也会给运营的方向起一个指导性的作用,但作为开发来说,这可以说是最没有技术含量的工作了,开发过程往往是在封装的 SDK 上将一堆重复的业务统计代码 ctrl-c/ctrl-v ,然后修改相应的参数,测试就更痛苦了,黑盒测试一共几十个用例,每个都需要不断的让 App 退到后台/进入前台并且开着 Charles 验证上传日志是否正确。细细想来,这两个过程都有值得优化的地方。

AOP 打点

先说一下我们这边的业务需求,打点分为两种,一种是统计课件学习时长,另一种是统计页面的学习时长。由于课件学习需要打点的视图较少,而且依赖的逻辑较长较特殊,比如视频播放暂停和PDF的打点时机完全不同,所以特殊情况特殊处理。但是页面学习时长的统计逻辑基本一致,特殊的打点时机也可以在框架层面做到兼容,为了避免在20多个类中反复写同一套代码,我采用面向切面编程的思路把通用的打点逻辑分离出来,统一集中到一个地方处理。在 OC 中可以直接使用 method swizzling 实现,以下是采用 AOP 前后的代码:

AOP 前需要在每个 Controller 中调用重复的添加通知、设置开始时间、记录时长、发送统计数据等模板代码

1
2
3
4
5
6
7
8
9
10
int64_t startLearnTime_;
- viewDidLoad
- viewWillAppear:
- viewWillDisappear:
- dealloc
- addNotification
- resetStartTime
- sendStatisticsData
- applicationWillResignActive
- applicationDidBecomeActive

采用 AOP 将 Controller 的四个生命周期方法 hook 掉之后,如果某个页面需要打点统计的功能,只需要实现 XYZStatisticsManagerDataSource 这个协议就可以获取全部打点的功能,由于我方法替换的内部实现判断了是否实现协议,所以 AOP 的方案不会造成其他无需打点的页面多打点的现象,其所有的逻辑全部集中到一个地方(用 category 实现),简单干净。调用方只需要给出四个打点的数据即可

1
2
3
4
5
6
@protocol XYZStatisticsManagerDataSource <NSObject>
@property XYZLearnType statLearnType;
@property int64_t statCourseId;
@property int64_t statTermId;
@property XYZStatisticsTrackSceneType statTrackSceneType; // 这个用于判断是 viewWillDisappear 还是 dealloc 时上报数据
@end

单元测试

把大量重复低质难以维护的代码分离出来之后,怎么保证这次重构有没有对当前业务的稳定性造成影响,怎么保证在下一个迭代和重构中不会出现漏打点/多打点的情况?虽然单元测试不是银弹,但当 Test Succeeded 的 toast 弹出时,相信你对这次重构和代码质量的信心又能增加好几倍。

由于我本身对自动化测试也不是很熟悉,如果操作姿势不太正确或者单测覆盖率不完整还请批评指正。

测试第一步,在程序运行时扫描整个注册表,获取所有类的信息并存到数组中;

1
2
3
4
unsigned int classCount;
Dl_info info;
dladdr(&_mh_execute_header, &info); // 获取app的路径
const char **classes = objc_copyClassNamesForImage(info.dli_fname, &classCount);

第二步,在内存中存放一份需要打点的 Controller 数组备用

1
NSArray *autoPageStatsVCs = @[...];

第三步,过滤出实现了 XYZStatisticsManagerDataSource 协议的 VC

1
2
Class class = NSClassFromString(className);
[class conformsToProtocol:NSProtocolFromString(@"XYZStatisticsManagerDataSource")]

第四步,与内存存放的自动打点的 VC 数组比较,看看有没有遗漏或多打

1
2
XCTAssertEqual(autoPageStatsVCs.count, classNames.count, @"自动打点的VC数量应该与定义的VC数量相等");
XCTAssertEqualObjects(autoPageStatsVCs, classNames, @"自动打点的每个VC应该与定义的每个VC相等");

第五步,由于 OC 是动态类型语言,实现了协议不代表协议中每个方法都有实现,所以需要判断打点的 Controller 是否都能响应每个方法

1
XCTAssertTrue(class_respondsToSelector(NSClassFromString(vc), NSSelectorFromString(property)), @"自动打点的VC: %@应该响应%@方法", vc, property);

经过上述五步,不能说用例完全覆盖,因为没有涉及到底层的网络请求测试,但是经此一役,整个打点统计的代码质量有了坚固的保障,作为开发终于可以大胆重构任何看不顺眼的页面,总体而言,单测在整个开发编码过程中是十分有必要的。

技术优化归优化,迭代过程中也不可能完全抛弃已有业务需求和陈旧代码的包袱,只能做到满足需求,按需重构。在小步快跑,快速试错的敏捷型互联网产品的发展过程中,技术不应成为瓶颈,但想在业内混,技术债总归是要还的。


阅读