news 2026/10/15 21:50:10

Flutter测试报告鸿蒙化适配:JUnit XML生成与Jenkins集成实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Flutter测试报告鸿蒙化适配:JUnit XML生成与Jenkins集成实战

做 Flutter 相关开发的团队,基本都绕不开测试报告展示这门功课。我用 junitreport_maintained 在常规 Flutter 工程里做测试报告生成已经很久了,它能把 flutter test 跑出来的结果落成标准 JUnit XML,Jenkins 直接拿它渲染出历史趋势、失败率这些面板。可一旦要把这套东西搬到鸿蒙设备上跑自动化,事情就完全不是一回事了。鸿蒙的测试执行环境、文件系统、工程结构都和 Android/iOS 有差异,原库拿过来直接跑基本会挂在“报告写不进去”“路径找不到”这类问题上。本文把我做 junitreport_maintained 鸿蒙化适配的完整过程记录下来,包括接口抽象改造、端侧 XML 生成、多设备报告合并,以及和 Jenkins 流水线的对接,适合正在做鸿蒙端 Flutter 自动化测试的同学参考。

1. 先说清楚:为什么 JUnit XML 在鸿蒙上不能直接跑

1.1 junitreport_maintained 原本的工作机制

junitreport_maintained 这个库本身做的事情并不复杂,核心就是充当一个“事件流转文件”的转换器。flutter test 在运行时如果带上--machine参数,会向标准输出吐出一串 JSON 事件流,每个事件代表一次测试的开始、结束、通过、失败、跳过。junitreport_maintained 做的事情就是监听这个事件流,在内存里构建测试模型:哪个测试套件里有哪些用例、每个用例跑了多久、如果失败了具体在文件的哪一行哪一列挂了,然后等测试进程全部跑完之后,把这个模型序列化成一个 XML 文档,写到指定路径。

这个机制里暗含了三个前置假设,也是后面鸿蒙化时要逐一击破的点:

  • 它假定标准输出可以被稳定读取,并且事件流是完整的、不会被吞掉。
  • 它假定目标文件系统是常规的可写目录,不区分设备沙箱和宿主环境。
  • 它假定报告文件最终能被 CI 节点拿到,所以生成路径必须可被外部访问。

在 Android 和 iOS 的传统 Flutter 工程里,这三个假设基本都能直接满足,因为测试大多在宿主机侧完成,或者通过 adb/xcrun 同步产物。但鸿蒙的工程模型不一样,后面细说。

1.2 鸿蒙环境差在哪:三个绕不开的坎

我把适配过程中碰到的差异归纳成三类,理解这三类,后面看代码就不会懵。

第一,执行形态不同。鸿蒙上跑 Flutter 测试,通常不是直接在宿主机执行 flutter test,而是要在设备或模拟器上安装测试专用 HAP,再通过 hvigor 或 DevEco 的测试面板触发。就算你走命令行,命令的参数体系和结果输出位置也有自己的规矩。这就导致原来“一个管道符把 flutter test 的输出喂给报告生成器”的做法,在鸿蒙环境下并不总是成立的,报告生成器必须能独立启动,并且能从指定输入源读取事件流。

第二,文件系统不同。鸿蒙应用跑在设备上时,默认情况下应用只能读写自己的沙箱目录,直接写/sdcard/或者相对路径都有权限问题。junitreport_maintained 默认写的路径如果不改,在鸿蒙设备上大概率拿到 permission denied,或者更阴间的“写下去但拉不出来”。这不是库有 bug,是运行时对文件访问的约束本来就不一样。

第三,插件模型不同。原库在传统 Flutter 里可能依赖了一些基于 Android/iOS 原生实现的能力,鸿蒙环境下这些原生实现不存在,需要走鸿蒙侧的 API。更麻烦的是,很多 Flutter 库的鸿蒙适配实际上是通过兼容层完成的,部分 dart:io 接口在兼容层上行为不一致,不是报错,而是静默返回错误结果,排查起来比较难受。

理解完这三个差异,适配的总体思路就很清晰了:不重写解析核心,而是把“文件写入”“输入源获取”这些平台相关能力从库里抽离出来,让库的核心逻辑在鸿蒙环境下保持原样,只替换平台适配层。

2. 适配前准备:环境核查与依赖盘点

