iOS LLVM-Clang 浅谈

LLVM概念
  1. LLVM官网: https://llvm.org/
  2. 编译器架构图:


    image
  • Frontend:前端 → 词法分析、语法分析、语义分析、生成中间代码(LLVM IR)
  • Optimizer:优化器 → 中间代码优化
  • Backend:后端 → 生成机器码
  • 传统编译器(如 CGG )的前端和后端没有完全分离,耦合在了一起,因而如果要支持一门新的语言或硬件平台,需要做大量的工作。


    image
  • 不同的前端后端使用统一的中间代码LLVM Intermediate Representation (LLVM IR)
      1. LLVM IR格式以 .ll结尾、以 .bc 的二进制格式结尾、内存格式
      1. Bitcode(Xcode 7之后)就是以.bc结尾的中间代码,是LLVM-IR在磁盘上的一种二进制表示形式。例如:clang -c -emit-llvm xxxx.m 生成 xxxx. bc
      1. 如果要转换成文本格式查看,例如:llvm-dis xxxx.bc -o xxxx.ll
      1. 苹果单独对 Bitcode 进行了额外的优化.
        • i) 应用上传到 AppStore时,Xcode会将程序对应的 Bitcode一起上传;
        • ii) AppStore会将 Bitcode重新编译为可执行程序,供用户下载;
        • iii) Bitcode被Xcode打包成 xar文档,嵌入的 MachO中。
  • 如果需要支持一种新的编程语言、硬件设备,那么只需要实现一个新的前后端
  1. LLVM 同时支持 AOT 预先编译和 JIT 即时编译
Clang概念
  1. 官网:http://clang.llvm.org/
  2. Clang项目为LLVM 项目的C语言系列(C,C ++,Objective C / C ++,OpenCL,CUDA和RenderScript)中的语言提供语言前端和工具基础结构。提供了与GCC兼容的编译器驱动程序(clang)和与MSVC兼容的编译器驱动程序(clang-cl.exe)
  3. 说白了就是LLVM项目的一个子项目,LLVM的C语言家族前端。
  4. 相比于GCC,Clang具有如下优点
    • 编译速度快:在某些平台上,Clang的编译速度显著的快过GCC(Debug模式下编译OC速度比GGC快3倍)
    • 占用内存小:Clang生成的AST所占用的内存是GCC的五分之一左右
    • 模块化设计:Clang采用基于库的模块化设计,易于IDE 集成及其他用途的重用
    • 诊断信息可读性强:在编译过程中,Clang 创建并保留了大量详细的元数据(metadata),有利于调试和错误报告
    • 设计清晰简单,容易理解,易于扩展增强
编译流程
image
  1. Objective-C与swift都采用Clang作为编译器前端,编译器前端主要进行词法分析、语法分析、语义分析、生成中间代码,在这个过程中,会进行类型 检查,如果发现错误或者警告会标注出来在哪一行。
  2. 编译器后端会进行机器无关的代码优化,生成机器语言,并且进行机器相关的代码优化,根据不同的系统架构生成不同的机器码。


    image
自定义规则
image

执行oclint自带脚本scaffoldRule:
cd ~/oclint/oclint-scripts
./scaffoldRule Property -t ASTVisitor(参数可选为Generic,SourceCodeReader,ASTVisitor,ASTMatcher)

加载自定义规则Xcode项目

oclint-xcoderules目录下,执行create-xcode-rules.sh脚本

#! /bin/sh -e
cmake -G Xcode \
        -D CMAKE_CXX_COMPILER=../build/llvm-install/bin/clang++ \ 
        -D CMAKE_C_COMPILER=../build/llvm-install/bin/clang \
        -D OCLINT_BUILD_DIR=../build/oclint-core \
        -D OCLINT_SOURCE_DIR=../oclint-core \
        -D OCLINT_METRICS_SOURCE_DIR=../oclint-metrics 
        \ -D OCLINT_METRICS_BUILD_DIR=../build/oclint-metrics \
        -D LLVM_ROOT=../build/llvm-install/../oclint-rules
编译自定义项目生成dylib文件

复制到~/oclint/build/oclint-release/lib/oclint/rules下

调试规则
WX20200822-165241.png

以下编译环境是在测试demo中的,添加到scheme下的run配置中:


