打造基于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- ...
随机推荐
- [分享] 自动化测试与持续集成方案-- UI 检查
对于自动化测试中,UI 自动化测试估计是最有争议的,让人又爱又恨. UI 自动化做回归测试,可以省下很多人力.如果版本一直不稳定,投入跟产出不成比例的. 时机 一般是要版本稳定,界面改动不大.如果迭代 ...
- HDOJ2008-数值统计
Problem Description 统计给定的n个数中,负数.零和正数的个数. Input 输入数据有多组,每组占一行,每行的第一个数是整数n(n<100),表示需要统计的数值的个数,然 ...
- ML(1)--概念理解
机器是如何模拟人来学习的? 人: observations===>learning===>skill 人从出生开始经过大量的观察(也可能经过身边的的指导)进行学习然后得到相应的技能(比如 ...
- 一步一步学Vue(九)
接上篇,这次是真的接上篇,针对上篇未完成的部分,增加鉴权功能,开始之前,我们先要介绍一个新的知识,路由元数据. 在vue-router中,定义元数据的方式: const router = new Vu ...
- ASP.NET前台html页面AJAX提交数据后台ashx页面接收数据
摘要:最近在写网站,好不容易弄好了需求又变了,没错企业的门户网站硬要弄成后台管理系统一样,没办法作为小工的我只能默默的改.前台HTML页面需要提交数据到后台处理,又不能用form表单,于是乎研究了1天 ...
- CVE-2016-3714 - ImageMagick 命令执行
ImageMagick是一款使用量很广的图片处理程序,很多厂商都调用了这个程序进行图片处理,包括图片的伸缩.切割.水印.格式转换等等.但近来有研究者发现,当用户传入一个包含『畸形内容』的图片的时候,就 ...
- java亦或(^)
在java程序里面的异或用法: 相同输出0,不同输出1,例如: System.out.println(1^1); 输出0 System.out.println(1^2):输出3,因为最后2个低位都不一 ...
- 尝试在CentOS7.2上编译安装Swift
苹果提供 Ubuntu上构建Swift 的教程,通过这个教程我尝试使用CentOS7.2上玩儿一把.目前已经成功在CentOS7.2上班成功安装 swift 4.0 https://github.co ...
- POJ--1088--dp--滑雪
#include<iostream> using namespace std; ; }; }; int dp(int,int); int row,col; int main() { whi ...
- 【模板--完全背包】HDU--2602 Bone Collector
Problem Description Many years ago , in Teddy's hometown there was a man who was called "Bone C ...