1. LLVM概述
LLVM是架构编译器(compiler)的框架系统,以C++编写而成。用于优化以任意程序语言编写的程序的编译时间(compile-time)、链接时间(link-time)、运行时间(run-time)以及空闲时间(idle-time),对开发者保持开放,并兼任已有脚本
LLVM计划启动于2000年,最初由美国UIUC大学的Chris Lattner博士主持开展。2006年Chris Lattner加盟Apple Inc,并致力于LLVM在Apple开放体系中的应用
Apple也是LLVM计划的主要自助者
目前LLVM已经被苹果iOS开发工具、Xilinx Vivado、Facebook、Google等各大公司采用
1.1 传统编译器设计

源码(
Source Code),经过编译器前端(Frontend)→优化器(Optimizer)→编译器后端(Backend),生成机器代码(Machine Code)机器代码(
Machine Code):就是CPU可执行的二进制代码从源码到机器码的生成,这个过程都是编译器负责完成的
1.2 iOS的编译器架构
Objective-C、C、C++使用的编译器前端是Clang,Swift使用的编译器前端是swiftc,而它们使用的编译器后端都是LLVM
各个模块的职责:
编译器前端(
Frontend)编译器前端的任务是解析源代码。它会进行:词法分析、语法分析、语义分析,检查源代码是否存在错误,然后构建抽象语法树(
Abstract Syntax Tree, AST)LLVM的前端还会生成中间代码(intermediate representation, IR)
优化器(
Optimizer)- 优化器负责进行各种优化。改善代码的运行时间,例如:消除冗余计算等
后端(
Backend)/代码生成器(Code Generator)- 将代码映射到目标指令集。生成机器代码,并进行机器代码的相关优化
1.3 LLVM的设计
当编译器决定支持多种源语言或多种硬件框架时,LLVM最重要的地方就来了。其他的编译器如GCC,它方法非常成功,但由于它是作为整体应用程序设计的,因此它们的用途受到了很大的限制
LLVM设计的最重要方面是,使用通用的代码表示形式(IR),它是用来在编译器中表示代码的形式。所以LLVM可以为任何编程语言独立编写前端,并且可以为任意硬件架构独立编写后端
- 简单来说,
LLVM最大的优势,就是将编译器的前后端分离,从而提高可扩展性
1.4 Clang
Clang是LLVM项目中的一个子项目
它是基于LLVM架构的轻量级编译器,诞生之初为了替代GCC,提供更快的编译速度
它是负责编译C、C++、Objective-C语音的编译器,它属于整个LLVM架构中的编译器前端
对于开发者来说,研究Clang可以给我们带来很多好处
2. 编译流程
创建main.m文件,写入以下代码:
#import <stdio.h>int main(int argc, const char * argv[]) {return 0;}
通过命令,打印源码的编译阶段
clang -ccc-print-phases main.m-------------------------//输出以下内容:+- 0: input, "main.m", objective-c+- 1: preprocessor, {0}, objective-c-cpp-output+- 2: compiler, {1}, ir+- 3: backend, {2}, assembler+- 4: assembler, {3}, object+- 5: linker, {4}, image6: bind-arch, "x86_64", {5}, image
- 0:输入文件,找到源文件
- 1:预处理阶段,这个过程包括宏的替换,头文件的导入
- 2:编译阶段,进行词法分析、语法分析、检测语法是否正确,最终生成IR
- 3:后端,
LLVM会通过一个一个的Pass去优化,每个Pass做一些事情,最终生成汇编代码 - 4:生成
.o目标文件 - 5:链接,链接需要的动态库和静态库,生成可执行文件
- 6:通过不同的架构,生成对应的可执行文件
2.1 预处理阶段
预编译阶段:将宏和导入的头文件进行替换
打开main.m文件,写入以下代码:
#import <stdio.h>#define C 30int main(int argc, const char * argv[]) {int a = 10;int b = 20;printf("%d",a + b + C);return 0;}
通过命令,打印预处理阶段
clang -E main.m//可生成预处理后的文件//clang -E main.m >> main2.m-------------------------//输出以下内容:# 1 "main.m"# 1 "<built-in>" 1# 1 "<built-in>" 3# 379 "<built-in>" 3# 1 "<command line>" 1# 1 "<built-in>" 2# 1 "main.m" 2...typedef signed char __int8_t;typedef unsigned char __uint8_t;typedef short __int16_t;typedef unsigned short __uint16_t;typedef int __int32_t;typedef unsigned int __uint32_t;typedef long long __int64_t;typedef unsigned long long __uint64_t;typedef long __darwin_intptr_t;typedef unsigned int __darwin_natural_t;...int main(int argc, const char * argv[]) {int a = 10;int b = 20;printf("%d",a + b + 30);return 0;}
- 展开宏和
stdio头文件,main函数中原本+ C变为+ 30
使用define和typedef的区别:
define:宏定义,在预处理阶段会被替换- 可用来做代码混淆,将
App中核心代码,用系统相似的名称进行取别名,然后在预处理阶段就被替换,以此达到代码混淆的目的
- 可用来做代码混淆,将
typedef:对数据类型取别名,在预处理阶段不会被替换掉
2.2 编译阶段
编译阶段可划分为三个部分:
词法分析
语法分析
生成
IR中间代码
2.2.1 词法分析
预处理完成后,就会进行词法分析,这里会把代码切成一个个Token,例如:大小括号,等于号,还有字符串等
命令:
clang -fmodules -fsyntax-only -Xclang -dump-tokens main.m//指定sdk路径//clang -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator14.5.sdk -fmodules -fsyntax-only -Xclang -dump-tokens main.m
查看词法分析之后的结果:
2.2.2 语法分析
词法分析完成之后就是语法分析,它的任务是验证语法是否正确。在词法分析的基础上,将单词序列组合成各类语法短语,例如:“程序”,“语句”,“表达式”等,然后将所有节点组成抽象语法树(Abstract Syntax Tree, AST)。语法分析程序判断源程序在结构上是否正确
命令:
clang -fmodules -fsyntax-only -Xclang -ast-dump main.m
查看语法分析之后的结果:
重点关键字的介绍:
FunctionDecl:函数ParmVarDecl:参数CallExpr:函数调用BinaryOperator:运算符
2.2.3 生成IR中间代码
完成以上步骤后,就会开始生成IR中间代码,代码生成器(Code Generator)会将语法树自顶向下遍历,逐步翻译成LLVM IR
通过以下命令,可以生成.ll文件,查看IR代码:
clang -S -fobjc-arc -emit-llvm main.m
Objective-C代码,在这一步会进行Runtime的桥接:property合成,ARC处理等
查看IR中间代码:
IR基本语法介绍:
@:全局标示%:局部标示alloca:开辟空间align:内存对齐i32:32个bit,4字节store:写入内存load:读取数据call:调用函数ret:返回
2.2.4 IR的优化
在Xcode中,找到Target→Build Setting→Optimization Level,可以对当前项目设置优化等级
在LLVM中,优化级别分别是-O0、-O1、-O2、-O3、-Os(第一个是大写英文字母O)
通过以下命令,可设置优化等级,并生成IR代码:
clang -Os -S -fobjc-arc -emit-llvm main.m -o main.ll
查看优化后的IR代码:
main函数中的代码优化的非常简短,直接计算出结果并返回
2.2.5 Bitcode
Xcode7以后,开启Bitcode设置,苹果会做进一步的优化,生成.bc中间代码
命令:
clang -emit-llvm -c main.ll -o main.bc
什么是Bitcode?
Bitcode是被编译程序的一种中间形式的代码。包含Bitcode并上传到App Store Connect的App,会在App Store上编译和链接。包含Bitcode可以在不提交新版本App的情况下,允许Apple在将来的时候再次优化你的App二进制文件
在Xcode中,默认开启Bitcode设置。如果你的App支持Bitcode,App使用到的其他二进制形式也要支持Bitcode,否则就会报错
解决Bitcode报错只有两种方案:
【方案一】将不支持
Bitcode的SDK移除掉,或等待第三方更新【方案二】:将使用
Bitcode的选项设置为NO
2.3 生成汇编代码
通过最终的.ll或.bc代码,生成汇编代码
命令:
clang -S -fobjc-arc main.ll -o main.sclang -S -fobjc-arc main.bc -o main.s
查看汇编代码:
汇编代码也可以设置OPT的优化等级进行优化
clang -Os -S -fobjc-arc main.ll -o main.s
查看优化后的汇编代码:
2.4 生成目标文件(汇编器)
目标文件的生成,是汇编器以汇编代码作为输入,将汇编代码转换为机器代码,最后输出目标文件(object file)
命令:
clang -fmodules -c main.s -o main.o
通过nm命令,查看main.o中的符号:
xcrun nm -nm main.o-------------------------//输出以下内容:(undefined) external _printf0000000000000000 (__TEXT,__text) external _main
_printf函数,被标记为undefined externalundefined:表示在当前文件中,暂时找不到符号。因为printf为外部函数,链接后才能找到符号所属动态库external:表示这个符号在外部是可以被访问的
2.5 生成可执行文件(链接)
链接:将多个目标文件合并,符号表(包括重定位符号表)合并成一张表,经过链接最后,会分配虚拟内存地址,最终生成可执行文件或动态库
这个过程还会链接需要的动态库和静态库
静态库,和可执行文件合并
动态库,独立存在,运行时,由
dyld动态加载
使用以下命令,生成可执行文件:
clang main.o -o main
查看链接后可执行文件的符号:
xcrun nm -nm main-------------------------//输出以下内容:(undefined) external _printf (from libSystem)(undefined) external dyld_stub_binder (from libSystem)0000000100000000 (__TEXT,__text) [referenced dynamically] external __mh_execute_header0000000100003f77 (__TEXT,__text) external _main0000000100008008 (__DATA,__data) non-external __dyld_private
链接后,
_printf符号可以找到所属的动态库,但依然被标记为undefined。因为libSystem属于系统动态库,在运行时进行动态绑定链接后,还多了
dyld_stub_binder符号,它在运行时用于符号的重绑定以
printf函数为例,printf函数存在于libSystem系统库中,它存在于懒加载符号表中。它的函数地址在运行时,首次对printf函数进行调用,才会通过dyld_stub_binder进行重绑定而
dyld_stub_binder函数地址的绑定时机:当dyld加载主程序时,符号被dyld直接绑定
3. Clang插件
编写一个Clang插件,实现效果:定义NSString、NSArray、NSDictionary类型的属性,未使用copy修饰,对该属性提示警告
3.1 下载LLVM
由于国内的网络限制,需要借助镜像下载LLVM的源码:https://mirror.tuna.tsinghua.edu.cn/help/llvm/
下载LLVM项目
git clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm/llvm.git
在LLVM的tools目录下,下载Clang
cd llvm/toolsgit clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm/clang.git
在LLVM的projects目录下,下载compiler-rt、libcxx、libcxxabi
cd ../projectsgit clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm/compiler-rt.gitgit clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm/libcxx.gitgit clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm/libcxxabi.git
在Clang的tools下,安装extra工具
cd ../tools/clang/toolsgit clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm/clang-tools-extra.git
3.2 安装cmake
使用brew命令,查看是否安装cmake,如果已安装,跳过此步骤
brew list
通过brew安装cmake
brew install cmake
3.3 编译LLVM
3.3.1 通过Xcode编译LLVM
cmake编译成Xcode项目
mkdir build_xcodecd build_xcodecmake -G Xcode ../llvm
使用Xcode编译Clang
选择手动管理Schemes
点击左下⻆加号,在Target中添加clang和clangTooling
通过Run Without Building运⾏,代码没有改变的时候,不需要重新编译,直接运⾏现有可执⾏⽂件即可
3.3.2 通过ninja编译LLVM
安装ninja
brew install ninja
在LLVM源码根目录下,新建一个build_ninja目录,最终会在build_ninja目录下生成build.ninja
在LLVM源码根目录下,新建一个llvm_release目录,最终编译文件会在llvm_release文件夹路径下
cd llvm_buildcmake -G Ninja ../llvm -DCMAKE_INSTALL_PREFIX=安装路径
- 本机为
/Users/xxx/xxx/LLVM/llvm_release,注意DCMAKE_INSTALL_PREFIX后面不能有空格
依次执行编译、安装指令
ninjaninja install
3.4 创建插件
在/llvm/tools/clang/tools目录下,新建插件HKPlugin
修改/llvm/tools/clang/tools目录下的CMakeLists.txt文件
新增add_clang_subdirectory(HKPlugin)
在HKPlugin目录下,新建HKPlugi.cpp和CMakeLists.txt文件
打开CMakeLists.txt文件,写入以下内容:
add_llvm_library( HKPlugin MODULE BUILDTREE_ONLYHKPlugin.cpp)
利用cmake重新生成Xcode项目,在build_xcode目录中执行cmake命令
cmake -G Xcode ../llvm
最后,可以在LLVM的Xcode项目中,在Loadable modules目录下找到自定义Plugin目录
- 打开
HKPlugi.cpp文件,可以在里面编写插件代码
3.5 编写插件代码
3.5.1 文件和顶级节点的解析
导入插件使用的头文件和命名空间
#include <iostream>#include "clang/AST/AST.h"#include "clang/AST/DeclObjC.h"#include "clang/AST/ASTConsumer.h"#include "clang/ASTMatchers/ASTMatchers.h"#include "clang/Frontend/CompilerInstance.h"#include "clang/ASTMatchers/ASTMatchFinder.h"#include "clang/Frontend/FrontendPluginRegistry.h"using namespace clang;using namespace std;using namespace llvm;
定义命名空间、定义HKASTAction类,继承自系统的PluginASTAction类
namespace HKPlugin {class HKASTAction:public PluginASTAction{};}
注册插件
static FrontendPluginRegistry::Add<HKPlugin::HKASTAction> X("HKPlugin","this is the description");
参数1:插件名称参数2:插件描述
现有的需求分为三个步骤:
【第一步】读取代码
【第二步】找到目标类型定义的属性和修饰符
【第三步】不符合标准,提示警告
实现需求的第一步读取代码,需要用到AST语法树,然后对AST节点进行解析
我们可以使用以下两个函数:
CreateASTConsumerParseArgs
在HKASTAction类中,重写CreateASTConsumer和ParseArgs函数
namespace HKPlugin {class HKASTAction:public PluginASTAction{public:std::unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &CI, StringRef InFile) {return unique_ptr<ASTConsumer> (new ASTConsumer);}bool ParseArgs(const CompilerInstance &CI, const std::vector<std::string> &arg) {return true;}};}
ASTConsumer是系统提供的基类,作为基类,它的作用大多有两种
抽取代码
由开发者继承,实现它的子类,对其进行扩展
所以,我们不能直接使用ASTConsumer,需要对其进行继承,实现自定义子类
namespace HKPlugin {class HKConsumer:public ASTConsumer{public:bool HandleTopLevelDecl(DeclGroupRef D) {cout<<"正在解析..."<<endl;return true;}void HandleTranslationUnit(ASTContext &Ctx) {cout<<"文件解析完成..."<<endl;}};class HKASTAction:public PluginASTAction{public:std::unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &CI, StringRef InFile) {return unique_ptr<HKConsumer> (new HKConsumer);}bool ParseArgs(const CompilerInstance &CI, const std::vector<std::string> &arg) {return true;}};}
重写HandleTopLevelDecl和HandleTranslationUnit函数
HandleTopLevelDecl:顶级节点解析回调函数,顶级节点,例如:全局变量、函数定义、属性HandleTranslationUnit:整个文件解析完成后的回调
编译HKPlugin项目,在项目的Products目录下,找到编译出的clang可执行文件
同样在Products目录下,找到HKPlugin.dylib
使用插件,测试文件和顶级节点的解析
创建hello.m文件,写入以下代码:
int sum(int a);int a;int sum(int a){int b = 10;return 10 + b;}int sum2(int a,int b){int c = 10;return a + b + c;}
使用以下命令,测试插件
//自己编译的clang路径 -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator14.5.sdk/ -Xclang -load -Xclang 插件(.dylib)路径 -Xclang -add-plugin -Xclang 插件名称 -c 源码路径/Volumes/study/Source/llvm-hk/build_xcode/Debug/bin/clang -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator14.5.sdk/ -Xclang -load -Xclang /Volumes/study/Source/llvm-hk/build_xcode/Debug/lib/HKPlugin.dylib -Xclang -add-plugin -Xclang HKPlugin -c hello.m-------------------------//输出以下内容:正在解析...正在解析...正在解析...正在解析...文件解析完成...
- 共解析出四个顶级节点
3.5.2 分析OC代码
搭建App项目,打开ViewController.m文件,写入以下代码:
#import "ViewController.h"@interface ViewController ()@property(nonatomic, strong) NSString* name;@property(nonatomic, strong) NSArray* arrs;@end@implementation ViewController- (void)viewDidLoad {[super viewDidLoad];}@end
生成AST代码,找到属性的声明
- 在
ObjCPropertyDecl节点中,可以找到属性的声明,包含属性的类型和修饰符
3.5.3 AST节点的过滤
系统API提供MatchFinder,用于AST语法树节点的查找
其中addMatcher函数,可以查找指定节点
void addMatcher(const DeclarationMatcher &NodeMatch,MatchCallback *Action);
参数1:设置指定节点参数2:执行回调,此处并非使用回调函数,而是一个回调类。需要继承MatchCallback系统类,实现自己的子类
添加MatchFinder所在命名空间
using namespace clang::ast_matchers;
实现HKMatchHandler回调类,继承自MatchCallback
class HKMatchHandler:public MatchFinder::MatchCallback{public:void run(const MatchFinder::MatchResult &Result) {const ObjCPropertyDecl *propertyDecl = Result.Nodes.getNodeAs<ObjCPropertyDecl>("objcPropertyDecl");if(propertyDecl){string typeStr = propertyDecl->getType().getAsString();cout<<"------拿到了:"<<typeStr<<endl;}}};
- 必须实现
run函数,它就是真正的回调函数 - 通过
Result结果,获取节点对象 - 通过节点对象的
getType().getAsString(),以字符串的形式返回属性类型
在HKConsumer类中,定义私有MatchFinder和HKMatchHandler,重写构造方法,添加AST节点过滤器
class HKConsumer:public ASTConsumer{private:MatchFinder matcher;HKMatchHandler handler;public:HKConsumer(){matcher.addMatcher(objcPropertyDecl().bind("objcPropertyDecl"), &handler);}};
- 解析语法树,查找
objcPropertyDecl节点
在文件解析完成的回调函数中,调用matcher的matchAST函数,将文件的语法树传入过滤器
void HandleTranslationUnit(ASTContext &Ctx) {cout<<"文件解析完成..."<<endl;matcher.matchAST(Ctx);}
测试插件
- 通过语法树分析,可以找到属性的声明,包含属性的类型和修饰符
- 但也存在一些问题,在预处理阶段,头文件会被展开,我们可能会获取到系统头文件中的属性,所以我们要想办法过滤掉系统文件中的代码
3.5.4 过滤系统文件
可以通过文件路径判断系统文件,因为系统文件都存在于/Applications/Xcode.app/开头的目录中
在PluginASTAction类中,存在CompilerInstance类型的CI参数
std::unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &CI,StringRef InFile) override = 0;
CI为编译器实例对象,可以通过它获取到文件路径,以及警告的提示
重写HKConsumer的构造函数,增加CI参数
HKConsumer(CompilerInstance &CI){matcher.addMatcher(objcPropertyDecl().bind("objcPropertyDecl"), &handler);}
在HKASTAction类中,创建ASTConsumer时,将CI传入
std::unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &CI, StringRef InFile) {return unique_ptr<HKConsumer> (new HKConsumer(CI));}
重写HKMatchHandler的构造函数,增加CI参数。定义私有CompilerInstance,通过构造函数对其赋值
class HKMatchHandler:public MatchFinder::MatchCallback{private:CompilerInstance &CI;public:HKMatchHandler(CompilerInstance &CI):CI(CI){}};
在HKConsumer的构造函数中,对HKMatchHandler中的CI进行传递
HKConsumer(CompilerInstance &CI):handler(CI){matcher.addMatcher(objcPropertyDecl().bind("objcPropertyDecl"), &handler);}
在HKMatchHandler使用CI,获取文件路径并进行过滤
class HKMatchHandler:public MatchFinder::MatchCallback{private:CompilerInstance &CI;bool isUserSourceCode(const string fileName){if(fileName.empty()){return false;}if(fileName.find("/Applications/Xcode.app/")==0){return false;}return true;}public:HKMatchHandler(CompilerInstance &CI):CI(CI){}void run(const MatchFinder::MatchResult &Result) {const ObjCPropertyDecl *propertyDecl = Result.Nodes.getNodeAs<ObjCPropertyDecl>("objcPropertyDecl");string fileName = CI.getSourceManager().getFilename(propertyDecl->getSourceRange().getBegin()).str();if(propertyDecl && isUserSourceCode(fileName)){string typeStr = propertyDecl->getType().getAsString();cout<<"------拿到了:"<<typeStr<<endl;}}};
- 通过
CI.getSourceManager().getFilename获取文件名称,包含文件路径 - 需要传入
SourceLocation,可以通过节点的propertyDecl->getSourceRange().getBegin()获得 - 实现
isUserSourceCode函数,判断路径非空,并且非/Applications/Xcode.app/目录开头,视为自定义文件
测试插件
文件解析完成...------拿到了:NSString *------拿到了:NSArray *
- 成功过滤系统文件,获取到自定义文件中的两个属性
3.5.5 判断属性的类型
实现isShouldUseCopy函数,传入属性类型,判断当前类型是否为必须使用copy修饰的类型
class HKMatchHandler:public MatchFinder::MatchCallback{private:CompilerInstance &CI;bool isUserSourceCode(const string fileName){if(fileName.empty()){return false;}if(fileName.find("/Applications/Xcode.app/")==0){return false;}return true;}bool isShouldUseCopy(const string typeStr){if(typeStr.find("NSString") != string::npos ||typeStr.find("NSArray") != string::npos ||typeStr.find("NSDictionary") != string::npos){return true;}return false;}public:HKMatchHandler(CompilerInstance &CI):CI(CI){}void run(const MatchFinder::MatchResult &Result) {const ObjCPropertyDecl *propertyDecl = Result.Nodes.getNodeAs<ObjCPropertyDecl>("objcPropertyDecl");string fileName = CI.getSourceManager().getFilename(propertyDecl->getSourceRange().getBegin()).str();if(propertyDecl && isUserSourceCode(fileName)){string typeStr = propertyDecl->getType().getAsString();if(isShouldUseCopy(typeStr)){cout<<"------拿到了:"<<typeStr<<endl;}}}};
在ViewController.m中,增加其他类型的属性声明
#import "ViewController.h"@interface ViewController ()@property(nonatomic, strong) NSString* name;@property(nonatomic, strong) NSArray* arrs;@property(nonatomic, strong) id objc;@property(nonatomic, strong) NSSet *sets;@property(nonatomic, strong) NSDictionary * dict;@end@implementation ViewController- (void)viewDidLoad {[super viewDidLoad];}@end
测试插件
文件解析完成...------拿到了:NSString *------拿到了:NSArray *------拿到了:NSDictionary *
- 成功过滤其他类型的属性
3.5.6 判断属性的修饰符
通过propertyDecl->getPropertyAttributes()获取属性修饰符,和OBJC_PR_copy进行位与运算
void run(const MatchFinder::MatchResult &Result) {const ObjCPropertyDecl *propertyDecl = Result.Nodes.getNodeAs<ObjCPropertyDecl>("objcPropertyDecl");string fileName = CI.getSourceManager().getFilename(propertyDecl->getSourceRange().getBegin()).str();if(propertyDecl && isUserSourceCode(fileName)){string typeStr = propertyDecl->getType().getAsString();ObjCPropertyDecl::PropertyAttributeKind attr = propertyDecl->getPropertyAttributes();if(isShouldUseCopy(typeStr) && !(attr & ObjCPropertyDecl::OBJC_PR_copy)){cout<<"------请使用copy修饰:"<<typeStr<<endl;}}}
测试插件:
文件解析完成...------请使用copy修饰:NSString *------请使用copy修饰:NSArray *------请使用copy修饰:NSDictionary *
3.5.7 提示警告信息
当判断目标类型使用非copy修饰,目前只是内容打印,正确的做法在Xcode中提示警告信息
使用编译器实例对象CI提示警告信息
void run(const MatchFinder::MatchResult &Result) {const ObjCPropertyDecl *propertyDecl = Result.Nodes.getNodeAs<ObjCPropertyDecl>("objcPropertyDecl");string fileName = CI.getSourceManager().getFilename(propertyDecl->getSourceRange().getBegin()).str();if(propertyDecl && isUserSourceCode(fileName)){string typeStr = propertyDecl->getType().getAsString();ObjCPropertyDecl::PropertyAttributeKind attr = propertyDecl->getPropertyAttributes();if(isShouldUseCopy(typeStr) && !(attr & ObjCPropertyDecl::OBJC_PR_copy)){DiagnosticsEngine &diag = CI.getDiagnostics();diag.Report(propertyDecl->getLocation(), diag.getCustomDiagID(DiagnosticsEngine::Warning, "请使用copy修饰"));}}}
- 通过
CI的getDiagnostics函数,获取诊断引擎,需要传入位置和DiagID - 通过节点获取位置,使用
propertyDecl->getLocation()获得当前节点的位置 - 通过
diag.getCustomDiagID获取DiagID,设置提示级别和文案
测试插件
文件解析完成...ViewController.m:12:40: warning: 请使用copy修饰@property(nonatomic, strong) NSString* name;^ViewController.m:13:39: warning: 请使用copy修饰@property(nonatomic, strong) NSArray* arrs;^ViewController.m:16:45: warning: 请使用copy修饰@property(nonatomic, strong) NSDictionary * dict;^3 warnings generated.
3.5.8 Xcode集成插件
打开测试项目,在Xcode中注册插件,来到Build Settings→Other C Flags
//-Xclang -load -Xclang (.dylib)插件路径 -Xclang -add-plugin -Xclang 插件名称-Xclang -load -Xclang /Volumes/study/Source/llvm-hk/build_xcode/Debug/lib/HKPlugin.dylib -Xclang -add-plugin -Xclang HKPlugin
在Xcode中替换Clang,来到Build Settings中新增两项用户自定义设置
分别添加CC和CXX
CC对应自己编译的Clang绝对路径CXX对应自己编译的Clang++绝对路径
/Volumes/study/Source/llvm-hk/build_xcode/Debug/bin/clang/Volumes/study/Source/llvm-hk/build_xcode/Debug/bin/clang++
在Build Settings中,将Enable Index-Wihle-Building Functionality设置为NO
测试插件
总结
LLVM:
LLVM是一个编译器优势:前后端分离,可扩展性强
编译型语言 & 解释型语言:
解释型语言:直接读取源码,使用解释器即可直接执行
编译型语言:通过编译器,将源码编译成机器代码才可执行
编译器:
前端:经过词法分析、语法分析,生成
AST抽象语法树,LLVM还会生成IR中间代码,开启Bitcode会生成bc代码优化器:在
LLVM的前端和后端都会对代码进行优化,根据一个又一个Pass进行优化。前端优化IR代码,后端优化汇编代码后台:生成汇编代码,生成目标文件,经过链接,根据不同的机构生成对应的可执行文件
编译流程:
读取源码
预处理阶段
- 展开宏和头文件
编译阶段
词法分析
语法分析
生成
IR中间代码IR代码优化Bitcode代码
生成汇编代码
生成目标文件(汇编器)
生成可执行文件(链接)
Clang插件:
编译
LLVM工程创建插件
编写插件代码
文件和顶级节点的解析
分析
OC代码AST节点的过滤过滤系统文件
判断属性的类型
判断属性的修饰符
提示警告信息
Xcode集成插件