WX20200822-165317.png
-R=/Users/libing/oclint/oclint-xcoderules/rules.dl/Debug /Users/libing/oclint/oclint-xcodebuild/TestRule/TestRule/ViewController.m -- -x objective-c -isystem ~/oclint/build/oclint-release/lib/clang/5.0.1/include/ -arch x86_64 -fmessage-length=0 -fdiagnostics-show-note-include- stack -fmacro-backtrace-limit=0 -std=gnu11 -fobjc-arc -fobjc-weak -fmodules -gmodules -fmodules-cache-path=/Users/libing/Library/Developer/Xcode /DerivedData/ModuleCache.noindex -fmodules-prune-interval=86400 -fmodules-prune-after=345600 -fbuild-session-file=/Users/libing/Library/Developer/Xcode /DerivedData/ModuleCache.noindex/Session.modulevalidation -fmodules-validate-once-per-build-session -Wnon-modular-include-in-framework-module - Werror=non-modular-include-in-framework-module -Wno-trigraphs -fpascal-strings -O0 -fno-common -Wno-missing-field-initializers -Wno-missing-prototypes - Werror=return-type -Wdocumentation -Wunreachable-code -Wno-implicit-atomic-properties -Werror=deprecated-objc-isa-usage -Wno-objc-interface-ivars - Werror=objc-root-class -Wno-arc-repeated-use-of-weak -Wimplicit-retain-self -Wduplicate-method-match -Wno-missing-braces -Wparentheses -Wswitch - Wunused-function -Wno-unused-label -Wno-unused-parameter -Wunused-variable -Wunused-value -Wempty-body -Wuninitialized -Wconditional-uninitialized - Wno-unknown-pragmas -Wno-shadow -Wno-four-char-constants -Wno-conversion -Wconstant-conversion -Wint-conversion -Wbool-conversion -Wenum- conversion -Wno-float-conversion -Wnon-literal-null-conversion -Wobjc-literal-conversion -Wshorten-64-to-32 -Wpointer-sign -Wno-newline-eof -Wno- selector -Wno-strict-selector-match -Wundeclared-selector -Wdeprecated-implementations -DDEBUG=1 -DOBJC_OLD_DISPATCH_PROTOTYPES=0 -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator12.2.sdk -fasm-blocks -fstrict-aliasing - Wprotocol -Wdeprecated-declarations -mios-simulator-version-min=12.2 -g -Wno-sign-conversion -Winfinite-recursion -Wcomma -Wblock-capture- autoreleasing -Wstrict-prototypes -Wno-semicolon-before-method-body -Wunguarded-availability -fobjc-abi-version=2 -fobjc-legacy-dispatch -index-store- path /Users/libing/Library/Developer/Xcode/DerivedData/TestRule-fnyamuwmajnavbcsgeuhbzloxpbs/Index/DataStore -iquote /Users/libing/Library/Developer /Xcode/DerivedData/TestRule-fnyamuwmajnavbcsgeuhbzloxpbs/Build/Intermediates.noindex/TestRule.build/Debug-iphonesimulator/TestRule.build/TestRule- generated-files.hmap -I/Users/libing/Library/Developer/Xcode/DerivedData/TestRule-fnyamuwmajnavbcsgeuhbzloxpbs/Build/Intermediates.noindex/TestRule.build /Debug-iphonesimulator/TestRule.build/TestRule-own-target-headers.hmap -I/Users/libing/Library/Developer/Xcode/DerivedData/TestRule- fnyamuwmajnavbcsgeuhbzloxpbs/Build/Intermediates.noindex/TestRule.build/Debug-iphonesimulator/TestRule.build/TestRule-all-target-headers.hmap -iquote /Users/libing/Library/Developer/Xcode/DerivedData/TestRule-fnyamuwmajnavbcsgeuhbzloxpbs/Build/Intermediates.noindex/TestRule.build/Debug-iphonesimulator /TestRule.build/TestRule-project-headers.hmap -I/Users/libing/Library/Developer/Xcode/DerivedData/TestRule-fnyamuwmajnavbcsgeuhbzloxpbs/Build/Products /Debug-iphonesimulator/include -I/Users/libing/Library/Developer/Xcode/DerivedData/TestRule-fnyamuwmajnavbcsgeuhbzloxpbs/Build/Intermediates.noindex /TestRule.build/Debug-iphonesimulator/TestRule.build/DerivedSources-normal/x86_64 -I/Users/libing/Library/Developer/Xcode/DerivedData/TestRule- fnyamuwmajnavbcsgeuhbzloxpbs/Build/Intermediates.noindex/TestRule.build/Debug-iphonesimulator/TestRule.build/DerivedSources/x86_64 -I/Users/libing/Library /Developer/Xcode/DerivedData/TestRule-fnyamuwmajnavbcsgeuhbzloxpbs/Build/Intermediates.noindex/TestRule.build/Debug-iphonesimulator/TestRule.build /DerivedSources -F/Users/libing/Library/Developer/Xcode/DerivedData/TestRule-fnyamuwmajnavbcsgeuhbzloxpbs/Build/Products/Debug-iphonesimulator -MMD - MT dependencies -MF /Users/libing/Library/Developer/Xcode/DerivedData/TestRule-fnyamuwmajnavbcsgeuhbzloxpbs/Build/Intermediates.noindex/TestRule.build /Debug-iphonesimulator/TestRule.build/Objects-normal/x86_64/ViewController.d --serialize-diagnostics /Users/libing/Library/Developer/Xcode/DerivedData /TestRule-fnyamuwmajnavbcsgeuhbzloxpbs/Build/Intermediates.noindex/TestRule.build/Debug-iphonesimulator/TestRule.build/Objects-normal/x86_64 /ViewController.dia -c /Users/libing/oclint/oclint-xcodebuild/TestRule/TestRule/ViewController.m -o /Users/libing/Library/Developer/Xcode/DerivedData/TestRule- fnyamuwmajnavbcsgeuhbzloxpbs/Build/Intermediates.noindex/TestRule.build/Debug-iphonesimulator/TestRule.build/Objects-normal/x86_64/ViewController.o

