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}, image
6: 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 30
int 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.s
clang -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 _printf
0000000000000000 (__TEXT,__text) external _main
_printf
函数,被标记为undefined external
undefined
:表示在当前文件中,暂时找不到符号。因为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_header
0000000100003f77 (__TEXT,__text) external _main
0000000100008008 (__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/tools
git clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm/clang.git
在LLVM
的projects
目录下,下载compiler-rt
、libcxx
、libcxxabi
cd ../projects
git clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm/compiler-rt.g
it
git clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm/libcxx.git
git clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm/libcxxabi.git
在Clang
的tools
下,安装extra
工具
cd ../tools/clang/tools
git clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm/clang-tools-e
xtra.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_xcode
cd build_xcode
cmake -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_build
cmake -G Ninja ../llvm -DCMAKE_INSTALL_PREFIX=安装路径
- 本机为
/Users/xxx/xxx/LLVM/llvm_release
,注意DCMAKE_INSTALL_PREFIX
后面不能有空格
依次执行编译、安装指令
ninja
ninja 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_ONLY
HKPlugin.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
节点进行解析
我们可以使用以下两个函数:
CreateASTConsumer
ParseArgs
在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
集成插件