iOS底层原理探索-----LLVM和Clang插件开发

1,274 阅读15分钟

LLVM 概述

LLVM是架构编译器(compiler)的框架系统,以C++编写而成。用于优化以任意程序语言编写的程序的编译时间(compile-time)、链接时间(link-time)、运行时间(run-time)以及空闲时间(idle-time),对开发者保持开放,并兼任已有脚本。

LLVM计划启动于2000年,最初由美国UIUC大学的Chris Lattner博士主持开展。2006Chris Lattner加盟Apple Inc,并致力于LLVMApple开放体系中的应用。

Apple也是LLVM计划的主要自助者。

目前LLVM已经被苹果iOS开发工具、Xilinx VivadoFacebookGoogle等各大公司采用。

LLVM下载

由于国内的网络限制,我们需要借助镜像下载LLVM的源码
mirror.tuna.tsinghua.edu.cn/help/llvm/

传统编译器设计

image.png

  • 源码(Source Code),经过编译器前端(Frontend)→优化器(Optimizer)→编译器后端(Backend),生成机器代码(Machine Code
    • 机器代码(Machine Code):就是CPU可执行的二进制代码

    • 从源码到机器码的生成,这个过程都是编译器负责完成的

iOS的编译器架构

Objective-CCC++使用的编译器前端是ClangSwift使用的编译器前端是swiftc,而它们使用的编译器后端都是LLVM

image-1.png

各个模块的职责:

  • 编译器前端(Frontend
    • 编译器前端的任务是解析源代码。它会进行:词法分析、语法分析、语义分析,检查源代码是否存在错误,然后构建抽象语法树(Abstract Syntax Tree, AST);

    • LLVM的前端还会生成中间代码(intermediate representation, IR)。

  • 优化器(Optimizer
    • 优化器负责进行各种优化。改善代码的运行时间,例如:消除冗余计算等。

  • 后端(Backend)/代码生成器(Code Generator
    • 将代码映射到目标指令集。生成机器代码,并进行机器代码的相关优化。

LLVM的设计

当编译器决定支持多种源语言或多种硬件框架时,LLVM最重要的地方就来了。其他的编译器如GCC,它方法非常成功,但由于它是作为整体应用程序设计的,因此它们的用途受到了很大的限制

LLVM设计的最重要方面是,使用通用的代码表示形式(IR),它是用来在编译器中表示代码的形式。所以LLVM可以为任何编程语言独立编写前端,并且可以为任意硬件架构独立编写后端

image.png

  • 简单来说,LLVM最大的优势,就是将编译器的前后端分离,从而提高可扩展性。

Clang

ClangLLVM项目中的一个子项目;

它是基于LLVM架构的轻量级编译器,诞生之初为了替代GCC,提供更快的编译速度;

它是负责编译CC++Objective-C语音的编译器,它属于整个LLVM架构中的编译器前端;

对于开发者来说,研究Clang可以给我们带来很多好处。

编译流程

创建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:通过不同的架构,生成对应的可执行文件。

预处理阶段

预编译阶段:将宏和导入的头文件进行替换

打开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

使用definetypedef的区别:

  • define:宏定义,在预处理阶段会被替换:

    • 可用来做代码混淆,将App中核心代码,用系统相似的名称进行取别名,然后在预处理阶段就被替换,以此达到代码混淆的目的。
  • typedef:对数据类型取别名,在预处理阶段不会被替换掉。

编译阶段

编译阶段可划分为三个部分:

  • 词法分析;

  • 语法分析;

  • 生成IR中间代码。

词法分析

预处理完成后,就会进行词法分析,这里会把代码切成一个个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

查看词法分析之后的结果:

image.png

语法分析

词法分析完成之后就是语法分析,它的任务是验证语法是否正确。在词法分析的基础上,将单词序列组合成各类语法短语,例如:“程序”,“语句”,“表达式”等,然后将所有节点组成抽象语法树(Abstract Syntax Tree, AST)。语法分析程序判断源程序在结构上是否正确。

命令:

clang -fmodules -fsyntax-only -Xclang -ast-dump main.m

查看语法分析之后的结果:

image.png

重点关键字的介绍:

  • FunctionDecl:函数

  • ParmVarDecl:参数

  • CallExpr:函数调用

  • BinaryOperator:运算符

生成IR中间代码

完成以上步骤后,就会开始生成IR中间代码,代码生成器(Code Generator)会将语法树自顶向下遍历,逐步翻译成LLVM IR

通过以下命令,可以生成.ll文件,查看IR代码:

clang -S -fobjc-arc -emit-llvm main.m
  • Objective-C代码,在这一步会进行Runtime的桥接:property合成,ARC处理等。

查看IR中间代码:

image.png

IR基本语法介绍:

  • @:全局标示

  • %:局部标示

  • alloca:开辟空间

  • align:内存对齐

  • i3232bit4字节

  • store:写入内存

  • load:读取数据

  • call:调用函数

  • ret:返回

IR的优化

Xcode中,找到TargetBuild SettingOptimization Level,可以对当前项目设置优化等级。

LLVM中,优化级别分别是-O0-O1-O2-O3-Os(第一个是大写英文字母O)。

通过以下命令,可设置优化等级,并生成IR代码:

clang -Os -S -fobjc-arc -emit-llvm main.m -o main.ll

查看优化后的IR代码:

image.png

  • main函数中的代码优化的非常简短,直接计算出结果并返回。

Bitcode

Xcode7以后,开启Bitcode设置,苹果会做进一步的优化,生成.bc中间代码。

命令:

clang -emit-llvm -c main.ll -o main.bc

什么是Bitcode

Bitcode是被编译程序的一种中间形式的代码。包含Bitcode并上传到App Store ConnectApp,会在App Store上编译和链接。包含Bitcode可以在不提交新版本App的情况下,允许Apple在将来的时候再次优化你的App二进制文件。

Xcode中,默认开启Bitcode设置。如果你的App支持BitcodeApp使用到的其他二进制形式也要支持Bitcode,否则就会报错。

解决Bitcode报错只有两种方案:

  • 【方案一】将不支持BitcodeSDK移除掉,或等待第三方更新。

  • 【方案二】:将使用Bitcode的选项设置为NO

生成汇编代码

通过最终的.ll.bc代码,生成汇编代码。

命令:

clang -S -fobjc-arc main.ll -o main.s 
clang -S -fobjc-arc main.bc -o main.s

查看汇编代码:

image.png

汇编代码也可以设置OPT的优化等级进行优化。

clang -Os -S -fobjc-arc main.ll -o main.s

查看优化后的汇编代码:

image.png

生成目标文件(汇编器)

目标文件的生成,是汇编器以汇编代码作为输入,将汇编代码转换为机器代码,最后输出目标文件(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:表示这个符号在外部是可以被访问的。

生成可执行文件(链接)

链接:将多个目标文件合并,符号表(包括重定位符号表)合并成一张表,经过链接最后,会分配虚拟内存地址,最终生成可执行文件或动态库。

这个过程还会链接需要的动态库和静态库

  • 静态库,和可执行文件合并。

  • 动态库,独立存在,运行时,由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直接绑定。

Clang 插件

编写一个Clang插件,实现效果:定义NSStringNSArrayNSDictionary类型的属性,未使用copy修饰,对该属性提示警告。

下载LLVM

下载LLVM项目:

git clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm/llvm.git

LLVMtools目录下,下载Clang

cd llvm/tools 
git clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm/clang.git

LLVMprojects目录下,下载compiler-rtlibcxxlibcxxabi

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

Clangtools下,安装extra工具:

cd ../tools/clang/tools 
git clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm/clang-tools-e 
xtra.git

安装cmake

使用brew命令,查看是否安装cmake,如果已安装,跳过此步骤:

brew list

通过brew安装cmake

brew install cmake

编译LLVM

通过Xcode编译LLVM

cmake编译成Xcode项目:

mkdir build_xcode 
cd build_xcode 
cmake -G Xcode ../llvm

使用Xcode编译Clang

选择手动管理Schemes

7B575BF9-0E14-49C9-9865-04CE4EA39655.png

点击左下⻆加号,在Target中添加clangclangTooling

image-1.png

通过Run Without Building运⾏,代码没有改变的时候,不需要重新编译,直接运⾏现有可执⾏⽂件即可:

image.png

通过ninja编译LLVM

安装ninja

安装`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

创建插件

/llvm/tools/clang/tools目录下,新建插件HKPlugin

image.png

修改/llvm/tools/clang/tools目录下的CMakeLists.txt文件

image.png

新增add_clang_subdirectory(HKPlugin)

image.png

HKPlugin目录下,新建HKPlugi.cppCMakeLists.txt文件:

image.png

打开CMakeLists.txt文件,写入以下内容:

add_llvm_library( HKPlugin MODULE BUILDTREE_ONLY HKPlugin.cpp )

利用cmake重新生成Xcode项目,在build_xcode目录中执行cmake命令:

cmake -G Xcode ../llvm

最后,可以在LLVMXcode项目中,在Loadable modules目录下找到自定义Plugin目录:

image.png

  • 打开HKPlugi.cpp文件,可以在里面编写插件代码。

编写插件代码

文件和顶级节点的解析

导入插件使用的头文件和命名空间

#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类中,重写CreateASTConsumerParseArgs函数

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; 
            } 
    };
}

重写HandleTopLevelDeclHandleTranslationUnit函数

  • HandleTopLevelDecl:顶级节点解析回调函数,顶级节点,例如:全局变量、函数定义、属性;

  • HandleTranslationUnit:整个文件解析完成后的回调。

编译HKPlugin项目,在项目的Products目录下,找到编译出的clang可执行文件:

image.png

同样在Products目录下,找到HKPlugin.dylib

image.png

使用插件,测试文件和顶级节点的解析。

创建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 -XclangHKPlugin -c hello.m

-------------------------

//输出以下内容: 
正在解析... 
正在解析... 
正在解析... 
正在解析... 
文件解析完成...
  • 共解析出四个顶级节点。

分析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代码,找到属性的声明

image.png

  • ObjCPropertyDecl节点中,可以找到属性的声明,包含属性的类型和修饰符。

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类中,定义私有MatchFinderHKMatchHandler,重写构造方法,添加AST节点过滤器:

class HKConsumer:public ASTConsumer { 
    private: 
        MatchFinder matcher; 
        HKMatchHandler handler; 
        
    public: 
        HKConsumer() { 
            
            matcher.addMatcher(objcPropertyDecl().bind("objcPropertyDecl"), &handler); 
        } 
};
  • 解析语法树,查找objcPropertyDecl节点。

在文件解析完成的回调函数中,调用matchermatchAST函数,将文件的语法树传入过滤器:

void HandleTranslationUnit(ASTContext &Ctx) { 
    cout<<"文件解析完成..."<<endl; 
    matcher.matchAST(Ctx); 
}

测试插件

image.png

  • 通过语法树分析,可以找到属性的声明,包含属性的类型和修饰符;

  • 但也存在一些问题,在预处理阶段,头文件会被展开,我们可能会获取到系统头文件中的属性,所以我们要想办法过滤掉系统文件中的代码。

过滤系统文件

可以通过文件路径判断系统文件,因为系统文件都存在于/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 *
  • 成功过滤系统文件,获取到自定义文件中的两个属性。

判断属性的类型

实现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 *
  • 成功过滤其他类型的属性

判断属性的修饰符

通过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 *

提示警告信息

当判断目标类型使用非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修饰")); 
        } 
    }
}
  • 通过CIgetDiagnostics函数,获取诊断引擎,需要传入位置和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.

Xcode集成插件

打开测试项目,在Xcode中注册插件,来到Build SettingsOther C Flags

image.png

//-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中新增两项用户自定义设置: 、 image.png

分别添加CCCXX

image.png

  • 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

image.png

测试插件

image.png