备注:-R=/Users/libing/oclint/oclint-xcoderules/rules.dl/Debug /Users/libing/oclint/oclint-xcodebuild/TestRule/TestRule/ViewController.m -- -x objective-c - isystem ~/oclint/build/oclint-release/lib/clang/5.0.1/include/ 加上图上从-arch x86_64 开始复制到最后,拼起来放到自定义规则的scheme的run配置中。

image
查看编译过程

clang -ccc-print-phases ViewController.m

0: input, "ViewController.m", objective-c
1: preprocessor, {0}, objective-c-cpp-output → 预编译
2: compiler, {1}, ir → 前端编译,生成中间代码IR
3: backend, {2}, assembler → 后端Backend 4: assembler, {3}, object → 生成目标.o文件 5: linker, {4}, image → 链接
6: bind-arch, "x86_64", {5}, image → 绑定架构
查看 preprocessor(预处理)

clang -E ViewController.m

image

注意:出现fatal error: 'UIKit/UIKit.h' file not found,但不影响查看结果,如果不报错指定一下sdk
clang -E -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk ViewController.m

词法分析,生成Token

clang -fmodules -E -Xclang -dump-tokens ViewController.m

image

语法分析,生成抽象AST语法树
image

通过语法分析可以清晰的看到语法树中的节点,对应的节点Decl(声明) / Stmt(语句、表达式),通过Google 搜索:clang XXX 可以直接定位到clang的该节 点文档说明。

// 分析所有声明
bool VisitDecl(Decl *decl) {
    return true;// 返回true以继续遍历AST,返回false以终止遍历,退出Clang 
}
// 分析表达式
bool VisitStmt(Stmt *S) {
    return true;// 返回true以继续遍历AST,返回false以终止遍历,退出Clang 
}
自定义规则源码

OClintScripts

自定义属性规范
  1. nonatomic 与 atomic 修饰:遗忘或错用了 atomic 修饰
bool VisitObjCPropertyDecl(ObjCPropertyDecl *node) { 
  if (node->isAtomic()) {
    string description = "不应该用atomic修饰 " + node->getNameAsString() + " 请改用nonatomic";
    addViolation(node, this, description);//发出警告
  } 
}

代码解析: 通过AST抽象语法树分析:


image

入口在VisitObjCPropertyDecl方法,那么在此方法中断点验证确实如此,接下来分析,属性中是否是atomic修饰,凭着灵感发现确实自动补全了isAtomic 方法,也可以这么写:

