
简介Understand 2.0是一款面向开发者的代码分析与理解工具专为大型项目结构梳理、旧代码维护和重构前评估而设计。它支持 C、C、Java、C#、Python 等多种语言能够生成类图与调用图清晰呈现类、函数、变量之间的关系和调用层次同时检查冗余代码、未使用变量及可能出现的空指针异常帮助定位潜在问题。压缩包采用 zip 格式共2个文件内含 Windows 32 位安装程序和 HTML 格式的使用说明文档整个压缩包约43.61MB下载后即可对照说明完成部署。已有372人学习适用于个人开发者快速上手也适合团队借助版本控制集成与报告输出来统一代码理解、开展代码审查。借助其可视化界面与分析引擎用户能降低接手陌生代码的适应成本提升调试与优化效率为后续重构和团队协作提供有力支持。1. 为什么代码分析不能只靠人眼understand2.0 把“读代码”变成“查代码”代码量一旦过十万行人眼读代码的速度就永远追不上需求变更的速度。understand2.0 这类代码分析工具核心思路是把“读代码”变成“查代码”先让工具把整个仓库的符号表、调用关系、依赖关系全部抽出来再按问题去精确检索。我接手一个八年前的老系统时被问“这个模块到底被谁调用了”在 IDE 里翻了一个下午没翻明白换工具之后一条查询十几秒出结果。它解决的是代码理解与质量评估问题查调用链、找循环依赖、算圈复杂度、看架构分层都不用靠猜。适合做技术债盘点的人、刚接手旧系统的开发者、要给重构方案找依据的工程师。这章只讲明白它能干什么具体怎么建索引、参数怎么配、坑在哪儿往下看。2. understand2.0 的数据模型符号表、引用关系与指标计算2.1 多语言解析与统一符号表understand2.0 能同时消费多种语言。C/C、Java、Python、JavaScript、PHP、C#、Objective-C 是常见组合实际项目里经常出现一个仓库混着 C 和 Python 的情况工具会把它们全部导到同一套“实体-引用”模型里。这里说的实体是文件、类、函数、变量、宏、命名空间这些代码里的“东西”引用是实体之间的关系比如函数 A 调用了函数 B类 C 继承了类 D。它的解析过程大致是先做词法与语法分析再构建符号表最后把符号之间的引用关系存成图。符号表是整套数据的骨架understand2.0 用统一符号表抹平了跨语言的差异。这意味着你在看一个 Python 模块的时候能一路跟踪到它底层调用的 C 实现只要两边都被纳入同一个工程。这里有一个容易被忽略的价值点跨语言调用也能被完整跟踪。比如 C 侧通过扩展机制调用了 Python 模块在调用图里能把两层关系拉通前提是两种语言的解析器都认识对应语法。我一般在混编项目里专门建一个全量工程用它替代“人工翻代码理依赖”的流程准确率比人盯着看高很多。为什么不建议直接拿正则去扫代码正则只能做文本匹配遇到同名函数重载、宏展开、条件编译就会误报。understand2.0 做的是语法级分析能区分“声明一个函数”和“调用一个函数”这是它和 grep、正则脚本的本质差异。理解这一点你才会明白后面所有查询功能为什么比 IDE 自带的全局搜索可靠。2.2 实体、引用、依赖看懂核心数据结构打开 understand2.0 的数据库底层其实是四类核心元素实体Entity、引用Reference、层级Hierarchy和指标Metric。实体是节点引用是边层级描述父子关系指标是挂在节点上的数值。这套结构和你在工程里画的“模块依赖图”是一回事只不过它是由解析器从真实代码生成而不是人手工维护。实体类型含义典型例子File源文件legacy_system/network.cppClass类/结构体HttpClientFunction函数/方法send_request()Variable全局/局部变量g_timeout_msMacro宏定义BUFFER_SIZENamespace命名空间/包network::transport依赖关系是从引用关系聚合出来的。比如模块 A 的 10 个函数都调用了模块 B 的函数那么依赖图上就会有一条 A→B 的边。understand2.0 的依赖图不是简单地把 import/include 关系画出来而是按真实调用和类型引用聚合所以能看到“间接依赖”和“运行时才发生的调用”。实际看代码时我一般会先打开某个核心文件的“被引用列表”也就是反向引用。它会告诉你这个文件被谁 include、这个函数被谁调用、这个类被谁实例化。反向引用是理解老系统最趁手的工具比从入口函数往下一层层读快得多。2.3 指标计算圈复杂度、扇入扇出是怎么算出来的指标是 understand2.0 的另一个强项。它内置几十种代码度量指标最常用的三类是规模指标、结构指标和复杂度指标。规模指标就是代码行数、语句数、注释行数结构指标是扇入扇出、嵌套深度复杂度指标就是圈复杂度。圈复杂度用“独立路径数”来衡量一个函数的复杂程度。解析器会先构造函数的控制流图然后按公式计算V(G) E - N 2其中 E 是边的数量N 是节点数量。if、while、for、case 每多一个判断分支复杂度就上升。这个数值的含义很直接复杂度 10 以下的函数基本能读懂超过 20 的函数建议拆分超过 50 的函数基本属于“改一处坏三处”的重灾区。扇入扇出也值得多说一句。扇出是一个函数直接调用了多少个不同函数扇入是有多少个函数调用了它。扇出太高的函数往往承担了过多的职责扇入太高的函数超过 20 甚至 50通常是全局工具类改它的影响面会非常大。我在重构前必查这两个数它们直接决定了“要不要动这个文件”的风险等级。这些指标不是事后写脚本算出来的而是建索引时解析器顺手算完存进数据库的。所以查询速度很快几万行的老项目在普通笔记本上也能秒出结果。后面第 4 章我会演示怎么用这些指标快速定位坏味道。3. 安装与建索引命令行参数、增量更新与常见配置3.1 环境准备与安装understand2.0 的安装包根据平台区分Windows 上是 exe 引导安装程序Linux 上是压缩包加安装脚本。我一般在 Linux 服务器上跑批处理和分析任务所以后面的命令都以 Linux 为例。安装时有一个很容易踩的小坑安装路径不要带空格和中文。它底层涉及大量路径拼写一旦路径里有空格构建工程时会偶尔出现找不到数据库文件的报错。虽然新版对空格路径的兼容好了一些但没必要拿环境赌这个规规矩矩放到/opt/und2或者~/tools/und2下面最省心。安装完成后验证一下命令行是否可用。它的命令行工具叫und通常在安装目录的bin子目录下。执行版本检查命令cd /opt/und2/bin ./und -version如果能看到版本号输出说明安装正常。如果提示缺少动态链接库说明系统缺libtinfo之类的依赖用系统包管理器补上即可。这个验证动作我每次换新机器都强制走一遍避免后续所有命令都报“command not found”。3.2 创建分析工程从源码目录到数据库understand2.0 的工作方式是先建立一个工程文件后缀是.udb然后把源码目录挂进去最后执行索引分析。索引分析会把源码读一遍并生成数据库后续所有查询和指标都从数据库里取不再碰源码。命令行的创建语法如下cd /opt/und2/bin ./und -db /data/projects/legacy.udb -dir /data/code/legacy_system create这条命令里-db指定数据库文件的路径-dir指向源码根目录create表示新建工程。建好后执行分析./und -db /data/projects/legacy.udb analyzeanalyze会把-dir指向目录下的源码全部解析一遍。执行期间命令行会打印当前解析到哪个文件速度取决于工程规模。一个 30 万行的项目单核大概要十几分钟如果机器允许建议开多线程参数。./und -db /data/projects/legacy.udb -threads 8 analyze-threads 8让解析器用 8 个线程并行处理文件。这个参数在超大仓库上提升非常明显我处理的某跨平台系统从 40 分钟压到了 12 分钟。注意线程数不要超过机器物理核心数开太多反而会因为内存带宽争抢而变慢。3.3 增量更新与后台分析老项目不是一成不变的你分析完后同事又提交了几百行代码。重新全量分析一遍很浪费时间所以 understand2.0 提供了增量更新模式只重新解析有变动的文件以及受头文件变化影响的文件。./und -db /data/projects/legacy.udb -add /data/code/legacy_system analyze这里的关键是-add参数它告诉工具“这个目录比上次多了文件和改动”工具会比较文件时间戳只重算变动的部分。增量分析的耗时通常只有全量分析的十分之一到五分之一。我的习惯是白天写代码时随时增量更新晚上睡觉前挂一个全量分析做最终数据校准。分析过程如果特别长建议用nohup放到后台跑避免终端意外断开导致分析中断nohup ./und -db /data/projects/legacy.udb -threads 8 analyze /tmp/und_analyze.log 21 日志重定向到/tmp/und_analyze.log跑完去看这个日志的末尾有没有completed字样。如果中途报错日志里会有具体文件路径和失败原因最常见的是“解析器无法识别的语法”后面第 5 章讲怎么处理。另外命令行还支持直接导出指标数据不需要打开图形界面./und -db /data/projects/legacy.udb -metrics all /tmp/metrics_report.txt-metrics all表示导出所有内置指标输出到指定文件。这个文件是纯文本表格一行表示一个函数或类的所有指标数据后续用脚本分析或者导入 Excel 都很方便。我一般会把它留作每次巡检的基线数据对比版本之间的复杂度变化。4. 实操三连查调用链、抓循环依赖、揪坏味道4.1 用 Search 快速定位代码类 Search 语法与过滤器understand2.0 的图形界面里最常点开的是 Search 面板。它和 IDE 的搜索不一样IDE 只能搜文本它能搜实体类型、限定语言、按引用方向过滤。举个例子我想找所有调用send_request这个函数的地方Search 表达式这么写call: send_request冒号前是查询类型call冒号后是目标函数名。查询结果会列出所有调用方每条结果显示所在文件、行号和调用参数。如果想看更完整的信息可以加参数限定function: send_request这个查询会直接定位到函数定义本身包括它的返回类型、参数列表、文件位置。结合左侧的引用选项卡选中这个函数后逐条看即时的反向引用比在 IDE 里右键查找引用的结果更全因为宏展开和模板实例化带来的隐藏调用也会被识别。我排查问题时最常用的是跨文件调用链查询。先用function: x找到入口再用反向引用逐层往下追三步之内就能判断一个改动会波及多少个文件。Search 面板还支持正则过滤和文件路径过滤这个组合在定位特定模块时效率极高。以查询network目录下所有函数的调用点为例call: * network/*对新手来说记住一个技巧就够了先用实体类型缩小范围再用冒号后的名字做模糊匹配。*是通配符文件名可以用通配符拼出目录过滤条件。这个查询面板没有记忆功能所以我会把复杂查询保存为命名查询下次直接复用不用重新敲。4.2 用 Graph 看依赖环找到架构崩溃的元凶Graph 视图是 understand2.0 里最直观的模块它会画出函数与函数、文件与文件之间的调用和依赖关系。正常项目应该呈现为分层结构而老系统大多是乱成一团的关系网边线横七竖八。这时候第一步就是检查有没有循环依赖。循环依赖指的是 A 依赖 B、B 又依赖 A或者通过 C 绕一圈回到 A。循环依赖是架构腐化的头号信号它导致编译顺序必须靠技巧、重构时无法单独抽离模块。Graph 视图里有一个内置图叫做“依赖环检测”点击运行后工具会列出所有参与循环的文件或类。处理循环依赖的实操顺序我一般是在 Graph 里选择“层级图”按目录分层展示文件依赖。打开依赖环检测把环上的文件单独拉到一个分组。找到环上依赖关系最弱的一条边——往往是某个工具类被反向引用了——通过调整调用方向来破环。Graph 视图还支持“点击放大”和“隔离节点”双击某个文件只显示它的上下游适合在环里做局部判断。注意图里默认显示的边可能包含 include 和 call 两类建议在过滤条件里把边的类型切到“仅程序调用”因为 include 环和调用环的治理手段完全不同。include 环靠头文件结构调整调用环靠接口抽象混在一起看只会越看越乱。4.3 用 Metrics 揪出最该重构的类Metrics 是集成了所有指标的报表模块。它默认给出文件、类、函数三级维度每一行都附带行数、圈复杂度、扇入扇出、嵌套深度等列。我拿到一个新项目时会先把 Metrics 按“最大圈复杂度”降序排列排在前 20 的函数就是最需要重构的重灾区。指标预警值危险值圈复杂度1020单个函数行数100200扇出1020扇入2050嵌套深度48这张表是我自己的排查阈值不同团队可以按风险偏好调整。重点不是死记数字而是理解指标背后的含义圈复杂度高的函数改起来容易引入回归扇入高的函数一旦改错影响面是扇入那么多个调用方。指标报表还能做“版本对比”。上次导出的metrics_report.txt还在的话用 diff 对比两次的复杂度变化可以看到哪些函数在迭代中变得越来越复杂。有一次巡检我发现一个工具类的扇入从 15 涨到了 65查代码才发现大家都图省事把公共逻辑往它里面堆。这个趋势必须靠指标才能发现靠 code review 很难注意到数量级的增长。5. 避坑与排查索引翻车、乱码与指标失真的四段记录5.1 索引失败明明加了文件搜出来却是空现象分析命令执行完毕打开 Search 搜索文件名关键词结果列表为空或者 Metrics 报表完全没有这个文件的指标数据。原因最常见的是文件没有被工程识别。-dir指向的目录如果有嵌套子目录而工程配置里设了“排除目录”部分路径会被过滤掉。还有一个常见情况文件扩展名不在支持的语言扩展名列表里比如.cc、.hh这种 C 变体后缀如果工具配置里没有映射就会被静默跳过。解决打开工程的首选项检查源码目录和排除规则把排除项中的多余路径删掉再把文件扩展名映射补全.cc映射到 C、.hh映射到 C 头文件然后重新执行全量分析。判断是否生效先看分析日志末尾的统计它通常会打印“解析文件总数”把这个数和系统里实际源文件数对一下差别大的时候优先查排除规则。5.2 中文注释导出乱码数据库里是 UTF-8终端是 GBK现象打开源码没问题的中文注释在 understand2.0 的界面里显示成乱码导出指标报告后用cat看也是一堆问号。原因源码是 GB 编码explain 界面的默认编码是 UTF-8或者反过来。这是编码不一致导致的和工具本身的数据解析能力无关。它读取源文件的编码推断有时会走默认配置猜错了就把中文字节序列解释成了非法字符。解决把终端字符集切到与工具一致。我一般给 Linux 终端加环境变量export LANGzh_CN.UTF-8并确认源码文件统一转成 UTF-8 编码。批量转换用 iconv 命令iconv -f GBK -t UTF-8 legacy_system/network.cpp legacy_system/network.cpp.bak mv legacy_system/network.cpp.bak legacy_system/network.cpp转换完成之后再重新分析界面和报告里的中文就正常了。注意转换前先做一次全量备份iconv批量跑老代码时偶尔会遇到单个文件失败别把原始文件弄丢。5.3 指标失真圈复杂度显示异常低现象某个函数的圈复杂度指标算出来只有 2但这函数里明显有大量if/else分支。原因这个函数在条件编译分支里。代码被#ifdef DEBUG包起来了而解析时该宏没有被定义导致工具只把当前宏条件下可见的代码纳入了分析范围。它分析的是预处理之后的代码视图而不是原始字符流所以指标反映的是”当前宏配置下的真实代码“不是全部可能分支。解决在工程配置里设置宏定义列表把项目常用的宏比如DEBUG1、PLATFORM_LINUX提前喂给解析器。这样#ifdef中DEBUG分支下的函数体不会被剪裁掉指标才接近完整代码。设置后重新跑全量分析。如果不关心条件编译只想要文件名和调用关系也可以接受不配宏的结果但复杂度相关指标必须配了才可信。5.4 误报宏展开导致查询结果多了一条“幽灵引用”现象查某函数的引用点发现一个明显不相关的文件赫然在列仔细看源码里根本没有直接调用。原因这个文件里有一个宏宏体内部调用了目标函数。常见的比如SAFE_LOG(x) do { write_log(x); } while(0)宏被展开后调用点被算作write_log的引用。这是宏展开带来的“间接引用”不是工具判断错误但对于只想看直接调用关系的人来说它确实算误报。解决在 Graph 或 Search 的过滤设置里把引用类型改为“直接调用引用”宏展开引用就会被过滤。老系统里宏嵌套宏的情况很常见一个宏展开后会带出七八个底层函数调用直接看的话根本没法定位真实调用者。我处理这类问题的方法是先看“直接引用”理清主干后再单独开一个宏展开引用图层看那些被宏隐藏的调用点是否制造了隐藏依赖。6. 进阶用 Python API 把 understand2.0 变成团队巡检工具6.1 用 Python 打开数据库并抽取实体指标understand2.0 的 Python API 能让命令行和图形界面之外的能力全部脚本化。它提供的understand模块可以直接打开.udb数据库然后枚举实体、读取引用、取指标。对团队来说这意味着可以写一个巡检脚本每天定时跑一遍把高风险函数列表自动推到工作群。import understand as und db und.open(/data/projects/legacy.udb) for func in db.ents(function): complexity func.metric(cyclomatic) if complexity is not None and complexity 20: name func.name() file func.file().longname() print(f高风险函数: {file} - {name} (圈复杂度 {complexity})) db.close()这段代码的关键点有三个db.ents(function)是遍历所有函数实体func.metric(cyclomatic)是取出圈复杂度指标func.file().longname()定位到所在文件。metric返回None说明该实体没有对应指标所以要加is not None过滤。跑完这个脚本所有圈复杂度超过 20 的函数都会列出来。6.2 生成模块依赖清单并接入巡检流程除了复杂度还可以抽取模块间的依赖关系。下面这个脚本会统计每个顶层目录对哪些外部目录产生了引用并输出一个简单的依赖矩阵import understand as und from collections import defaultdict db und.open(/data/projects/legacy.udb) dep_map defaultdict(set) for ref in db.refs(file ~include ~inactive): if ref.file() and ref.dep().file(): src_dir ref.file().name().split(/)[0] dst_dir ref.dep().file().name().split(/)[0] if src_dir ! dst_dir: dep_map[src_dir].add(dst_dir) for src, deps in sorted(dep_map.items()): print(f{src}: {, .join(sorted(deps))}) db.close()db.refs(file ~include ~inactive)获取所有文件级引用并排除了 include 和 inactive 类型。ref.file()是引用源ref.dep()是引用目标split(/)[0]取顶层目录名作为模块名。这个脚本输出的清单可以直接作为架构评审的输入哪些模块偷偷跨层引用了不该依赖的东西一眼就能看出来。把这两个脚本组合到一个 shell 脚本里用 cron 定时执行就是最简单的日常巡检。从那以后我每次接新项目都会先花一个小时把工程建好、把巡检脚本搭起来再开始看业务代码。数据结构清晰了复杂度和依赖图谱摆在那里比任何“系统很乱”的抱怨都有说服力。希望帮到你。本文还有配套的精品资源点击获取