打造基于Clang LibTooling的iOS自动打点系统CLAS(一)

5 篇文章 0 订阅
3 篇文章 0 订阅

1. 手动打点的弊端

在很多ios工程师的日常工作中,不但要对接产品提出的功能性需求,还会收到产品出于数据统计分析需求目的而提出的附带的隐形需求:统计打点。大多数公司的基础框架层都会对统计打点功能做高级封装,工程师只需要在某个操作被触发的时候在处理的方法内加入一行函数调用即可完成,例如:

- (void)btnCloseClicked:(id)sender {
    [MCCStatistic logEvent:@"详情页-关闭按钮-点击"];
    [self.navigationController popViewController];
}

这个看起来简单无比的工作,实际上做起来却是无聊透顶,而且极易出错。经常会出现App上线后发现有些统计点没有打上、打错了地方或者打出了错别字。漏打点会造成数据分析失真,而打错点则不但会造成数据失真还会造成数据永久污染,而这些错误都需要重新发版才能解决。然而就算重新发版,仍然会有一定比例的用户不升级,由此造成在几个月甚至长达半年的时间里持续性地对数据分析的准确性产生影响。

这种手动方式还存在另外一个更严重的问题:打点的代码散落分布在整个工程内,使得打点数据维护起来异常困难。比如如果想知道当前项目内实际打点情况的汇总,与产品的打点列表进行交叉比对,看是否有遗漏、不一致或者可以删除的统计点,这对于工程师来说就是个异常头疼的难题了。大的项目经常不是由一两个工程师维护的,各个不同的模块由不同的团队分别负责,汇总这些打点会浪费工程师大量的时间和精力,而且仍然不能保证完全准确,因为按照上面的方法,如果想找出项目内所有的打点,只能靠搜索打点函数调用的方式去进行,一个工程可能会有几千个统计点分布在上千个文件内,稍不留神就会遗漏或出错,由此产生的汇总的可信度也会大打折扣。

2. 手动打点的改进

可能有些同学会想到用切面编程(AOP)的方式来处理打点。这种方案我在这里就不赘述了,网上有不少成熟的方案,有兴趣的同学可以参考。AOP方案功能简单强大,能够将散落的打点代码聚合在一起方便维护,但是对于遗忘打点或者打点错误这种情况除了重新发版依然束手无策,毕竟打点的文案是在编译前就确定的。当然这个问题也可以解决。把需要打点的文案全部集中在一个配置文件里,然后给每一个统计点起一个独一无二的常量名字,在AOP的代码里只需要以查表的方式获取真正的文案,这样配置文件便可以从线上进行热更新。这种方案看起来很美好,然而操作起来一样很烦人。给每一个页面或者事件起个名字一样是个耗时耗力无聊透顶的工作,同样也容易出错,这看起来和直接埋点没有什么本质区别,唯一的优点就是可以把项目或者模块内所有的点集中到一起来维护更直观些而已。

3. 自动打点方案C.L.A.S.

虽然上面的方案并不完美,但是它给了我们很大的启发:有没有办法自动为每个点击事件生成一个独一无二的名字呢?这个问题看起来很难,但是可以换一种思考方式,如果我们有办法为特定的方法自动生成一个名字并插入打点代码,那刚才的AOP方案就已经很好了。为此我们就需要借助Clang所提供的黑科技了。为了避免最终方案过度复杂,我们在这里进行了一些条件限定:

  1. 适应项目内80%的打点需求
  2. 对现有代码逻辑无侵入
  3. 对现有编译工具链无侵入

最初,我们曾经考虑过直接创建一个Clang的Plugin,在内存中对AST直接进行修改,达到动态插入代码的目的。但是这条路困难重重,AST在Clang官方的定义是经过语法分析器处理后的不可变(immutable)的结果,动态修改AST会造成SourceLocation错乱,最终导致Codegen在生成IR的时候崩溃。我们在尝试了数次之后最终放弃了这个看似“直接”的想法。

经过一番权衡后,我们确定了基于Clang的LibTooling创建的前端工具对OC源代码进行分析和插入的方案,将结果写入中间文件再发送给Clang进行编译。这个方案我们后面称作CLAS,可以由下图描述:

enter image description here

输入的.m文件经由CLAS分析和重写到临时文件,再传入Clang进行正常的编译流程。因为所有对源代码的改动都发生在临时文件层面,源文件不会发生任何改动,同时我们也没有对Clang的编译过程做任何干预,所以这个方案可以理解为一个对OC源代码进行特殊预处理的Preprocessor。有了如何插入代码的工具,那么为每一个方法起一个响亮而唯一的名字就看起来很简单了。因为每遇到一个OC的方法,都可以使用OC的类名+扩展名(Category)+方法名(selector)的方式来获得一个唯一的标识,绝对不会重复,否则编译的时候Clang就会报语法错误。

