您好!
欢迎来到京东云开发者社区
登录
首页
博文
课程
大赛
工具
用户中心
开源
首页
博文
课程
大赛
工具
开源
更多
用户中心
开发者社区
>
博文
>
移动端代码质量提升探索-代码静态分析
分享
打开微信扫码分享
点击前往QQ分享
点击前往微博分享
点击复制链接
移动端代码质量提升探索-代码静态分析
自猿其说Tech
2021-11-29
IP归属:未知
112560浏览
精益创新
计算机编程
### 1 技术背景 随着版本的迭代,App包越来越大,如何从代码层面分析删除无用代码? 大家工作中,都碰到过因为代码不规范,代码规范执行不到位引起的问题或事故。那么如何高效执行规范?如何监管?所以,静态分析,就进入了我们的视线。 ### 2 如何做静态分析 经调研Clang提供的Clang AST接口非常强大,通过Clang提供的丰富接口功能,就可以开发出静态分析工具,不仅可以用于无用代码分析,还可以用于代码规范检查、插桩统计方法耗时、OCRunner、DynamicCocoaSDK等热更新方案。 ### n3 Clang是什么 Clang是一个基于LLVM的C、C++、Objective-C编译器。是LLVM的子项目。他的目标是提供一个GNU编译器套装(GCC)的替代品。如下所示: - 广义的LLVM是一个完整的编译器架构,包括了前端、后端、优化器、众多库函数以及模块。 - 狭义的LLVM就是聚焦于编译器后端功能的一系列模块和库 - LLVM是一个模块化可重用的编译器和工具链技术的集合。LLVM计划启动于2000年,由伊利诺伊大学香槟分校的Chris Lattner (克里斯 拉特纳)主持开展。 ![](//img1.jcloudcs.com/developer.jdcloud.com/99f78a16-42ee-4014-a4d6-f88d5cee535920211129140852.png) Clang目标是提供一个GNU编译器套装(GCC)的替代品,这里需要讲一点历史: - 1988年乔布斯离开苹果创立NextSTEP公司,买下了OC语言的授权,扩展了GCC使之支持OC编译,并基于OC开发了AppKit与Foundation Kit等库。 - 1996年苹果收购NextSTEP,乔布斯回归苹果,OC等相关开发环境也带到了苹果,然后Apple对Objective-C语言新增的特性,GCC并不买账,不给实现,Apple 想做的很多功能,需要模块化的方式来调用GCC,但是GCC的代码耦合度太高。 - 于是,2005年Apple雇用Chris Lattner(LLVM项目主要发起人、Clang编译器作者、Swift语言之父)开始开发Clang。 ### 4 iOS编译流程 先来简单看一下iOS编译流程,静态分析就是在编译过程中 - 简单来说Clang编译器前端将C、C++、Objective-C等语言进行编译,输出LLVM IR中间代码 - 将LLVM IR 交给LLVM后端优化,生成对应平台的目标程序。 - 针对Clang编译器前端流程中的Clang AST语法树,进行分析,例如无用代码分析,主要思路就是在静态分析时从AST语法树中,找到未被显式调用到的方法。 - Xcode自带的Clang是不支持加载插件的,因此需要替换为自己编译的Clang,才能加载自定义Clang插件 ![](//img1.jcloudcs.com/developer.jdcloud.com/dd356656-6798-4aa6-a7df-fc0f021eb1ea20211129141148.png) ### 5 编译Clang ##### 5.1 Checkout LLVM工程 - https://github.com/llvm-mirror/llvm: 目前上大部分教程使用此仓库,且需要单独clone clang、clang-tools-extra、compiler-rt等工具,此仓库为旧仓库,已无法使用 - https://github.com/llvm/llvm-project: 新的官方工程仓库,对照readme即可编译成功 - https://github.com/apple/llvm-project: Apple fork of llvm-project,iOS代码静态分析,当然使用Apple的fork #### 5.2 build LLVM and Clang - cd llvm-project,git checkout apple/stable/20200714 - mkdir build、cd build - cmake –G Xcode –DLLVM_ENABLE_PROJECTS="clang;clang-tools-extra;libcxx;libcxxabi" ../llvm - 打开LLVM.xcodeproj,选择Automatically create schemes,选择ALL_BUILD编译 - 此处编译1小时左右,总共文件大小约为40G,编译产物即为Clang编译器 #### 5.3 替换Xcode中的Clang ![](//img1.jcloudcs.com/developer.jdcloud.com/e505e43f-9abd-4e21-98fc-b9c6c227272d20211129141342.png) ps:部分教程中使用的XcodeHacking,在新版本Xcode已经不适用。 ### 6 创建一个Clang插件 1)/llvm-project/clang/examples/CMakeLists.txt,在此文件中add_subdirectory(CodingStylePlugin) ``` if(NOT CLANG_BUILD_EXAMPLES) set_property(DIRECTORY PROPERTY EXCLUDE_FROM_ALL ON) set(EXCLUDE_FROM_ALL ON) endif() add_subdirectory(clang-interpreter) add_subdirectory(PrintFunctionNames) add_subdirectory(AnnotateFunctions) add_subdirectory(Attribute) add_subdirectory(CodingStylePlugin) ``` 2)/llvm-project/clang/examples/CodingStylePlugin/目录下新建CMakeLists.txt、CodingStylePlugin.cpp, CodingStylePlugin.exports等文件 CodingStylePlugin/CMakeLists.txt内容如下: ``` # If we don't need RTTI or EH, there's no reason to export anything # from the plugin. if( NOT MSVC ) # MSVC mangles symbols differently, and # CodingStylePlugin.export contains C++ symbols. if( NOT LLVM_REQUIRES_RTTI ) if( NOT LLVM_REQUIRES_EH ) set(LLVM_EXPORTED_SYMBOL_FILE ${CMAKE_CURRENT_SOURCE_DIR}/CodingStylePlugin.exports) endif() endif() endif() add_llvm_library(CodingStylePlugin MODULE CodingStylePlugin.hpp CodingStylePlugin.cpp PLUGIN_TOOL clang) if(LLVM_ENABLE_PLUGINS AND (WIN32 OR CYGWIN)) set(LLVM_LINK_COMPONENTS Support ) clang_target_link_libraries(CodingStylePlugin PRIVATE clangAST clangBasic clangFrontend ) endif() ``` 3)重新运行cmake –G Xcode –DLLVM_ENABLE_PROJECTS="clang;clang-tools-extra;libcxx;libcxxabi" ../llvm 4)Xcode项目中配置插件: ![](//img1.jcloudcs.com/developer.jdcloud.com/9cf3a0bb-d607-49bd-a9d9-b31080b718bc20211129141521.png) 5)cocoapods额外配置: ![](//img1.jcloudcs.com/developer.jdcloud.com/57416918-79db-4091-8050-36eb04719c5220211129141758.png) ### 7 编写Clang插件(此节demo为代码风格检查) #### 7.1 抽象语法树AST ![](//img1.jcloudcs.com/developer.jdcloud.com/aaba605f-6db5-486b-97a8-a2cff842217c20211129141814.png) 其中Decl是声明、Stmt是语句。这里我们可以清晰的看到类的声明、类的实现、方法的定义、方法的调用对应的节点,也声明了隐式参数self _cmd。其中ObjCMessageExpr,是一个标准的OC消息发送表达式。 #### 7.2 注册插件 ```objective-c static clang::FrontendPluginRegistry::Add<CodingStylePlugin::CodingStyleASTAction> X("CodingStylePlugin", "Coding Style Plugin"); ``` #### 7.3 自定义继承 ```objective-c #include<iostream> #include<sstream> #include<typeinfo> #include "clang/Frontend/FrontendPluginRegistry.h" #include "clang/AST/AST.h" #include "clang/AST/ASTConsumer.h" #include "clang/Frontend/CompilerInstance.h" #include "clang/AST/RecursiveASTVisitor.h" using namespace clang; using namespace std; using namespace llvm; namespace CodingStylePlugin { // 前序或后序深度优先搜索整个抽象语法树的基类 class CodingStyleASTVisitor : public RecursiveASTVisitor<CodingStyleASTVisitor> { private: ASTContext *context; public: void setContext(ASTContext &context); bool VisitDecl(Decl *decl); }; // 用于客户读取抽象语法树的抽象基类 class CodingStyleASTConsumer : public ASTConsumer { private: CodingStyleASTVisitor visitor; void HandleTranslationUnit(ASTContext &context); }; // 基于consumer的抽象语法树前端Action抽象基类 class CodingStyleASTAction : public PluginASTAction { public: unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &Compiler, llvm::StringRef InFile); bool ParseArgs(const CompilerInstance &CI, const std::vector<std::string>& args); }; } ``` #### 7.4 根据自身需要重载以下等方法,实现自定义的分析逻辑 此demo实现了一个简单的代码风格检查逻辑,通过搜索整个抽象语法树,检测类名不能包含 '_' ```objective-c namespace CodingStylePlugin { // visitor void CodingStyleASTVisitor::setContext(ASTContext &context) { this->context = &context; } bool CodingStyleASTVisitor::VisitDecl(Decl *decl) { string fileName = this->context->getSourceManager().getFilename(decl->getSourceRange().getBegin()).str(); if (fileName.find(srcRootPath)!=string::npos) { // DiagnosticsEngine &diagEngine1 = context->getDiagnostics(); // unsigned int diagID1 = diagEngine1.getCustomDiagID(DiagnosticsEngine::Warning, "fileName find srcRootPath : %0"); // diagEngine1.Report(diagID1) << fileName; if (isa<ObjCInterfaceDecl>(decl)) { ObjCInterfaceDecl *interfaceDecl = (ObjCInterfaceDecl *)decl; StringRef name = interfaceDecl->getName(); if ( name.find("_") != string::npos) { DiagnosticsEngine &diagEngine = context->getDiagnostics(); string tmpName(name.str()); tmpName.erase(remove(tmpName.begin(), tmpName.end(), '_'), tmpName.end()); StringRef replacement(tmpName); SourceLocation nameStart = interfaceDecl->getLocation(); SourceLocation nameEnd = nameStart.getLocWithOffset(name.size()-1); FixItHint fixItHint = FixItHint::CreateReplacement(SourceRange(nameStart, nameEnd), replacement); unsigned diagID = diagEngine.getCustomDiagID(DiagnosticsEngine::Warning, "类名不能包含 '_'"); diagEngine.Report( interfaceDecl->getLocation(), diagID).AddFixItHint(fixItHint); } } } return true; } // consumer void CodingStyleASTConsumer::HandleTranslationUnit(ASTContext &context) { visitor.setContext(context); visitor.TraverseDecl(context.getTranslationUnitDecl()); } // Action unique_ptr<ASTConsumer> CodingStyleASTAction::CreateASTConsumer(CompilerInstance &Compiler, llvm::StringRef InFile) { return unique_ptr<CodingStyleASTConsumer>(new CodingStyleASTConsumer); } bool CodingStyleASTAction::ParseArgs(const CompilerInstance &CI, const std::vector<std::string> &args) { return true; } } ``` ### 8 Clang插件的其他应用 #### 8.1 无用代码分析 通过设计protocol继承体系的数据结构,class继承体系的数据结构,方法method调用数据结构。以下是通过静态分析生成class继承体系与protocol继承体系的代码片段,可根据注释阅读: ```objective-c //通过UnusedCodeUtil工具类进行数据存储 bool UnusedCodeASTVisitor::VisitDecl(Decl *decl) { string fileName = this->context->getSourceManager().getFilename(decl->getSourceRange().getBegin()).str(); string srcRootPath = UnusedCodeUtil::srcRootPath(); if (isa<ObjCInterfaceDecl>(decl) || isa<ObjCImplDecl>(decl) || isa<ObjCProtocolDecl>(decl)) { objcClassInterface = ""; objcClassImpl = ""; objcMethod = ""; objcIsInstanceMethod = true; objcMethodFileName = ""; objcProtocol = ""; } //interface if (isa<ObjCInterfaceDecl>(decl)) { ObjCInterfaceDecl *interfaceDecl = (ObjCInterfaceDecl *)decl; objcClassInterface = interfaceDecl->getNameAsString(); string superClassName = interfaceDecl->getSuperClass() ? interfaceDecl->getSuperClass()->getNameAsString() : ""; vector<string> protocolVector; for (ObjCList<ObjCProtocolDecl>::iterator it = interfaceDecl->all_referenced_protocol_begin(); it != interfaceDecl->all_referenced_protocol_end(); it++) { protocolVector.push_back((*it)->getNameAsString()); } string jsonStr = UnusedCodeUtil::appendObjcClass(objcClassInterface, superClassName, protocolVector, fileName); } //impl if (isa<ObjCImplDecl>(decl)) { if (isa<ObjCCategoryImplDecl>(decl)) { ObjCCategoryImplDecl *categoryImplDecl = (ObjCCategoryImplDecl *)decl; objcClassImpl = categoryImplDecl->getClassInterface()->getNameAsString() + "+Category"; }else { ObjCImplDecl *implDecl = (ObjCImplDecl *)decl; objcClassImpl = implDecl->getNameAsString(); } } //category if (isa<ObjCCategoryDecl>(decl)) { ObjCCategoryDecl *categoryDecl = (ObjCCategoryDecl *)decl; objcClassInterface = categoryDecl->getClassInterface()->getNameAsString(); if (!categoryDecl->IsClassExtension()) { objcClassInterface = categoryDecl->getClassInterface()->getNameAsString() + "+Category"; } string superClassName = categoryDecl->getClassInterface()->getSuperClass() ? categoryDecl->getClassInterface()->getSuperClass()->getNameAsString() : ""; vector<string> protocolVector; for (ObjCList<ObjCProtocolDecl>::iterator it = categoryDecl->protocol_begin(); it != categoryDecl->protocol_end(); it++) { protocolVector.push_back((*it)->getNameAsString()); } string jsonStr = UnusedCodeUtil::appendObjcClass(objcClassInterface, superClassName, protocolVector, fileName); } //property if (isa<ObjCPropertyDecl>(decl)) { ObjCPropertyDecl *propertyDecl = (ObjCPropertyDecl *)decl; objcIsInstanceMethod = propertyDecl->isInstanceProperty(); if (objcClassInterface.length()) { string jsonStrGetter = UnusedCodeUtil::appendObjcClassInterface(objcClassInterface, propertyDecl->getGetterName().getAsString(), objcIsInstanceMethod); string jsonStrSetter = UnusedCodeUtil::appendObjcClassInterface(objcClassInterface, propertyDecl->getSetterName().getAsString(), objcIsInstanceMethod); } } //protocol if (isa<ObjCProtocolDecl>(decl)) { ObjCProtocolDecl *protocolDecl = (ObjCProtocolDecl *)decl; objcProtocol = protocolDecl->getNameAsString(); vector<string> protocolArray; for (ObjCProtocolList::iterator it = protocolDecl->protocol_begin(); it != protocolDecl->protocol_end(); it++) { protocolArray.push_back((*it)->getNameAsString()); } string currrentFile = context->getSourceManager().getFilename(protocolDecl->getSourceRange().getBegin()).str(); if (protocolDecl->hasDefinition() && context->getSourceManager().getFilename(protocolDecl->getDefinition()->getSourceRange().getBegin()) == context->getSourceManager().getFilename(protocolDecl->getSourceRange().getBegin())) { string jsonStr = UnusedCodeUtil::appendObjcProtocol(objcProtocol, fileName.find(srcRootPath)!=string::npos, protocolArray, fileName); } } //method通过bool UnusedCodeASTVisitor::VisitStmt(Stmt *stmt)继续处理 if (isa<ObjCMethodDecl>(decl)) { ObjCMethodDecl *methodDecl = (ObjCMethodDecl *)decl; objcIsInstanceMethod = methodDecl->isInstanceMethod(); objcMethod = methodDecl->getSelector().getAsString(); if (objcClassInterface.length()) { string jsonStr = UnusedCodeUtil::appendObjcClassInterface(objcClassInterface, objcMethod, objcIsInstanceMethod); }else if (objcProtocol.length()) { string jsonStr = UnusedCodeUtil::appendObjcProtocolInterface(objcProtocol, objcMethod, objcIsInstanceMethod); }else if (objcClassImpl.length()) { if (fileName.find(srcRootPath)!=string::npos && methodDecl->hasBody()) { objcMethodFileName = fileName; string jsonStr = UnusedCodeUtil::appendObjcClassMethodImpl(objcClassImpl, objcMethod, objcIsInstanceMethod, objcMethodFileName); } } } return true; } ``` 将上一步生成的数据,通过脚本再进行整理筛选,然后通过main函数,appdelegate、系统protocol等入口递归分析找出已用的方法,剩下的就是无用代码 #### 8.2 部分热更新方案 OCRunner核心流程图 ![](//img1.jcloudcs.com/developer.jdcloud.com/eceec216-4074-4397-aaef-b7b74dfeb78120211129142104.png) 滴滴DynamicCocoaSDK核心流程图 ![](//img1.jcloudcs.com/developer.jdcloud.com/68c14e9b-6570-47bb-acbc-ac9a40a0941c20211129142122.png) 包括美团的Flutter动态化框架,也是类似的思想。虽然编译、解释执行所用的编译器不一样 ![](//img1.jcloudcs.com/developer.jdcloud.com/1333bd68-3a5c-44b1-aaea-868e353143dd20211129142144.png) ### 9 总结 今天的这篇文章,跟大家分享了Clang提供了什么能力,从中可以看出,Clang提供的能力都是基于Clang AST接口的。以这个接口为基础,利用Clang Plugin、LibClang、LibTooling这些封装好的工具,就足够我们开发出满足静态分析的工具了。 ------------ ###### 自猿其说Tech-京东物流技术发展部 ###### 作者:崔洪禄
原创文章,需联系作者,授权转载
上一篇:Dubbo负载均衡策略之 平滑加权轮询
下一篇:京东APP秒级百G日志传输存储架构设计与实战
相关文章
移动端代码质量提升探索-代码静态分析
架构研究:中台与前台的依赖方向(以2个中台api 设计为例)
互联网行业项目经理钻石能力发展模型
自猿其说Tech
文章数
426
阅读量
2149964
作者其他文章
01
深入JDK中的Optional
本文将从Optional所解决的问题开始,逐层解剖,由浅入深,文中会出现Optioanl方法之间的对比,实践,误用情况分析,优缺点等。与大家一起,对这项Java8中的新特性,进行理解和深入。
01
Taro小程序跨端开发入门实战
为了让小程序开发更简单,更高效,我们采用 Taro 作为首选框架,我们将使用 Taro 的实践经验整理了出来,主要内容围绕着什么是 Taro,为什么用 Taro,以及 Taro 如何使用(正确使用的姿势),还有 Taro 背后的一些设计思想来进行展开,让大家能够对 Taro 有个完整的认识。
01
Flutter For Web实践
Flutter For Web 已经发布一年多时间,它的发布意味着我们可以真正地使用一套代码、一套资源部署整个大前端系统(包括:iOS、Android、Web)。渠道研发组经过一段时间的探索,使用Flutter For Web技术开发了移动端可视化编程平台—Flutter乐高,在这里希望和大家分享下使用Flutter For Web实践过程和踩坑实践
01
配运基础数据缓存瘦身实践
在基础数据的常规能力当中,数据的存取是最基础也是最重要的能力,为了整体提高数据的读取能力,缓存技术在基础数据的场景中得到了广泛的使用,下面会重点展示一下配运组近期针对数据缓存做的瘦身实践。
自猿其说Tech
文章数
426
阅读量
2149964
作者其他文章
01
深入JDK中的Optional
01
Taro小程序跨端开发入门实战
01
Flutter For Web实践
01
配运基础数据缓存瘦身实践
添加企业微信
获取1V1专业服务
扫码关注
京东云开发者公众号