ObjCPropertyDecl::PropertyAttributeKind attributeKind = node->getPropertyAttributes(); 
    if(!(attributeKind & ObjCPropertyDecl::OBJC_PR_nonatomic)){
    string description = "不应该用atomic修饰 " + node->getNameAsString() + " 请改用nonatomic";
    addViolation(node, this, description); //发出警告
}
  1. NSString 应尽量用 copy 修饰,避免用 strong 修饰
if(starts_with(typeStr, "NSString") && !(attributeKind & ObjCPropertyDecl::OBJC_PR_copy)) {
    addViolation(node, this, typeStr + node->getNameAsString() + " 应尽量用 copy 修饰"); 
}
  1. Delegate 应该用 weak 修饰
if(typeStr.find("<")!=string::npos && typeStr.find(">")!=string::npos && starts_with(node->getType().getAsString(), "id")){ if(!(attributeKind & ObjCPropertyDecl::OBJC_PR_weak)){
    cout<< node->getType().getAsString()<getNameAsString() + " 尽可能用 weak 修饰"); }
}
  1. Block 应该用 copy / strong 修饰
if (node->getType()->isBlockPointerType()) {
    if(!((attributeKind & ObjCPropertyDecl::OBJC_PR_copy) || (attributeKind & ObjCPropertyDecl::OBJC_PR_strong))){ 
    addViolation(node, this, node->getNameAsString() + " 应该用 copy / strong 修饰");
    } 
}
  1. 属性名称应该遵循驼峰命名,不应该以大写或_开头
string name = node->getNameAsString(); 
string::iterator itor = name.begin(); 
if(*itor >= 'A' && *itor <= 'Z'){
    addViolation(node, this, "Property "+name+" 应该遵循驼峰命名,不应该以大写字母开头"); }
if(*itor == '_'){
    addViolation(node, this, "Property "+name+" 应该遵循驼峰命名,不应该以下划线开头");
}
  1. 成员变量应该以_开头
bool VisitObjCIvarDecl(ObjCIvarDecl *node) {
    string name = node->getNameAsString(); 
    string::iterator itor = name.begin(); 
    if(*itor != '_'){
        addViolation(node, this, "成员变量 "+name+" 应该以下划线开头"); 
    }
    return true;
}
自定义空格规范
  1. 方法前面-或+请遵循空格规范、方法大括号请遵循空格规范
    bool VisitObjCMethodDecl(ObjCMethodDecl *node)
    {
        string methodDeclStr;
        ASTContext *context = _carrier->getASTContext();
        SourceLocation begin = node->getSourceRange().getBegin();
        SourceLocation end = node->getSourceRange().getEnd(); methodDeclStr.assign(context->getSourceManager().getCharacterData(begin),end.getRawEncoding()-begin.getRawEncoding());
        for(string::iterator it = methodDeclStr.begin(); it!= methodDeclStr.end(); it++)
        {
            if (*it =='-' || *it =='+') {
                if (it+2 < methodDeclStr.end()) {
                    if (!((*(it+1) == ' ')&&(*(it+2) == '('))) {
                        addViolation(node, this, "方法前面-或+请遵循空格规范");
                        return true;
                    }
                }
            }
            
            if (*it =='{') {
                if (it-1 >= methodDeclStr.begin()) {
                    if (*(it-1) != ' ' && *(it-1) != '\n') {
                        addViolation(node, this, "方法大括号请遵循空格规范");
                    }
                }
                return true;
            }
        }
        return true;
    }
};
  1. @property左括号、右括号、中间逗号、*修饰需要遵循空格规范