2.1 工具链怎么搭

适配 junitreport_maintained 之前,我先把工具链整理了一遍。需要一个能正常编译鸿蒙 Flutter 工程的开发环境,我这边用的是 DevEco Studio 配合官方鸿蒙 SDK,同时安装了针对鸿蒙的 Flutter 工具链分支。需要注意,当前社区里 Flutter 在鸿蒙平台的适配有多个来源,不同分支的 API 覆盖度有差异,建议先用官方渠道确认版本兼容关系,再动手改代码。

验证工具链是否可用的最快方式,是跑一个空的鸿蒙 Flutter 工程,执行一次最简单的 flutter test(哪怕只有一个恒通过的用例)。如果这一步能在设备或模拟器上稳定跑完,再往下走。我见过不少同学上来就改库,结果连基础测试都跑不起来,浪费了大量时间。

2.2 依赖核查:适配前必须过一遍的清单

在动手改库之前,我列了一张核查清单,每项都对着原库源码重新确认过。这张表建议你也过一遍,至少能少走一半弯路:

核查项原库中的依赖鸿蒙环境下的表现处理方式
dart:io 文件写入File、Directory沙箱路径受限,部分路径写不了抽象出 ReportSink 接口,鸿蒙端用专用 API 实现
标准输入流stdin命令形态不同,直接管道不可行支持从文件和 stdin 双输入源读取
进程调用Process.run调用系统命令可能受限制尽量用纯 Dart 实现核心逻辑,避免依赖外部命令
路径分隔符/ 与 \鸿蒙设备上有自己的路径规范路径统一走参数注入,不硬编码
时间戳格式本地时区设备时区可能不同,影响 XML 时间字段统一用 UTC 生成,避免时区偏差
XML 转义内置实现基本无差异保持原逻辑,但补充特殊字符测试用例

这张表的价值在于把“不确定因素”提前锁定成“确定的适配点”。比如时间戳,我最初根本没意识到会出问题,结果在设备上生成的 XML 里时间比本地时间慢了整整 8 小时,Jenkins 的趋势图全部对不上,后来统一改成 UTC 才解决。这种坑在排查阶段特别耗时间,提前在清单里标出来,能让后面的改造过程顺畅很多。

3. 核心改造:从接口抽象到端侧报告生成

3.1 先把平台相关逻辑抽出来

适配的第一步,是把原库里和平台强相关的逻辑抽成接口。原库的报告写出逻辑集中在几个文件里,我做的第一件事是定义一个抽象接口,把“把字符串内容写到某个目标”这件事和具体实现解耦。

