资讯动态

miniblink49 仓库内置 Google Test(gtest)实战 FAQ 全解析:断言机制、death test 与跨平台构建疑难

发布时间:2026/9/18 17:24:21 来源:尧图企业网站定制
miniblink49 仓库内置 Google Testgtest实战 FAQ 全解析断言机制、death test 与跨平台构建疑难【免费下载链接】miniblink49a lighter, faster browser kernel of blink to integrate HTML UI in your app. 一个小巧、轻量的浏览器内核用来取代wke和libcef项目地址: https://gitcode.com/GitHub_Trending/mi/miniblink49Google Test下称 gtest是 C 单元测试的事实标准之一本仓库在 v8_7_5/testing/gtest 下完整内置了 gtest 的源码、构建脚本与全套自测用例服务于 V8 引擎的测试体系。本文以 V1_5_FAQ.md 为骨架逐条解析 gtest 设计决策背后的原理并结合本仓库源码如 gtest.cc、gtest-death-test.cc、gtest-death-test.h深入说明 death test 的 fork 实现、断言宏的模板元编程技巧、MSVC 构建陷阱等实战要点帮助你写出正确、健壮、可移植的 C 单元测试。为什么选择 Google Test设计哲学与核心优势FAQ 开篇明确表态作者无意争论哪个 C 测试框架最好而是罗列了 gtest 打动他们的特性组合。这些特性同样是你在项目选型时的决策依据可移植性gtest 在std::string、std::vector都无法编译的嵌入式环境中也能工作不依赖异常exceptions和 RTTI可在 Linux、macOS、Windows 及多种嵌入式系统上运行。非致命断言EXPECT_*允许一次编辑-编译-测试循环中报告多个失败极大节省调试时间。信息丰富的断言消息直接使用流式语法追加上下文如ASSERT_EQ(5, Foo(i)) where i i;无需引入新宏。自动发现测试TEST()/TEST_F()定义后即自动注册无需手工枚举。可扩展断言词汇EXPECT_PRED*族宏让你低成本扩展自定义断言更优雅的写法是自定义断言宏。Death tests用于验证生产代码中的assert/abort/崩溃是否在正确条件下触发。SCOPED_TRACE帮助理解子函数或循环内部断言失败的上下文。名称模式过滤可以用名称模式决定运行哪些测试快速复现失败。值得注意的是本仓库 gtest 的测试目录 本身就用这些机制对 gtest 自身进行了大量自测例如 gtest_unittest.cc 中大量使用TEST、TEST_F、EXPECT_*、ASSERT_*与 death test是学习这些特性的第一手范本。跨平台与构建问题用 Visual Studio 2008 生成 64 位二进制答Trevor Robinson加载随附的解决方案文件msvc\gtest-md.sln或msvc\gtest.sln本仓库对应路径为 msvc/gtest-md.sln 与 msvc/gtest.sln按迁移向导升级到 VS2008。在Build菜单打开Configuration Manager...在Active solution platform下拉框选择New...新增x64平台Copy settings from保持Win32并勾选Create new project platforms点击OK。现在可从Standard工具栏在Win32/x64之间切换也可用 Batch Build 同时构建两种位宽。为避免 32/64 位构建产物互相覆盖需要修改所有项目的Intermediate Directory在Solution Explorer中多选如 shift-click除解决方案外的所有项目右键选择Properties左侧选Configuration PropertiesConfiguration下拉框选All Configurations确认平台是x64把Intermediate Directory从$(PlatformName)\$(ConfigurationName)改为$(OutDir)\$(ProjectName)。构建完成后64 位二进制位于msvc\x64\Debug目录。MinGW 下能否使用 gtest官方未自行测试但社区报告可在 Cygwin 的 MinGW 下成功编译安装配置命令为PATH/TO/configure CCgcc -mno-cygwin CXXg -mno-cygwin注意事项编译时会产生大量警告make check会有部分错误因为 gtest 自身的某些测试与 MinGW 不兼容。MSVC 链接器错误Runtime Library 设置不一致如果测试工程与 gtest 库不是用相同的编译器设置构建会出现以下链接错误/警告LNK2005: symbol already defined in objectLNK4217: locally defined symbol symbol imported in function functionLNK4049: locally defined symbol symbol imported原因在于 gtest 工程gtest.vcproj对应本仓库 msvc/gtest.vcproj的 Runtime Library 默认设为/MTDebug 为/MTd。如果你的工程用的是/MDDebug 为/MDd需要把 gtest 工程的该设置改成与你的工程一致。修改路径项目属性 →Configuration Properties | C/C | Code Generation→Runtime Library。也可以直接改用gtest-md.vcproj即 msvc/gtest-md.vcproj代替gtest.vcproj。Visual C 用户的重要警告测试放进库中却不执行把测试放进静态库、而main()放在另一个库或 .exe 中时测试不会运行。原因是 VC 链接器会丢弃未被引用的库测试注册用的静态对象构造函数不会执行。解决办法在测试库中声明一个导出函数例如__declspec(dllexport) int PullInMyLibrary() { return 0; }静态库中可省去__declspec(dllexport)在主程序中引用它int PullInMyLibrary(); static int dummy PullInMyLibrary();迫使链接器保留测试库为 .exe 工程添加/OPT:NOREF链接选项项目属性 → Linker → Optimization → References 设为Keep Unreferenced Data。另外若 gtest 以静态库形式使用gtest.vcproj的默认形态测试也必须放在静态库中如果测试必须放 DLL则必须将 gtest 也改为 DLL 构建。FAQ 给出的最终建议是不要把测试写在库里。断言机制深度剖析为什么断言不用异常实现gtest 最初的动机是让不支持异常的项目也能使用它后来发现还有额外好处析构函数中抛异常是 C 未定义行为。不用异常意味着断言可安全用于析构函数。EXPECT_*失败后继续执行让一次运行报告多个失败——因为 C 的编辑-编译-测试周期很长能一次修多个问题很有价值。若断言用异常实现用户代码可能误吞失败try { ... ASSERT_TRUE(...) ... } catch (...) { ... }上述代码即使ASSERT_TRUE抛出也会通过。虽然测试中很少这么写但在被测代码回调里写断言时可能踩中。代价是ASSERT_*用return实现只能中止当前函数不能中止整个TEST。源码佐证本仓库 gtest.cc 的UnitTest::Run()在 Windows 上通过SetErrorMode抑制崩溃对话框、_set_abort_behavior关闭调试弹窗正是为断言失败即预期行为、不弹出 UI这一设计服务的。为什么支持EXPECT_EQ(NULL, ptr)却不支持EXPECT_NE(NULL, ptr)由于 C 的语言特性让NULL作为EXPECT_XX()/ASSERT_XX()参数需要非平凡的模板元编程技巧因此 gtest 只在最需要的地方实现。EXPECT_EQ(expected, actual)参数有约定顺序把NULL放第一个期望值很常见故已实现EXPECT_NE(NULL, ptr)需求不强——失败时你已知道ptr必为NULL打印它不增加信息EXPECT_TRUE(ptr ! NULL)效果相同若要支持EXPECT_NE(NULL, ptr)为一致性必须同时支持EXPECT_NE(ptr, NULL)需要把元编程技巧用两次得不偿失更重要的是随着 Google Mock matcher 库的壮大官方鼓励用统一的EXPECT_THAT(value, matcher)语法——matcher 可以自由组合而EXPECT_NE这类宏无法组合。ASSERT_PREDn报 no matching function to call 怎么办如果传给ASSERT_PRED*/EXPECT_PRED*的谓词函数是重载函数或模板函数编译器无法确定该选哪个版本。ASSERT_PRED_FORMAT*/EXPECT_PRED_FORMAT*没有这个问题。解决办法首选改用(ASSERT|EXPECT)_PRED_FORMAT*还能获得更好的失败消息或显式告知编译器选哪个版本bool IsPositive(int n) { return n 0; } bool IsPositive(double x) { return x 0; } // 会编译错误 // EXPECT_PRED1(IsPositive, 5); // 正确写法——static_cast 到 int 版本函数指针 EXPECT_PRED1(static_castbool (*)(int)(IsPositive), 5);模板函数需要显式实例化template typename T bool IsNegative(T x) { return x 0; } ASSERT_PRED1(IsNegativeint, -5);多参数模板更微妙ASSERT_PRED2(GreaterThanint, int, 5, 0)不通过因为预处理器认为传了 4 个参数。解决方法是把谓词函数用括号包起来ASSERT_PRED2((GreaterThanint, int), 5, 0);no match for operator 是什么问题在断言中使用自定义类型FooType时必须为其定义std::ostream operator(std::ostream, const FooType)gtest 才能打印该类型的值。如果FooType声明在某个命名空间中运算符也必须定义在同一个命名空间中ADL 要求。构造函数/析构函数中不能使用ASSERT_*和FAIL*为了支持ASSERT_EQ(1, Foo()) blah blah foo;这种流式消息语法实现上放弃了在构造函数/析构函数中使用ASSERT*和FAIL*EXPECT*和ADD_FAILURE*不受影响。报错信息是constructor (or destructor) cannot return a value。解决办法把构造/析构函数的内容移到私有的void成员函数中或改用EXPECT_*()。必须使用RUN_ALL_TESTS()的返回值忽略RUN_ALL_TESTS()返回值只写RUN_ALL_TESTS();而非return RUN_ALL_TESTS();是错误且危险的测试运行器依据进程退出码判断成败而不是 stdout/stderr 输出。若main()忽略返回值即使断言失败测试也会被认为通过。gtest 在实现上让 gcc 对忽略返回值的行为发出警告修复方法就是把返回值作为main()的返回值。本仓库的标准入口 gtest_main.cc 正是标准示范GTEST_API_ int main(int argc, char **argv) { printf(Running main() from gtest_main.cc\n); testing::InitGoogleTest(argc, argv); return RUN_ALL_TESTS(); }InitGoogleTest()解析命令行中的 gtest 专属 flag 并将其从参数中移除必须在RUN_ALL_TESTS()之前调用且RUN_ALL_TESTS()只能调用一次与线程安全的 death test 等高级特性冲突。测试组织与 test fixture为什么TEST和TEST_F是两个不同宏C 宏系统无法用一个宏同时处理带/不带 fixture 的两种情况。虽然可以只提供一个宏要求用户偶尔定义空 fixtureclass FooTest : public ::testing::Test {}; TEST_F(FooTest, DoesThis) { ... }或typedef ::testing::Test FooTest; TEST_F(FooTest, DoesThat) { ... }但 gtest 的目标是让简单测试写起来毫不费力所以为简单测试单独提供TEST()宏。FAQ 认为两种方案都不完美但也都能接受。为什么 fixture 用 class 而不用 structgtest 只在表示纯数据时使用 struct。struct/class 的区分有助于表达代码意图——test fixture 包含SetUp()/TearDown()等逻辑定义为 class 更合适。能从已有 fixture 派生新 fixture 吗可以且没有深度限制。每个 fixture 都有同名的 test case意味着一个 test case 只能使用一个特定 fixture但多个 test case 可能想复用相同或相近的 fixture 逻辑比如确保 GUI 库的所有 test case 都不泄漏字体、画刷等系统资源。做法把共享逻辑放在基类 fixture再为每个 test case 派生独立 fixture// 定义基类 fixture class BaseTest : public ::testing::Test { protected: ... }; // 从 BaseTest 派生 FooTest class FooTest : public BaseTest { protected: virtual void SetUp() { BaseTest::SetUp(); // 先初始化基类 ... additional set-up work ... } virtual void TearDown() { ... clean-up work for FooTest ... BaseTest::TearDown(); // 注意清理完 FooTest 后记得拆基类 } }; TEST_F(FooTest, Bar) { ... } TEST_F(FooTest, Baz) { ... }完整的派生 fixture 示例见 samples/sample5_unittest.cc。不想为每个 test case 定义新 fixture 类怎么办可以直接typedeftypedef BaseTest FooTest; TEST_F(FooTest, Abc) { ... } TEST_F(FooTest, Def) { ... } typedef BaseTest BarTest; TEST_F(BarTest, Abc) { ... } TEST_F(BarTest, Def) { ... }为什么优先用 fixture 而不是全局变量测试常需要修改状态全局变量难以阻止副作用从一个测试泄漏污染另一个测试fixture 为每个测试提供一套同名但全新的变量测试相互独立。全局变量污染全局命名空间。fixture 可通过子类化复用全局变量做不到。用构造函数/析构函数还是SetUp()/TearDown()关键前提gtest不会跨测试复用同一个 fixture 对象。每个TEST_F都会新建一个 fixture 对象立刻调用SetUp()运行测试调用TearDown()然后立刻销毁。因此若构造函数/析构函数已能完成任务就无需再写SetUp()/TearDown()。以下情况仍建议用SetUp()/TearDown()清理操作可能抛异常析构函数中抛异常是未定义行为通常直接杀掉程序。注意许多标准库如 STL在启用异常时可能抛出因此要写可移植测试无论是否启用异常优先用TearDown()。gtest 团队考虑在启用异常的平台Windows、macOS、Linux 客户端上让断言宏抛出以省去用户手动传播失败的需要——因此不要在析构函数中使用 gtest 断言。构造函数/析构函数中无法对this做虚函数调用调用虚方法也是静态绑定。如果需要调用派生类覆盖的方法必须用SetUp()/TearDown()。SetUp没被调用注意拼写C 区分大小写。必须拼成SetUp()不是Setup()。同理SetUpTestCase()不能拼成SetupTestCase()。为什么TEST_F(Foo, Bar)报 no matching function for call to Foo::Foo()gtest 需要能创建 fixture 类的对象因此必须有默认构造函数。编译器通常会自动生成但以下情况需自行定义显式声明了非默认构造函数时必须再定义默认构造函数哪怕是空的Foo有const非静态数据成员时必须定义默认构造函数并在初始化列表中初始化该 const 成员早期 gcc 不强制该 bug 在 gcc 4 修复。私有成员测试不写FRIEND_TEST的替代方案首先建议尽量写出可测试的代码即类可以从公共接口轻松测试例如 Pimpl 惯用法把所有私有成员移入辅助类辅助类所有成员公开。若必须访问私有成员有以下不依赖FRIEND_TEST的选项把测试写成 fixture 类的成员fixture 类声明为被测类的 friend测试方法作为 fixture 成员访问私有成员class Foo { friend class FooTest; ... }; class FooTest : public ::testing::Test { protected: void Test1() {...} // 访问 Foo 的私有成员 void Test2() {...} }; TEST_F(FooTest, Test1) { Test1(); } TEST_F(FooTest, Test2) { Test2(); }在 fixture 中写访问器class FooTest : public ::testing::Test { protected: T1 get_private_member1(Foo* obj) { return obj-private_member1_; } };针对 protected 成员在测试文件中写一个测试专用子类改变访问级别class TestableYourClass : public YourClass { public: using YourClass::DoSomethingReturningInt; // 改变访问权限 }; TEST_F(YourClassTest, DoSomethingTest) { TestableYourClass obj; assertEquals(expected_value, obj.DoSomethingReturningInt()); }对于私有静态成员更好的做法是干脆不写成类成员把函数声明为头文件内internal命名空间中的自由函数测试里同样以internal::前缀调用从而把实现细节挡在 .h 之外。Death Test 专题原理、陷阱与最佳实践为什么 death test 用断言实现而不是 test runnerASSERT_DEATH(statement, expected_message)把所有信息集中在一处、用一种语言声明无样板代码它与其他断言语法和错误报告语义一致易于学习可以与任意断言/逻辑混合使用例如if (FooCondition()) { ASSERT_DEATH(Bar(), blah); } else { ASSERT_EQ(5, Bar()); }它还能引用局部变量、根据运行时信息决定做多少个 death testconst int count GetCount(); // 运行时才知道 for (int i 1; i count; i) { ASSERT_DEATH({ double* buffer new double[i]; ... initializes buffer ... Foo(buffer, i) }, blah blah); }runner 方案则更静态、更不灵活。ASSERT_DEATH的底层机制fork 子进程ASSERT_DEATH通过fork()创建子进程运行 death test。本仓库源码清晰印证了这一点gtest-death-test.cc 中有const pid_t child_pid fork();在 L1100-L1103 处if (use_fork (child_pid fork()) 0) { ExecDeathTestChildMain(args); _exit(0); }在支持clone()的 Linux 系统上会优先用clone(ExecDeathTestChildMain, ...)见 L1092。fork()使用写时复制copy-on-write页开销几乎为零子进程直接从用户提供的语句开始执行跳过全局/局部初始化。若从头启动子进程动态链接大量库的测试程序可能需要数秒加载时间——这是断言式实现的性能优势。gtest-death-test.h 注释明确了执行流程① 检测到多个活动线程时发出警告fork/clone 只在单线程时安全② 父进程 clone 出子进程并在其中运行 death test③ 父进程等待子进程终止④ 父进程检查子进程退出码与错误消息。death test 修改的状态为何丢失EXPECT_DEATH等在子进程中执行预期崩溃不会杀死测试程序父进程。因此它们产生的内存副作用只存在于各自子进程中父进程观察不到——可以理解为运行在平行宇宙里。death test 挂起或段错误怎么办death test 机制很微妙先理解原理再写。最常见的诱因是父进程存在多个线程第一步是消除EXPECT_DEATH()之外创建的线程。如果某些库在到达main()之前就创建了线程可以尝试尽量把更多活动移入EXPECT_DEATH()极端情况全部移入或让它里面尽量少留东西把 death test 风格设为threadsafe——更安全但更慢。注意线程安全 death test 会在子进程中从头重跑整个测试程序因此程序必须能与自己并行运行且是确定性的。最终这归结为良好的并发编程——确保没有竞态条件和死锁没有银弹。为什么ASSERT_DEATH抱怨已 join 的线程在 Linux pthread 库下一旦从单线程跨入多线程就无法回头。首次创建线程时会额外产生一个 manager 线程所以是 3 个而非 2 个线程之后创建的线程 join 回主线程时线程数减 1但 manager 线程永不消亡仍剩 2 个线程导致无法安全运行 death test。新的 NPTL 线程库没有 manager 线程、无此问题但若无法控制测试运行机器不应依赖这一点。为什么整个 test case 必须命名为FOODeathTestgtest 不会交错运行不同 test case 的测试而是跑完一个 test case 再跑下一个因为需要在首个测试前 setup、之后 teardown拆分会带来多次 setup/teardown 且语义不干净。若按测试名而非 test case 名决定顺序会出现矛盾TEST_F(FooTest, AbcDeathTest) { ... } TEST_F(FooTest, Uvw) { ... } TEST_F(BarTest, DefDeathTest) { ... } TEST_F(BarTest, Xyz) { ... }FooTest.AbcDeathTest需先于BarTest.Xyz但 gtest 不交错 test case意味着要先跑完整个FooTest再跑BarTest这与BarTest.DefDeathTest先于FooTest.Uvw的需求冲突。因此 death test 所在 test case 整体必须以DeathTest结尾命名便于框架调度。一个 test case 里既有 death test 又有普通测试怎么办不必把整个 test case 命名为FOODeathTest可以拆分为相关命名的FooTest与FooDeathTestclass FooTest : public ::testing::Test { ... }; TEST_F(FooTest, Abc) { ... } TEST_F(FooTest, Def) { ... } typedef FooTest FooDeathTest; TEST_F(FooDeathTest, Uvw) { ... EXPECT_DEATH(...) ... } TEST_F(FooDeathTest, Xyz) { ... ASSERT_DEATH(...) ... }ASSERT_DEATH的 statement 参数可以是什么ASSERT_DEATH(statement, regex)的statement只要是当前上下文中合法的 C 语句即可可以引用全局/局部变量可以是简单函数调用最常见复杂表达式复合语句。示例部分来自 FAQTEST(MyDeathTest, FunctionCall) { ASSERT_DEATH(Xyz(5), Xyz failed); } TEST(MyDeathTest, ComplexExpression) { const bool c Condition(); ASSERT_DEATH((c ? Func1(0) : object2.Method(test)), (Func1|Method) failed); } TEST(MyDeathTest, InsideLoop) { for (int i 0; i 5; i) { EXPECT_DEATH_M(Foo(i), Foo has \\d errors, ::testing::Message() where i is i); } } TEST(MyDeathTest, CompoundStatement) { ASSERT_DEATH({ for (int i 0; i 5; i) { Bar(i); } }, Bar has \\d errors); }更多示例见 test/gtest_unittest.cc。death test 的正则表达式语法POSIX 系统上使用 POSIX Extended 正则语法regex.hWindows 上使用 gtest 自带的一种受限正则变体。gtest-death-test.h 详细列出支持字面字符、.、\\d、\\D、\\f、\\n、\\r、\\s、\\S、\\t、\\v等但不支持并集x|y、分组(xy)、方括号[xy]、重复计数x{5,7}等 PCRE/POSIX 高级特性。完整的跨平台语法说明见 V1_5_AdvancedGuide.md 的正则表达式语法小节。并行执行与性能gtest 支持并行跑测试吗gtest 不直接解决并行问题——test runner 通常与构建/测试环境强耦合。gtest 的做法是与 test runner 良好协作XML 报告包含每个测试的耗时gtest_list_tests和gtest_filterflag 可用于把测试方法拆分到多个进程执行从而让 runner 并行运行。为什么不用多线程加速测试写线程安全的代码很难大多数测试并没有考虑线程安全多线程下可能工作不正常。当你已知其他线程在做什么时让代码正确工作已经很难当不知道时测试方法可能在你写完后被添加、删除或修改难上加难甚至不可能。想并行就跑在不同进程中。其他高频疑难静态 const 成员报 undefined references在类体内static const int kBar 100;只是声明还需要在类外定义一次// foo.cc const int Foo::kBar; // 无初始化器否则代码是非法 C可能在意外的地方出问题尤其在 gtest 比较断言EXPECT_EQ等中使用它时会产生 undefined reference 链接错误。接口有多个实现能否写一套测试跑多遍FAQ 承认 gtest 当时对这类测试、以及通用的数据驱动测试支持不佳期待后续改进。void value not ignored as it ought to be说明你在非void返回的函数里用了ASSERT_*()。ASSERT_*()只能用在void函数中因为它靠return实现中止。如何测试定义了main()的文件测试foo.cc需要把它编译链接进测试程序但若其中含main()会与测试程序的main()冲突。正确做法是拆成三个文件foo.h声明、foo.cc除main()外的定义、foo_main.cc只有main()定义。若不想做这种侵入式修改可以用 hack——把整个foo.ccinclude 进测试文件并先重命名main// File foo_unittest.cc #define main FooMain #include a/b/foo.cc // 测试从这里开始FAQ 强调这只是最后手段的 hack。想用不同参数重复跑同一测试不需要写多份拷贝使用值参数化测试value-parameterized tests一次定义、多参数重复见 V1_5_AdvancedGuide.md 的值参数化测试小节。测试输出被一堆日志淹没gtest 输出设计为简洁、人类友好。由于大多数日志走 stderrgtest 选择把测试输出写到stdout从而可以用重定向分离./my_test googletest_output.txt如何在 Emacs 中直接跳到失败行gtest 的失败消息格式可被 Emacs 及 acme、Xcode 等 IDE 识别。若消息位于 Emacs 编译缓冲区中即可点击按enter跳到对应源码或用 C-x 跳到下一个失败。如何抑制 Windows 上的内存泄漏报告gtest 的静态初始化单例需要在堆上分配导致 Visual C 内存泄漏检测器在程序结束时报告泄漏。最简单的方法是使用_CrtMemCheckpoint和_CrtMemDumpAllObjectsSince不报告静态初始化的堆对象详见 MSDN 的堆检查/调试例程。测试过早退出如何被识别源码层面gtest.cc 的UnitTest::Run()实现了一个提前退出协议启动时创建由环境变量TEST_PREMATURE_EXIT_FILE指定的文件gtest 工作完成后删除它。test runner 可据此判断测试程序是否提前退出death test 子进程不参与该协议以免干扰父进程。问题得不到解答时提问的正确姿势FAQ 建议按顺序尝试阅读 V1_5_Primer.md 与 V1_5_AdvancedGuide.md → 查阅 wiki 与邮件列表归档 → 在googletestframeworkgooglegroups.com提问需先加入讨论组。提问时尽量提供gtest 版本或 SVN 修订号、操作系统、编译器名称与版本、完整命令行 flag、完整编译错误信息、以及能复现问题的最小完整程序。小结gtest 之所以成为 C 测试的常用选择在于它对可移植性、非致命断言、自动注册、可扩展谓词断言与 death test 的组合设计。通过本仓库 v8_7_5/testing/gtest 的源码可以确认death test 依赖fork()/clone()子进程机制gtest-death-test.cc、断言宏基于模板元编程与流式消息技巧gtest.h、标准入口main()必须返回RUN_ALL_TESTS()的值gtest_main.cc。掌握这些设计原理与 FAQ 中的平台陷阱能让你在 miniblink49 这类大型 C 项目中写出更健壮、更易维护、跨平台可移植的单元测试。【免费下载链接】miniblink49a lighter, faster browser kernel of blink to integrate HTML UI in your app. 一个小巧、轻量的浏览器内核用来取代wke和libcef项目地址: https://gitcode.com/GitHub_Trending/mi/miniblink49创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价