bool VisitObjCPropertyDecl(ObjCPropertyDecl *node)
    {
        string methodDeclStr;
        ASTContext *context = _carrier->getASTContext();
        SourceLocation begin = node->getSourceRange().getBegin();
        SourceLocation end = node->getSourceRange().getEnd(); methodDeclStr.assign(context->getSourceManager().getCharacterData(begin),end.getRawEncoding()-begin.getRawEncoding());

        for(string::iterator it = methodDeclStr.begin(); it!= methodDeclStr.end(); it++)
        {
            if (*it == '(') {
                if (it-2 >= methodDeclStr.begin()) {
                    if (!(*(it-1) == ' ' && *(it-2) == 'y') && *(it+1) != '^') {
                        addViolation(node, this, node->getNameAsString() + "@property左括号请遵循空格规范");
                        return true;
                    }
                }
            }
            
            if (*it == ',') {
                if (it+1 < methodDeclStr.end()) {
                    if (*(it+1) != ' ') {
                        addViolation(node, this, node->getNameAsString() + "@property中逗号请遵循空格规范");
                        return true;
                    }
                }
            }
            
            if (*it == ')') {
                if (it+1 < methodDeclStr.end()) {
                    if (*(it+1) != ' ') {
                        addViolation(node, this, node->getNameAsString() + "@property右括号请遵循空格规范");
                        return true;
                    }
                }
            }
            
            if (*it == '*') {
                if (it-1 >= methodDeclStr.begin()) {
                    if (*(it-1) != ' ') {
                        addViolation(node, this, node->getNameAsString() + "@property属性*修饰请遵循空格规范");
                        return true;
                    }
                }
            }
        }
        return true;
    }
自定义枚举规范
  1. NS_ENUM/NS_OPTIONSenum
    经过调试发现enum枚举走VisitEnumDecl方法,而NS_ENUM/NS_OPTIONS均不走。


    image
bool VisitEnumDecl(EnumDecl *node)
{
        addViolation(node, this, node->getNameAsString()+"枚举尽量使用NS_ENUM/NS_OPTIONS");
        return true;
}

或者基于AbstractASTMatcherRule匹配

virtual void callback(const MatchFinder::MatchResult &result) override {
    const EnumDecl *enumDecl = result.Nodes.getNodeAs("enumDecl"); 
    if (enumDecl)
    {
        addViolation(node, this, "枚举尽量使用NS_ENUM/NS_OPTIONS"); 
    }
}
virtual void setUpMatcher() override {
    addMatcher(enumDecl().bind("enumDecl")); 
}
  1. 枚举不要下划线开头
bool VisitEnumConstantDecl(EnumConstantDecl *node)
{
    string name = node->getNameAsString();
    string::iterator itor = name.begin();
    if(*itor == '_'){
       addViolation(node, this, "枚举不要下划线开头");
    }
    return true;
}
自定义类名规范

类名使用大写字母开头

bool VisitObjCProtocolDecl(ObjCProtocolDecl *node)
{
        string name = node->getNameAsString();
        string::iterator itor = name.begin();
        if(!(*itor >= 'A' && *itor <= 'Z')){
            addViolation(node, this, name+"协议名请用大写字母开头");
        }
        return true;
}

  
bool VisitObjCImplDecl(ObjCImplDecl *node)
{
        string name = node->getNameAsString();
        string::iterator itor = name.begin();
        if(!(*itor >= 'A' && *itor <= 'Z')){
            addViolation(node, this, name+"类名请用大写字母开头");
        }
        return true;
}
自定义方法格式规范

方法参数超过3个,需要折行并冒号对齐

bool VisitObjCMethodDecl(ObjCMethodDecl *node)
{
    string name = node->getNameAsString();
    if( node->param_size()>_threshold){
        string methodDeclStr;
        ASTContext *context = _carrier->getASTContext();
        SourceLocation begin = node->getSourceRange().getBegin();
        SourceLocation end = node->getSourceRange().getEnd(); methodDeclStr.assign(context->getSourceManager().getCharacterData(begin),end.getRawEncoding()-node->getSourceRange().getBegin().getRawEncoding());
        string replaceName = formatObjcMethodName(methodDeclStr);
        if(methodDeclStr.find(replaceName)){
            addViolation(node, this, "方法参数超过"+to_string(_threshold)+"个,需要折行并冒号对齐");
        }
    }
    return true;
}
修正oclint误报规范

PreferEarlyExit:使用提前退出/继续来简化代码并减少缩进
误报:懒加载与init初始化误判
修正思路:识别懒加载与init方法进行判断

