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(三)的更多相关文章

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

    1. 手动打点的弊端 在很多ios工程师的日常工作中,不但要对接产品提出的功能性需求,还会收到产品出于数据统计分析需求目的而提出的附带的隐形需求:统计打点.大多数公司的基础框架层都会对统计打点功能做高 ...

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

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

  3. 建立apk定时自动打包系统第三篇——代码自动更新、APP自动打包系统

    我们的思路是每天下班后团队各成员在指定的时间(例如下午18:30)之前把各自的代码上传到SVN,然后服务器在指定的时间(例如下午18:30)更新代码.执行ant 打包命令.最后将apk包存放在指定目录 ...

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

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

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

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

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

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

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

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

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

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

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

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

随机推荐

  1. 聊一聊FE面试那些事

    聊一聊FE面试那些事 最近公司由于业务的扩展.技术的延伸需要招一批有能力的小伙伴加入,而我有幸担任"技术面试官"的角色前前后后面试了不下50多位候选人,如同见证了50多位前端开发者 ...

  2. Nlpir Parser敏感词搜索灵玖语义技术应用

    近年来随着网络技术的飞速发展和用户的剧烈增长,网络传输数据量越来越大,网络用语越来越趋于多样化.如何快速的屏蔽用户的不当言论.过滤用户发表内容中的非法词汇已成为关键词匹配领域的一项重大难题. 目前主要 ...

  3. 64位linux系统通过编译安装apache+…

    二.安装php 上传php压缩包 例如:php-5.2.3.tar.gz 移动 mv php-5.2.3.tar.gz /usr/local/src 进入 cd /usr/local/src 解压 t ...

  4. git远程仓库之添加远程库

    现在的情景是,你已经在本地创建了一个Git仓库后,又想在GitHub创建一个Git仓库,并且让这两个仓库进行远程同步,这样,GitHub上的仓库既可以作为备份,又可以让其他人通过该仓库来协作,真是一举 ...

  5. TFS build server搭建,搭建自动化构建服务器

    TFS build 服务器的搭建主要步骤如下: 一:环境准备: 新建一台build服务器 安装Visual Studio.主要目的是: a. 生成Build脚本所需要的build命令:b.与TFS组合 ...

  6. HPU--1280 Divisible

    题目描述 给定一个很大的整数,我想知道它能否被9整除. 输入 有t组测试数据,每组数据给定一个整数N不存在前导0.(1 <= t <= 20,1 <= N <= 10^200) ...

  7. 解决Nuget:https://api.nuget.org/v3/index.json 访问不了的问题

    最近在家中用使用VS编译项目时,Nuget包一直下载不了,直接在浏览器中访问https://api.nuget.org/v3/index.json ,浏览器也打不开网址.把https协议改成http协 ...

  8. HTML5 — Wed Storage简单示例

    一.Wed Storage 概述 Wed Storage功能:在Wed上储存数据的功能,这里的储存是针对客户端本地而言的. 具体分为两种: sessionStorage,将数据保存在session对象 ...

  9. 手工释放linux内存——/proc/sys/vm/drop_caches

    --手工释放linux内存——/proc/sys/vm/drop_caches 总有很多朋友对于Linux的内存管理有疑问,之前一篇日志似乎也没能清除大家的疑虑.而在新版核心中,似乎对这个问题提供了新 ...

  10. C#内存管理解析

    前言:对于很多的C#程序员来说,经常会很少去关注其内存的释放,他们认为C#带有强大的垃圾回收机制,所有不愿意去考虑这方面的事情,其实不尽然,很多时候我们都需要考虑C#内存的管理问题,否则会很容易造成内 ...