abstract class ReportSink { /// 将报告内容写入目标位置。 /// /// [target] 由调用方传入,可以是文件路径、URI 或设备专用标识。 /// 具体怎么解释这个 target,由实现类决定。 Future<void> writeReport(String content, String target); }

这个接口简单到有点“简陋”,但它把最重要的自由度留给了实现方。在传统 Flutter 环境下,实现类可以继续用File(target).writeAsString(content)完成原来的行为;在鸿蒙环境下,实现类则可以换成鸿蒙侧的文件 API,或者把内容写到沙箱目录再由测试框架统一拉取。核心解析逻辑完全不需要动。

这里有一个设计取舍值得展开说:为什么不用“在接口里加一个writeToDevice方法”这种方案?因为加方法意味着所有实现类都要改,而且很容易把平台语义泄漏到核心逻辑里。我选择的方式是反过来的:核心逻辑只依赖ReportSink抽象,具体平台在组装时注入实现。这样一来,原库的测试用例和逻辑分支全部保留,回归风险最小。

3.2 鸿蒙端文件写入实现

鸿蒙端的ReportSink实现,我参考了鸿蒙应用沙箱的访问规则。在鸿蒙环境里,应用可以访问自己的沙箱目录,通常通过特定的文件路径接口获取。下面这段代码是我在适配工程里使用的一个参考实现,你根据自己的 SDK 版本做微调即可:

class HarmonyReportSink implements ReportSink { @override Future<void> writeReport(String content, String target) async { // 这里不是直接 new File(target),而是先解析目标路径。 // 如果 target 以特定前缀开头,视为鸿蒙沙箱内路径; // 否则使用当前应用可访问的缓存目录。 final String resolvedPath = resolveTargetPath(target); // 实际写入逻辑,使用鸿蒙侧提供的文件写入能力。 // 需要注意:写入后最好确认文件确实存在且大小不为 0, // 否则后续 CI 拉取时会拿到一个空文件,很难排查。 final bool success = await writeDeviceFile(resolvedPath, content); if (!success) { throw Exception('failed to write report at $resolvedPath'); } } }

你不要纠结writeDeviceFile这个函数的具体实现,因为不同鸿蒙 SDK 版本提供文件写入的方式有差异,有的是同步接口,有的是异步接口,有的还需要先申请目录权限。关键是把握两点:第一,路径必须解析到应用可访问的范围内;第二,写完之后要做一次“回读校验”,确认报告文件真实可用。我在真机上踩过“写的时候明明成功,文件大小始终是 0”的坑,后来就是靠这段回读校验暴露出来的。

3.3 JUnit XML 生成核心逻辑

报告生成这部分是原库擅长的领域,我基本没有重写,只是把入口和配置方式调整了一下,让它能在鸿蒙环境下被单独调用。核心逻辑维持“模型构建 -> XML 序列化”两步走。下面这段代码是生成 JUnit XML 的骨架示例,展示了最关键的部分:

class JUnitXmlDocument { final List<TestSuite> suites; String toXmlString() { final buffer = StringBuffer(); buffer.writeln('<?xml version="1.0" encoding="UTF-8" ?>'); buffer.writeln('<testsuites disabled="0" errors="$totalErrors" failures="$totalFailures" tests="$totalTests" time="$totalTime">'); for (final suite in suites) { buffer.writeln( '<testsuite name="${escapeXml(suite.name)}" tests="${suite.cases.length}" ' 'failures="${suite.failureCount}" errors="${suite.errorCount}" time="${suite.time}">', ); for (final testCase in suite.cases) { buffer.writeln( '<testcase classname="${escapeXml(testCase.className)}" ' 'name="${escapeXml(testCase.name)}" time="${testCase.time}">', ); if (testCase.failure != null) { buffer.writeln('<failure message="${escapeXml(testCase.failure.message)}">${escapeXml(testCase.failure.detail)}</failure>'); } buffer.writeln('</testcase>'); } buffer.writeln('</testsuite>'); } buffer.writeln('</testsuites>'); return buffer.toString(); } }

这段代码里的escapeXml函数只是把&、<、>、"、'这些特殊字符替换成对应的 XML 实体,标准的实现方式,但很容易被忽略。测试用例的失败信息里出现<或&是家常便饭,如果不转义,Jenkins 解析 XML 时会直接报格式错误,整个报告都加载不出来。原库大概率已经有转义逻辑,但我在适配时仍然专门补了测试用例覆盖这个场景。

有个细节值得说一下:JUnit XML 对time属性有格式要求,Jenkins 会把它解析成浮点数,单位是秒。我在适配时发现有些用例执行时间极短,会输出类似0.000000的值,Jenkins 面板上就会显示成 0,看起来像“没跑”。后来我在生成时做了个规范化:如果时间小于 0.001,至少显示0.001,这样趋势图才有意义。

3.4 CLI 入口与命令行集成

原库原本的 CLI 用法主要是通过管道接收 flutter test 的事件流。鸿蒙环境下管道不一定方便,我给适配版加了一个“同时支持文件输入和 stdin 输入”的入口。这样在设备上可以先跑测试并把事件流写到临时目录,再调用报告生成器读取该文件生成 XML;在 CI 上也可以直接用管道方式运行。入参示例如下:

# 方式一:文件作为输入源(适合鸿蒙设备端) dart run junitreport_maintained_harmony:main \ --input events.json \ --output reports/junit.xml # 方式二:标准输入流(适合 CI 宿主机) flutter test --machine | \ dart run junitreport_maintained_harmony:main \ --output reports/junit.xml

这里没有用原库的默认命令,而是新建了独立入口,原因很实际:在适配期如果能和原库入口并存,方便对照验证行为,出了问题可以快速回到原库跑一遍确认是不是适配引入的问题。等你验证充分了,再考虑要不要直接替换原入口。

4. 多设备报告汇总:端侧产物怎么合并成一份标准 XML

4.1 汇总策略与目录约定

自动化测试只要涉及多台设备或者多个模块,报告就只有“汇总后”才有完整的价值。单台设备的报告只能说明这一台设备上用例过没过,管理者想看的往往是整体通过率、失败用例分布、哪台设备最不稳定。

我采用的策略是“设备内分散生成,CI 侧统一合并”。也就是说,每台设备在端侧生成自己的 JUnit XML 文件,CI 构建节点通过设备管理平台把文件拉取到固定目录,再跑一个合并脚本把所有 XML 合成一份总报告。目录约定如下:

reports/ device_serial_A/ junit.xml device_serial_B/ junit.xml merged/ junit-all.xml

这个约定虽然简单,但解决了一个很现实的问题:如果所有设备把报告都写到同一个文件,后拉取的那台设备会覆盖前一台设备的结果,最终报告里永远只有一台设备的测试数据。分目录存放从根上规避了并发覆盖问题,也让排查单台设备问题时能直接定位到对应文件。

4.2 合并脚本示例

合并脚本我用 Python 写的,因为 CI 节点上 Python 基本是标配,不需要额外安装依赖。核心思路是用标准库xml.etree.ElementTree解析所有 XML,把测试套件的节点取出来,挂到新的testsuites根节点下,再重新写一个文件。

import glob import xml.etree.ElementTree as ET def merge_junit(input_pattern, output_file): root = ET.Element("testsuites") total_tests = 0 total_failures = 0 total_errors = 0 total_time = 0.0 for path in glob.glob(input_pattern): tree = ET.parse(path) suite_root = tree.getroot() # 兼容什么根节点都有的情况,统一取 testsuite 或直接取根节点 suites = suite_root.findall("testsuite") if suite_root.tag == "testsuites" else [suite_root] for suite in suites: root.append(suite) total_tests += int(suite.get("tests", 0)) total_failures += int(suite.get("failures", 0)) total_errors += int(suite.get("errors", 0)) total_time += float(suite.get("time", 0)) root.set("tests", str(total_tests)) root.set("failures", str(total_failures)) root.set("errors", str(total_errors)) root.set("time", f"{total_time:.6f}") tree = ET.ElementTree(root) tree.write(output_file, encoding="UTF-8", xml_declaration=True) if __name__ == "__main__": merge_junit("reports/device_*/junit.xml", "reports/merged/junit-all.xml")

这个脚本在合并时要注意两个问题。第一,不同设备生成的 XML 里testsuite的name属性最好带上设备标识,否则两个设备跑同一个测试模块,合并后 Jenkins 上会显示两套同名套件,虽然也能看,但鼠标点进去分不清谁是谁。我建议端侧生成时就把设备名拼进 suite name,从源头解决。第二,时间戳和耗时属性在合并脚本里用浮点数重新累加,不要直接用字符串拼接,否则 Jenkins 解析时精度对不上。

4.3 成功率与失败用例统计

合并完成之后,Jenkins 里展示的通过率、失败增量、耗时趋势,全部以合并后的总报告为准。这里有一个统计口径的问题:合并脚本里total_tests是所有套件的用例数之和,而 Jenkins 最终显示的成功率是(total_tests - failures - errors) / total_tests。如果你的测试里有跳过(skipped)用例,JUnit XML 里对应的属性也要正确汇总,否则统计结果会偏。

我在实际项目里遇到过一种情况:某台设备因为长时间待机自动断开,导致该设备上执行的测试全部超时,生成出来的 XML 里有大量failure节点。从总报告看,通过率一下子掉了很多,看起来很吓人,但本质是设备连接问题而非用例质量退化。后来我在汇总逻辑里增加了一个“设备心跳检测”,如果某台设备拉取报告时发现异常超时,直接标记为“设备故障”,对应的报告文件不参与合并,以免污染整体统计。

5. Jenkins/CI 集成实战

5.1 流水线阶段设计

接入 Jenkins 时,我把整个流程分成了四个阶段:构建、测试、报告收集、汇总发布。构建阶段负责编译 HAP 并安装到测试设备;测试阶段执行 flutter test 并让鸿蒙端侧生成 XML;报告收集阶段从设备上拉取报告文件;汇总发布阶段跑合并脚本,并交给 Jenkins 的 JUnit 插件解析展示。

这个分阶段设计的核心考量,是让“测试执行”和“报告处理”完全解耦。这样 CI 机器不需要预装鸿蒙工具链也能处理报告,需要调设备时只需要在测试阶段配置设备农场信息,不会影响其他阶段。

5.2 Jenkinsfile 参考

下面是一份精简但完整的 Jenkinsfile 参考,你可以根据自己集群的标签和设备管理方式调整:

pipeline { agent any environment { REPORT_BASE = 'reports' DEVICE_POOL = 'harmony-device-pool' } stages { stage('Build HAP') { steps { // 在这里执行鸿蒙工程构建 sh 'hvigorw assembleHap' } } stage('Run Tests on Devices') { steps { // 在测试设备池上并行执行测试 // 每台设备执行结束后,把 reports/junit.xml 拉到 // ${REPORT_BASE}/device_${DEVICE_SERIAL}/junit.xml sh ''' ./run_tests_on_devices.sh --pool "${DEVICE_POOL}" --report-base "${REPORT_BASE}" ''' } } stage('Collect Reports') { steps { // 把设备端生成的 XML 从远端设备管理服务拉回本地 sh ''' ./pull_reports.sh --device-pool "${DEVICE_POOL}" --report-base "${REPORT_BASE}" ''' } } stage('Merge Reports') { steps { sh ''' python3 merge_junit.py \ "${REPORT_BASE}/device_*/junit.xml" \ "${REPORT_BASE}/merged/junit-all.xml" ''' } } } post { always { // JUnit 插件解析合并后的报告 junit 'reports/merged/junit-all.xml' } } }

这份流水线文件里的每个脚本都假定已经放在源码仓库里,并且能在 CI 节点上直接执行。post.always里的junit步骤是 Jenkins JUnit 插件提供的,它会解析 XML 并生成趋势图、失败分类列表。

5.3 报告解析与展示

Jenkins 的 JUnit 插件解析完 XML 之后,你会在构建详情页看到几个非常直观的板块:测试结果趋势图、失败用例列表、耗时统计。趋势图尤其有用,它能一眼看出某个测试类是不是从某个构建开始突然频繁失败。失败用例列表里会直接展示 failure 节点的 message 信息,这就是为什么前面强调生成阶段必须完整保留失败信息,否则这里只能看到干巴巴的“failed”字样。

CI 集成过程中有一个容易被忽略的细节:如果合并后的 XML 里某个测试套件没有time属性,Jenkins 会把整个套件的耗时当成 0,趋势图里会平白多出一堆零值,干扰判断。所以合并脚本里我专门做了归一化,即使原 XML 里没有 time 属性,也会填一个默认值。类似的边界情况,最好在合并脚本里统一兜底。

6. 常见问题与排查实录

6.1 高频问题速查表

适配和接入过程中,我把团队和自己遇到的典型问题整理成了一张速查表,希望对你排查时有帮助:

现象常见原因解决方法
报告文件大小为 0写入后没有做 fsync 或回读校验在 ReportSink 中增加写入后校验
XML 解析失败特殊字符未转义用标准 XML 转义函数,补测试用例
Jenkins 上看不到报告合并脚本输出路径和 junit 步骤不一致统一用相对路径,确认阶段间目录约定
多设备报告互相覆盖文件没有按设备分目录强制按 device_serial 分目录存储
时间全部显示为 8 小时前时区未统一生成时统一用 UTC 时间戳
设备上有报告但拉不下来报告生成在沙箱外或权限不足确认写入路径在应用可访问的沙箱范围内
测试事件流不完整命令管道被截断或进程提前退出改用文件作为输入源,测试结束后再生成

这张表里的每一条都是我或者身边同事实际撞到过的,不是拍脑袋总结的。最值得留意的是“报告文件大小为 0”这一条,它特别隐蔽,因为设备端看起来一切都是正常的,但 CI 拉回来的文件是空的,Jenkins 解析时报错或静默跳过,最终表现为“报告莫名消失”。

6.2 两个让我折腾最久的坑

第一个坑是“沙箱路径之外写入静默失败”。我在最初适配版本里直接沿用了原库的默认输出路径,想着鸿蒙再怎么样也不至于连个目录都写不了,结果真机上一跑,报告文件确实生成了,但怎么拉都只有 0 字节。排查了大半天,最后发现是应用在设备上根本不具备目标目录的写权限,写入请求被系统静默拦截了。从那以后,我所有设备端的路径都改成显式传入,并且写入后立刻读一遍文件字节数做校验,才彻底消除这个隐患。

第二个坑是“事件流从管道切到文件时丢尾巴”。端侧执行测试时,如果直接用管道把 flutter test 输出交给报告生成器,遇到设备资源紧张或连接不稳,末尾的 JSON 事件很容易丢失,导致报告里用例数少于预期。我的解决方式是在测试执行时把原始事件流先完整落盘,测试结束之后再由报告生成器读取文件、生成 XML。这样即使事件流在管道阶段丢了一部分,文件里仍然保留着原始日志,可以回放排查。后来验证下来,这个方式让报告用例数几乎和实际执行数完全一致。

结尾:说点实操后的真心话

这次把 junitreport_maintained 鸿蒙化的过程中,我最有价值的一个体会是:跨平台适配最耗时间的往往不是核心逻辑,而是环境差异带来的隐性行为变化。很多问题在 Android 上不存在,到了鸿蒙上就会以各种奇怪的方式冒出来,比如静默失败的写入、时区错位、路径权限陷阱。如果你也在做类似的事情,建议一开始就把“平台差异清单”拉出来,每个点都主动验证一遍,而不是等出了问题再回头猜。

最后再分享一个小技巧:适配库的时候,尽量给整个流程加一个“端到端冒烟测试”,就是用两个必通过的用例和两个必失败的用例跑通全链路,检查生成的 XML 在 Jenkins 上是否解析正确。这个小动作每次集成新设备类型都帮我提前暴露掉至少两三个隐藏问题。工具链这种东西,只有真正在流水线上跑稳了,才算适配完成。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/15 21:49:51

NetWorker 18.2.0.1安装验证指南:Linux与Windows双平台服务启动实战

简介&#xff1a;本资源是Dell EMC NetWorker 18.2.0.1企业级备份软件的完整安装部署指南&#xff0c;专为系统管理员、存储运维工程师及备份方案初学者设计&#xff0c;聚焦Windows与Linux双平台的服务端部署、客户端安装及关键环境配置&#xff0c;切实解决备份系统落地难、依…

作者头像 李华
网站建设 2026/10/15 21:49:08

API交易平台重构实践:Java 17 + Vue3微服务架构与安全设计

API灵钥系统是我这边长期维护的一个开发者服务平台&#xff0c;本质上是把数据服务包装成标准API&#xff0c;以商品形态对外售卖。老版本从最早的内部分发工具一路演进成对接外部开发者的交易平台&#xff0c;跑了四年多&#xff0c;终于在业务规模上来之后攒够了问题。去年年…

作者头像 李华
网站建设 2026/10/15 21:49:01

AI辅助软件工程毕业设计全流程:6款工具从需求分析到答辩

又到了一年一度熬夜赶毕设的季节。软件工程专业的毕业设计和别的专业不一样&#xff0c;它不是交一份报告就完事&#xff0c;而是要交付一个真能跑起来、演示得了、答辩经得起追问的系统。需求分析、数据库设计、后端接口、前端页面、测试用例、项目文档、答辩PPT&#xff0c;整…

作者头像 李华
网站建设 2026/10/15 21:48:52

SpringBoot+Vue3前后端分离物资申报系统:库存防超发与权限设计实战

这套仓库是 SpringBoot Vue3 MyBatis 的前后端分离社区防疫物资申报系统&#xff0c;覆盖了物资申报、审核发放、出入库台账、记录归档一整套链路。单看功能&#xff0c;它并不算复杂&#xff0c;但凡是做过管理系统的都知道&#xff0c;这类业务的关键从来不在增删改查&…

作者头像 李华
网站建设 2026/10/15 21:48:14

STL容器竞赛常用操作全指南:从vector到priority_queue避坑技巧

搞竞赛的都清楚&#xff0c;算法思路想明白只是第一步&#xff0c;真正让代码“跑得起来、交得上去”的&#xff0c;往往是你对STL容器操作的熟练程度。STL容器在竞赛里承担的角色很实在&#xff1a;帮你省掉手写平衡树、手写堆、手写链表的那些体力活&#xff0c;把时间留给核…

作者头像 李华