bool starts_with(const string& s1, const string& s2) {
    return s2.size() <= s1.size() && s1.compare(0, s2.size(), s2) == 0;
}
// 将Property入栈到数组与懒加载比较是否selector相同
bool VisitObjCPropertyDecl(ObjCPropertyDecl *node) {
    if(std::find(_arraySelector.begin(), _arraySelector.end(), node->getNameAsString()) == _arraySelector.end()) {
        _arraySelector.push_back(node->getNameAsString());
    }
    return true;
}
//以init开头的都认为是初始化方法
bool VisitObjCMethodDecl(ObjCMethodDecl *node) {
    string selectorName = node->getSelector().getAsString();
    if (starts_with(selectorName, "init") || std::find(_arraySelector.begin(), _arraySelector.end(), selectorName) != _arraySelector.end() ) {
        _isSysSelector = true; 
    } else {
        _isSysSelector = false; 
    }
    return true; 
}
//通过_isSysSelector修正误报
bool VisitCompoundStmt(CompoundStmt* compoundStmt) {
    if (compoundStmt->size() < 2) {
        return true; 
    }
    if (_isSysSelector) { 
        return true;
    }
    auto last = compoundStmt->body_rbegin();
    if (isFlowOfControlInterrupt(*last)) {
        addViolationIfStmtIsLongIf(*++last);
    }
    return true; 
}
自定义内存泄漏检测

由于目前xcode对block循环引用在编译时期已有提示:capturing 'self' strongly in this block is likely to lead to a retain cycle,但如果API 中的block并未检测。新增了此处关于block强引用的检测。

virtual void setUp() override {
    _teamname = RuleConfiguration::stringForKey("TEAM_NAME", "xxxxx");       
    this->ignoreArr.push_back("UIView"); // 忽略UIView类的block检测 
}
virtual void tearDown() override {}
bool isMasnoryBlock(BlockDecl *node) {
    for (BlockDecl::param_iterator iterator = node->param_begin() ; iterator != node->param_end(); iterator ++) 
    {
       if ((*iterator)->getType().getAsString().find("MASConstraintMaker") != string::npos) return true;
       return false;
    }
bool isIgnoreArrClass(ObjCMessageExpr *node) {
    string type = node->getClassReceiver().getAsString();
    vector::iterator ret = std::find(this->ignoreArr.begin(), this->ignoreArr.end(), type); 
    if(ret == this->ignoreArr.end()) {
        return false; 
    } else {
        return true;
    }
}
bool VisitObjCMessageExpr(ObjCMessageExpr *node) 
{
    if (this->isIgnoreArrClass(node)) {
        return true; 
    }
    int argCount = node->getNumArgs(); 
    Expr **exprArray = node->getArgs(); 
    for (int i = 0; i < argCount; i ++ ) {
        BlockExpr *expr = dyn_cast_or_null(exprArray[i]); 
        if (expr && expr->getBlockDecl()) {
            BlockDecl *blockDecl = expr->getBlockDecl();
            for (BlockDecl::capture_const_iterator iterator = blockDecl->capture_begin() ; iterator != blockDecl->capture_end(); iterator ++) {
                ImplicitParamDecl *implicitParamDecl = dyn_cast_or_null(iterator->getVariable());
                if (implicitParamDecl && implicitParamDecl->getName() == "self") {
                    if (!isMasnoryBlock(blockDecl)) { 
                        string methodDeclStr;
                        ASTContext *context = _carrier->getASTContext();
                        SourceLocation begin = this->methodDecl->getSourceRange().getBegin();
                        SourceLocation end = this->methodDecl->getSourceRange().getEnd(); 
                        methodDeclStr.assign(context->getSourceManager().getCharacterData(begin),end.getRawEncoding()-begin.getRawEncoding());
                        string methodblock;
                        SourceLocation beginBlock = node->getSourceRange().getBegin();
                        SourceLocation endBlock = node->getSourceRange().getEnd(); 
                        methodblock.assign(context->getSourceManager(). getCharacterData(beginBlock),endBlock.getRawEncoding()-beginBlock.getRawEncoding());
                        if (methodDeclStr.find(methodblock)!=string::npos && methodDeclStr.find("@weakify(self)")==string::npos && methodDeclStr.find("__weak")==string::npos && node->getSelector().getAsString().find("animateWithDuration:")==string::npos) 
                        {
                            addViolation(blockDecl, this,"block中强引用了self,注意使用weak修饰self"); 
                        }
                    }
                }
            }
        }
    }
    return true;
}
bool VisitObjCMethodDecl(ObjCMethodDecl *node) {
    this->methodDecl = node;
    return true; 
}

你可能感兴趣的:(iOS LLVM-Clang 浅谈)