插入的打点代码原则上要保证对性能尽可能小的损耗,全局会维护一张Hash表,用来维护名字 --- 打点文案之间的映射关系。这样做可以用尽可能小的内存大幅提高查询时间,因为绝大部分名字并没有对应的打点文案。这张Hash表由App内置一份,每次发版前由开发人员内置到Bundle内,同时每次App启动也会尝试更新这张Hash表支持动态更新映射关系。而插入代码的具体位置,定位在方法的左大括号后面,与大括号保持同一行,并使用{}进行包围。这样可以保证不破坏下面所提的Debug信息的行数,避免需要重新生成Debug信息的工作量。例如:

- (int)calWithA:(int)a andB:(int)b {     {/*插入代码的位置*/}
    a = a * 2 + b - 3;
    return a;
}

这个方案最大的难题在于在哪些方法上插入代码。全量插入当然是最简单粗暴的方法,将项目内的.m文件内所有方法全部打点。这样做好处很明显,如果漏打了哪个方法,可以通过线上更新的方式补打,但是同时这样做的坏处也很明显,很多方法永远不会需要打点却被插入了一段毫无用处的代码影响执行效率,因为被插入代码的方法,每次执行时都要先去查表看看当前的名字是否有映射的打点文案,如果有则发送打点,否则忽略,虽然查Hash表理论上是个很快的操作,但是如果发生在一些频繁调用的方法上依然会对系统性能产生负面的影响。为了避免这个问题,我们可以规定需要打点的方法只能出现在ViewController、View以及Manager(如果你用MVVM也可以是ViewModel)里面,并且排除不太可能需要打点的方法(例如ViewWillLayoutSubviews等),这种规范可以通过代码审核来约束工程师。当然命名规范本来也应该在成熟的项目内强制实施,保证代码可读性和质量。如果有些方法写在了ViewController里面却被频繁调用并且不需要打点,为了不影响性能,可以在方法起始处通过指定__attribute__((clas_ignore))属性进行强制跳过。这种方式与Clang的__attribute__((always_inline))相似。例如:

__attribute__((clas_ignore))
- (void)func {
    ....
}

有了这些我们可以大幅缩小插入代码的范围,减少插入代码对App性能所造成的影响。

4. CLAS的缺点

就像任何一种方案都有缺点一样,CLAS也存在着一些明显的缺点:

无法适用于条件打点
插入的代码可能会造成编译失败
插入范围过大
编译出的文件包含与源文件不符的Debug信息
插入代码导致二进制体积变大

条件打点一般会出现在逻辑复杂或者内容动态的界面上,比如一个按钮的点击事件,在某些情况下是A,另外一些情况是B,又或者打点的触发取决于当时场景的条件判断,这样动态变化的打点是无法通过CLAS来完成的。打点的事件不跟随条件变化的打点我们称之为静态打点。App内大约80%的打点的场景是属于这种静态打点的场景,CLAS也是为静态打点设计和服务的。

插入范围过大我们在3里面已经讨论过了,并有了一些优化的方法。插入代码可能造成编译失败是因为插入的代码可能需要引用一些在当前.m文件里没有引用的其他头文件导致编译过程失败,这个可以通过配置CLAS插入用户指定的#include#import来解决。Debug信息不符的问题比较棘手,因为.m被修改成临时文件并通过Clang编译出.o文件,生成的Debug Symbols是与临时文件(.clas.m)的信息相符的,与源文件并不相符,这个就需要我们在生成dSYMs的时候,把所有的临时文件信息替换为原始文件信息,为了达到这个目的,我们需要修改LLVM的dsymutil替换系统原生的dsymutil。我们会在接下来的文章里详细讲解我们如何构建一套完整的CLAS工具链的。

因为插入了大量代码,编译后的二进制体积必然会有所增大,所以原则上插入的代码应该是功能内聚的,一到两条语句为佳,避免在插入代码里直接构造含有复杂逻辑和功能的语句。例如:

{ [MCStatistik logEvent:@"%__FUNC_NAME__%"]; }

这里出现了一个%__FUNC_NAME__%看似的怪异名字,这是CLAS所支持的变量替换,以%开始和结束,在插入代码的时候会自动替换为对应的值。例如%__FUNC_NAME__%在插入代码的时候会自动替换为当前插入位置的函数名。

待续…

在接下来的几篇文章里,我们会详细介绍如何从零开始一步一步地构建一个基于Clang LibTooling的编译器前端工具CLAS,敬请期待!

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值