打造基于Clang LibTooling的iOS自动打点系统CLAS(三)
1. 源码变换
第一章我们提到过,CLAS的本质是对源码做一次非常简单的变换(有些文章里称作变形),即Source-Source-Transformation,将打点代码精确地插入到目标函数的首部,保存到临时文件,代替原始文件传递到Clang进行编译。这个变换过程对于Clang的编译流程没有侵入,保证了与不同版本Clang一定的兼容性,即使Clang进行小版本升级CLAS仍然可以正常工作无需重新编译(例如Xcode从8.2.1升级为8.3.3)。围绕着源码变换可以做出许多非常有创意的工具,大家有兴趣可以深入研究这个话题,我们在这里就不展开了。
Clang提供给我们了一个非常好用的类clang::Rewriter
用于源码变换。如果你熟悉Clang可能会知道有一个大名鼎鼎的编译选项-rewrite-objc
,这个选项可以帮助你将OC代码重写成C++代码,很多对于OC内部运行机制的窥视和分析都是基于这个选项得来的,而它就是基于我们第二章所讲的ASTConsumer以及本章所讲的Rewriter构建出来的。
细看Rewriter的接口会发现,它满足了CLAS对源码内容增删改查的全部需求。例如你可以通过Rewriter向源码内指定位置插入删除任意长度的代码,然后将修改后的内容保存到一个临时文件中。Rewriter的接口在Clang的模块里可以算得上是超级简单易用的了,方法的含义根据方法名就一目了然,而且不需要复杂的上下文参数传递。编译器这种动辄几十人持续很多年维护同一个工程的代码,想要很容易地看懂里面任何一个功能都不是那么简单的事情,Rewriter算得上是Clang里面的异类。
2. 插入代码
既然大致了解了Rewriter,接下来我们就要开始真正的插入代码了。假设我们需要在每个方法的开始加入这么一句话,让每次方法执行时打印出被调用的方法名:
{ NSLog(@"进入方法:%__FUNCNAME__%"); }
第一个问题马上就出现了,插入的代码是预先定义好的,如何能够根据不同的方法名插入不同的代码呢?这个问题很好解决,我们需要定义一些CLAS变量,以%包围,例如上面的%__FUNCNAME__%。在遍历到每一个方法准备插入代码的之前,将%__FUNCNAME__%替换为当前的方法名即可。至于定义哪些变量取决于工具的需要。正式的CLAS系统我们只需要有限的几个变量即可(例如__FUNCNAME__, __CLASSNAME__,__CATEGORYNAME__等),因为需要插入的代码按照第二章的要求都应该是尽可能自包含的静态代码,不需要在插入代码的时候进行过多的人为干预。
在这里我们还要单独说明一下,插入的代码不要包含换行符和制表符等,因为这些符号,尤其是换行符会破坏源码的位置信息(SourceLocation),导致debug的时候指向错误的行数。无论再长的代码,都不要换行,当然避免插入过长的代码才是最好的。
我们把需要插入的代码保存到一个单独的文本文件里,然后让CLAS在启动的时候读取这个文件的内容到内存中,并在遍历到每一个OC方法的时候插入这段代码。至于如何将代码内容从文件中读入内存的细节不在本文讨论范围内,熟悉C++的你可以直接阅读CLAS源代码。我们打开ClangAutoStats.cpp,首先需要引入Rewriter的头文件:
#include "clang/Rewrite/Core/Rewriter.h"
然后我们需要定义一个Rewriter的静态变量:
static clang::Rewriter TheRewriter;
我们假设需要插入的代码片段已经从文件中读入内存,并存入静态变量:
static std::string CodeSnippet;
接下来我们在ClangAutoStatsVisitor的handleObjcMethDecl方法里加入如下代码:
CompoundStmt *cmpdStmt = MD->getCompoundBody();
SourceLocation loc = cmpdStmt->getLocStart(). getLocWithOffset(1);
if (loc.isMacroID()) {
loc = TheRewriter.getSourceMgr().getImmediateExpansion Range(loc).first;
}
ObjCMethodDecl有一个方法getCompoundBody,会返回当前方法的复合语句节点(Compound Statement)。在AST里,每一条语句(Statement)都是一个Stmt节点,而复合语句从Stmt继承而来,是包含有0至n个Stmt的容器型Stmt,复合语句也可以嵌套包含复合语句。If、For、Switch、While、do、以及OC方法都可以包含一个复合语句。我们插入代码的位置在方法的复合语句大括号后面,例如:
- (void)func {/*在这里插入代码,不会破坏debug信息*/
}
CompoundStmt的getLocStat方法可以返回复合语句的起始位置,这相当于是左大括号的位置,我们在这个位置的基础上再向后偏移1个字节指向大括号后面的位置。在上面的例子,这个位置会是回车‘\n’的位置(行级注释都不会出现在AST里面)。找到这个位置后我们还需要做一个额外的检查,看看这个复合语句是不是从宏定义展开而来的。如果是根据宏定义展开的复合语句,直接调用getLocStart方法会获得定义这个复合语句的宏定义的声明位置,那么我们计算的插入代码的位置就错了。正确的做法是调用SourceMgr的getImmediateExpansionRange方法获取这个复合语句的实际在源码内展开的位置。计算完毕后,我们要调用Rewriter的InsertTextBefore方法进行代码插入。在插入CodeSnippet之前,我们还需要把%__FUNCNAME__%替换为当前方法名(C++操作起字符串来真的是比OC费劲太多了...):
static std::string varName("%__FUNCNAME__%");
std::string funcName = MD->getDeclName().getAsString();
std::string codes(CodeSnippet);
size_t pos = 0;
while ((pos = codes.find(varName, pos)) != std::string::npos) {
codes.replace(pos, varName.length(), funcName);
pos += funcName.length();
}
TheRewriter.InsertTextBefore(loc, codes);
我们目前修改了Rewriter的内容,但并没有对源文件有任何影响,按照CLAS的设计要求,我们还需要将修改过后的文件内容保存至临时文件。这个我们选择在ClangAutoStatsAction里重写EndSourceFileAction方法,在这里面我们将Rewriter的内容保存至与原文件同名的.clas后缀的临时文件:
void EndSourceFileAction() override {
size_t pos = filePath.find_last_of(".");
if (pos != std::string::npos) {
ClasFilePath = filePath + ".clas";
}
std::ofstream clasFile(ClasFilePath);
assert(clasFile.is_open());
FileID fid = getCompilerInstance().getSourceManager(). getMainFileID();
RewriteBuffer &buffer = LogRewriter.getEditBuffer(fid);
RewriteBuffer::iterator I = buffer.begin();
RewriteBuffer::iterator E = buffer.end();
for (; I != E; I.MoveToNextPiece()) {
(clasFile << I.piece().str());
}
clasFile.flush();
clasFile.close();
}
3. Clang参数的裁剪和重排
上面的一节,我们基本完成了CLAS的框架结构,能够在OC方法最前面自动插入自定义代码,当然这种插入目前还是无差别的全量插入,肯定还需要根据需求进行针对性的打磨,这种精细化的定制需求就不在本文讨论范围内了,你可以根据这个框架继续改进代码。
接下来我们需要考虑的是如何应对Xcode传入的Clang指令及参数,以符合CLAS的需要。在前一章我们讨论过LibTooling的Fixed Compilation Database,它与Clang的参数形式并不直接兼容。CLAS被定义为一个类似Clang Wrapper的工具,为了避免过多的对编译工具链进行入侵,我们需要将Xcode传入的Clang指令进行精心地裁剪和重新排序,以便让CLAS可以正常工作。
举个很简单的例子,比如我们有一个HelloWorld.m的文件需要处理:
#import <Foundation/Foundation.h>
@interface HelloWorld : NSObject
@end
@implementation HelloWorld
- (void)sayHi:(NSString *)msg {
NSLog(@"Hello %@", msg);
}
@end
如果在Xcode里编译这个文件,查看Build Log会看到Xcode发出了如下指令及参数给Clang(略去了-W以及-I, -F,否则太长了):
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/clang -x objective-c -arch x86_64 -std=gnu99 -fobjc-arc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator10.3.sdk -mios-simulator-version-min=8.0 -c /Users/test/HelloWorld/HelloWorld.m -o /Users/test/HelloWorld/HelloWorld.o
如果调用CLAS,则参数列表需要转换为如下格式:
/usr/local/clas/bin/clas /Users/test/HelloWorld/HelloWorld.m -- -x objective-c -arch x86_64 -std=gnu99 -fobjc-arc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator10.3.sdk -mios-simulator-version-min=8.0 -F/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator10.3.sdk/System/Library/Frameworks -I/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include/c++/v1 -I/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk/usr/include -o /Users/test/HelloWorld/HelloWorld.o
我们可以看到,HelloWorld.m被移到了第二位,后面紧跟了"--"参数,表明后面跟随的都是Clang所需的参数。这些参数多了一个-F和两个-I,分别指向了ios的系统Frameworks目录,以及include目录。之所以我们需要添加这三个参数,是因为苹果的Clang会默认加入对这些目录,而我们从源码编译的LibTooling的工具却不会,如果不添加这些参数会导致LibTooling分析文件的时候因为找不到各种系统头文件而失败。这就是参数裁剪重排的意义。CLAS执行完成后,还有一个非常重要的任务,就是将原文件.m重命名后,将CLAS输出的临时文件重命名为原文件,拼接剩余参数并调用苹果原生的Clang(/usr/bin/clang),clang执行完成后,无论成功与否,将临时文件删除并将原文件.m复原,编译流程至此结束。
如果你熟悉C/C++,这些代码可以在CLAS里完成而保证最高的执行效率,如果不熟悉上面提到的操作完全可以通过脚本来完成,脚本拦截Xcode发出的编译指令,处理参数后传递给CLAS,CLAS处理完成后,在脚本里继续执行苹果的Clang。这里我们就不对这些做详细描述了,如果有兴趣可以直接研究CLAS源码。
4. 最后
到了这里,我们已经构建了一个简单的基于Clang LibTooling的编译前端工具,可以解析AST,并在指定位置插入自定义代码。本文并没有覆盖正式项目所具有的实用性功能,例如针对性的代码插入、灵活的功能配置(例如通过配置文件)等。我们会在接下来的文章里介绍针对性的代码插入以及如果将CLAS集成到Xcode编译链中,敬请期待...
打造基于Clang LibTooling的iOS自动打点系统CLAS(三)的更多相关文章
- 打造基于Clang LibTooling的iOS自动打点系统CLAS(一)
1. 手动打点的弊端 在很多ios工程师的日常工作中,不但要对接产品提出的功能性需求,还会收到产品出于数据统计分析需求目的而提出的附带的隐形需求:统计打点.大多数公司的基础框架层都会对统计打点功能做高 ...
- 打造基于Clang LibTooling的iOS自动打点系统CLAS(二)
1. 配置LLVM和Clang 在这篇文章里,我们会基于上一篇所述的方案进行展开,详细讲解如何从0开始创建一个基于Clang LibTooling的编译器前端工具.在开始之前,我们假设你已经基本了解何 ...
- 建立apk定时自动打包系统第三篇——代码自动更新、APP自动打包系统
我们的思路是每天下班后团队各成员在指定的时间(例如下午18:30)之前把各自的代码上传到SVN,然后服务器在指定的时间(例如下午18:30)更新代码.执行ant 打包命令.最后将apk包存放在指定目录 ...
- 基于模板特化的Lua自动绑定系统
LuaBind http://www.rasterbar.com/products/luabind.html http://blog.sina.com.cn/s/blog_646817c00100gk ...
- 翻译:打造基于Sublime Text 3的全能python开发环境
原文地址:https://realpython.com/blog/python/setting-up-sublime-text-3-for-full-stack-python-development/ ...
- 40、IOS自动打包-Python脚本
第一种:基于编译的打包 编译工程--找到.app文件--新建Payload文件夹--拷贝.app到Payload文件夹--压缩成zip--更改后缀名为ipa--完成! 第二种(有问题,暂时不需要看) ...
- 【原创】打造基于Dapper的数据访问层
[原创]打造基于Dapper的数据访问层 前言 闲来无事,花几天功夫将之前项目里用到的一个数据访问层整理了出来.实现单个实体的增删改查,可执行存储过程,可输出返回参数,查询结果集可根据实际情况返回 ...
- 自己动手打造基于 WKWebView 的混合开发框架(一)WKWebView 上手
http://www.cocoachina.com/ios/20150911/13301.html 代码示例:https://github.com/johnlui/Swift-On-iOS/tree/ ...
- 打造更好用的 EF 自动审计
打造更好用的 EF 自动审计 Intro 上次基于 EF Core 实现了一个自动审计的功能,详细可以参考 https://www.cnblogs.com/weihanli/p/auto-audit- ...
随机推荐
- 1分钟选好最合适你的JavaScript框架
欢迎大家持续关注葡萄城控件技术团队博客,更多更好的原创文章尽在这里~~ Javascript框架(以下简称框架)也被称为Javascript库,是一组包含丰富功能和函数的JavaScript代码集, ...
- Hibernate--inverse属性与cascade属性
转载:http://www.cnblogs.com/otomedaybreak/archive/2012/01/17/2324772.html Hibernate 集合映射中,经常会使用到" ...
- 亚马逊AWS EC2云实例AMI安装LNMP环境(2)——PHP5.6
概括:这里选择亚马逊EC2的Linux AMI实例,该Linux服务器是亚马逊预配置的Linux环境,内置多个YUM源,属于亚马逊首推的稳定Linux服务器.默认登录用户名为ec2-user,执行ro ...
- 网站相关人员信息记录humans.txt
前面的话 robots.txt文件告诉搜索引擎哪些页面可以抓取,哪些页面不能抓取.而humans.txt文件则是为人类准备的,包含参加该网页设计和建立的相关人员的信息.本文将详细介绍humans.tx ...
- git push解决办法: ! [remote rejected] master -> master (pre-receive hook declined)
前天准备上传一个project到GitLab上,但是试了很多次都上传不上去,报错如下: ! [remote rejected] master -> master (pre-receive hoo ...
- 谈一谈EasyUI的TreeGrid的过滤功能
写在最前面 这个星期一直在纠结easyui的treegrid的过滤功能,原因呢,自然是项目中一个莫名奇妙的需求. easyui虽说是后端程序员的前端框架,但是说句实话,除去api,让我直接写里面的节点 ...
- cacti监控部署与配置
cacti是一套基于PHP,mysql,SNMP及RRDTool开发的网络流量测试图形分析工具 cacti是通过snmpget来获取数据,使用RRDtool绘画图形 ,而且完全可以不需要了解RRDto ...
- hdu--2570--迷瘴
#include<iostream> #include<vector> #include<algorithm> using namespace std; int m ...
- vue指令v-once示例解析
只渲染元素和组件一次.随后的重新渲染,元素/组件及其所有的子节点将被视为静态内容并跳过.这可以用于优化更新性能. <!-- 单个元素 --> <span v-once>This ...
- SELECT中的if_case流程函数
DQL中常用的流程函数if_case ---流程函数在一个SQL语句中实现条件选择 模拟对职员薪水进行分类: mysql> create table salary_tab(userid ,)); ...