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,可以由下图描述:

输入的.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,敬请期待!

打造基于Clang LibTooling的iOS自动打点系统CLAS(一)的更多相关文章

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

    1. 源码变换 第一章我们提到过,CLAS的本质是对源码做一次非常简单的变换(有些文章里称作变形),即Source-Source-Transformation,将打点代码精确地插入到目标函数的首部,保 ...

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

    1. 配置LLVM和Clang 在这篇文章里,我们会基于上一篇所述的方案进行展开,详细讲解如何从0开始创建一个基于Clang LibTooling的编译器前端工具.在开始之前,我们假设你已经基本了解何 ...

  3. 基于模板特化的Lua自动绑定系统

    LuaBind http://www.rasterbar.com/products/luabind.html http://blog.sina.com.cn/s/blog_646817c00100gk ...

  4. 翻译:打造基于Sublime Text 3的全能python开发环境

    原文地址:https://realpython.com/blog/python/setting-up-sublime-text-3-for-full-stack-python-development/ ...

  5. 40、IOS自动打包-Python脚本

    第一种:基于编译的打包 编译工程--找到.app文件--新建Payload文件夹--拷贝.app到Payload文件夹--压缩成zip--更改后缀名为ipa--完成! 第二种(有问题,暂时不需要看) ...

  6. 【原创】打造基于Dapper的数据访问层

    [原创]打造基于Dapper的数据访问层   前言 闲来无事,花几天功夫将之前项目里用到的一个数据访问层整理了出来.实现单个实体的增删改查,可执行存储过程,可输出返回参数,查询结果集可根据实际情况返回 ...

  7. 自己动手打造基于 WKWebView 的混合开发框架(一)WKWebView 上手

    http://www.cocoachina.com/ios/20150911/13301.html 代码示例:https://github.com/johnlui/Swift-On-iOS/tree/ ...

  8. 打造更好用的 EF 自动审计

    打造更好用的 EF 自动审计 Intro 上次基于 EF Core 实现了一个自动审计的功能,详细可以参考 https://www.cnblogs.com/weihanli/p/auto-audit- ...

  9. 艺萌文件上传下载及自动更新系统(基于networkComms开源TCP通信框架)

    1.艺萌文件上传下载及自动更新系统,基于Winform技术,采用CS架构,开发工具为vs2010,.net2.0版本(可以很容易升级为3.5和4.0版本)开发语言c#. 本系统主要帮助客户学习基于TC ...

随机推荐

  1. Java 泛型在实际开发中的应用

    java泛型是对Java语言的类型系统的一种扩展,泛型的本质就是将所操作的数据类型参数化.下面我会由浅入深地介绍Java的泛型. 一:泛型出现的背景 在java代码里,你会经常发现类似下边的代码: p ...

  2. JQuery中实现Ajax

    简单小案例: $("input").click(function() { $.get("test.txt",function(data){ $("h1 ...

  3. es6知识总结--3

    es6知识总结--3 es6对咱们es3,es5的数据类型进行了升级下边说新APIs! js数据类型有Number.String .oject.Boolean.Null.Undefined六种数据类型 ...

  4. 【Xbox one S】开箱&开机&初入坑心得

    再来一发水贴,先上产品标准照镇贴: 前言 身为一个资深单机游戏玩家,常年混迹在PC平台,但内心深处一直对主机有种迷之向往,感觉那才是单机游戏的正处之地,坐沙发上拿着手柄对着电视跌宕起伏才是正确的游戏姿 ...

  5. Eclipse中常用快捷键

    Ctrl+C:复制. Ctrl+V:粘贴. Ctrl+X:剪切. Ctrl+S:保存. Ctrl+Z:撤销. Ctrl+A:全选. F3:快速定位光标位置的某个类.方法和属性. Ctrl+Q:跳到最后 ...

  6. 将git版本号编译进程序

    问题的提出 不管是什么版本管理工具,每一条提交记录都会有一个对应的版本号,一般是一个整数,git是一个hash字符串.不管怎样,这个版本号是唯一的,有时候我们在程序运行的时候会在日志里面输出程序的版本 ...

  7. Spark笔记——技术点汇总

    目录 概况 手工搭建集群 引言 安装Scala 配置文件 启动与测试 应用部署 部署架构 应用程序部署 核心原理 RDD概念 RDD核心组成 RDD依赖关系 DAG图 RDD故障恢复机制 Standa ...

  8. Sqlserver事务备份和还原实例

    create database mydb use mydb go create table account( id ), name ), balance float ) go select * fro ...

  9. mysql浅龟定

    一,尽量不字啊数据库做运算 1,尽量不再数据库做运算, 2,将复杂运算移动到cpu 3,尽可能简单应用mysql 二,控制表数据量 1,纯 int不超过1000w 2,含char不超过500w 3,建 ...

  10. js中常用的日期总结

    js开发中经常用到日期,这里总结一下: /** * 获取当前日期 * 格式:2017-07-31 13:45:14 */ function getNowFormatDate() { var date ...