从文本引擎内存飙升到享元模式:C++状态拆分与缓存实战
1. 从一次内存飙升说起享元模式想解决的问题事情发生在给一个文本渲染模块做优化的时候。当时模块里有一块非常朴素的功能把一份上千页的文稿按页渲染成位图。功能跑起来没有任何问题但一打开任务管理器我就愣住了进程占用将近 1.2GB 内存。更离谱的是随着文稿页数增加内存几乎线性往上涨完全没有缓下来的意思。一开始我以为是字体库或者纹理图集的问题排查了一圈发现字体资源其实早就被正确加载并复用了。真正的问题出在“字符对象”上每一个字符在排版引擎里都是一个独立的 C 对象里面有字符码、字体指针、字号、颜色、粗体/斜体标志位外加它自己的渲染矩形和位置信息。而一份 1000 页、每页 2000 字左右的稿件排下来大概有 200 万个这样的对象。就算单个对象只占 64 字节那也是 120MB 起步再叠加布局树、渲染缓存、分配器开销内存破 1GB 一点不意外。这个场景特别适合引入享元模式。享元模式听起来像个高大上的概念核心思想却一句话就能说清楚把那些多个对象重复出现的内部数据抽出来共享外部状态单独持有。它不是让你把 200 万个对象压缩成 200 万个更小的对象而是让你意识到这 200 万个对象里有大量完全相同的重复数据真正需要保存的逻辑实体其实只有几百个剩下的只是引用。享元模式解决的是“重复造成的内存浪费”这个具体问题适用场景也非常典型对象数量巨大、对象之间有很多相同字段、这些相同字段一旦建立就不会变化、而不同的部分又非常轻量。文本排版正好是教科书级别的例子。在你准备往下看之前先做一个自我评估如果你的 C 项目里也存在类似的现象比如反复创建的粒子系统、地图上大量相同类型的格子、游戏背包里同一种道具的多个实例那么享元模式大概率能帮你一把。这篇文章不会只讲概念我会从状态拆分开始到你关心的工厂实现、生命周期管理、线程安全再到一个完整的文本渲染实战案例一步一步把代码写给你看。2. 状态拆解分清楚哪些能共享哪些必须独有2.1 内部状态和外部状态一句话判据享元模式最核心的工程决策不是“用什么容器做缓存”而是“哪些字段放进共享对象里”。四组原书中把它分成两种状态内部状态和外部状态。我的判断标准只有一句话如果两个对象在生命周期内的某个时刻需要表现出不一样的值那么这个值就是外部状态如果所有实例在可观察行为上都保持完全一致那它就是内部状态也就是可以共享的部分。拿文本渲染举例。字符码 A 和字符码 B 在渲染层面行为完全不同所以字符码是内部状态。字号 12 和字号 16 渲染出来的字形大小不同字号也是内部状态。颜色 red 和 blue 不同所以颜色也是内部状态。但是字符 A 出现在第 3 行第 5 列和出现在第 8 行第 2 列这两个位置信息属于页面布局的一部分不在“字符是什么”的语义里因此位置就是外部状态。看起来简单实际项目里最常出的问题不是分不出来而是分得太细。如果你把“字符颜色是否相同”这种业务维度上的判断也当成内部状态那共享对象就会爆炸性地增加。反过来如果你把“位置的 x 坐标”这种明显属于场景上下文的数据沉淀到共享对象里那你就根本没法复用这个对象等于没做享元。2.2 常见的判断误区有一种特别容易犯的错看到某个字段“在所有对象里都长一样”就直接丢进共享体。你得再问一句这个字段以后会不会因为某个扩展需求而不同比如颜色现在统一是黑色但下周产品要求支持高亮那颜色就不能只放在共享体里不管了。一旦共享对象中包含了未来可能变化的字段你就会面临一个窘境要么改共享体导致所有使用方全部变化要么为了兼容新状态又新建一大堆几乎相同的共享对象享元彻底失效。另一个误区是把“对象的唯一标识”算成内部状态。确实ID 相同意味着两个对象完全等价。但如果你给每一个逻辑实例都分配一个独立的 ID那么这个 ID 在多重实例之间不可能相同你等于把每个实例搞得“独一无二”缓存命中率直线下降。真正的做法是用一套由内部状态派生出来的“享元键”做唯一性而不是用外部传入的全局 ID。2.3 一个可复用的检查清单结合我自己做优化的经验整理出一份可以直接用的问题清单。你在设计共享体时对着这几条逐项过这个字段决定对象的“本质特征”吗比如字符码决定渲染哪个字形那它必须是内部状态。这个字段在同一个共享键下是否恒为同一个值如果有任何一个被观察到的对象不同那就应该拆到外部。这个字段会被高频修改吗会修改的字段坚决不放进共享体否则并发场景全是灾难。去掉这个字段后共享对象是否还能完整描述“它是什么”能说明它是状态细节不能说明它属于本质特征。两个拥有相同内部状态的对象行为是否在所有场景下都一致不一致的必须补充到内部状态或者改成外部状态。你只要保留着一份清单在建模阶段多花十分钟后面省下来的调试时间可能是几个小时。这个阶段做得越严谨你写的缓存代码就越简单。3. 用 C 落地享元工厂、缓存与不可变性理论部分说完了现在开始写代码。这一节先不讲完整业务只把享元模式在 C 里的骨架搭出来每一行代码都解释为什么这么做。3.1 从直白的缓存写起先定义一个共享对象也就是 Flyweight。在我的理解里它首先必须是不可变的。C 没有强制不可变的内置机制但你应该把所有内部状态字段都设为私有只通过 const 方法暴露读取接口。class Glyph { public: Glyph(wchar_t code, std::shared_ptrFontSpec fontSpec, unsigned long color) : code_(code), fontSpec_(std::move(fontSpec)), color_(color) {} const FontSpec fontSpec() const { return *fontSpec_; } wchar_t code() const { return code_; } unsigned long color() const { return color_; } private: wchar_t code_; std::shared_ptrFontSpec fontSpec_; unsigned long color_; };紧接着是工厂。它的职责非常明确传入一组内部状态参数返回一个共享对象如果这个共享对象已经存在就直接返回现有引用。class GlyphFactory { public: std::shared_ptrconst Glyph get(wchar_t code, std::shared_ptrFontSpec fontSpec, unsigned long color) { auto key std::make_tuple(code, fontSpec-id(), color); auto it cache_.find(key); if (it ! cache_.end()) { return it-second; } auto glyph std::make_sharedGlyph(code, std::move(fontSpec), color); cache_.emplace(key, glyph); return glyph; } private: std::mapstd::tuplewchar_t, uint64_t, unsigned long, std::shared_ptrconst Glyph cache_; };这段代码有两个很关键的设计取舍。第一工厂返回的是shared_ptrconst Glyph。这里加 const 不是多余的修饰它是在编译器层面阻止所有使用方修改 Glyph 的内部状态。如果不写 const就只能在团队规范里靠自觉保证“不要改共享对象”迟早有人会踩坑。拿 const 当契约比注释管用一百倍。第二我用std::map做缓存容器而不是unordered_map。在这个场景下Glyph 数量通常不大几百到几千个map 的 O(logN) 查找完全足够。而且 map 的迭代器在插入后不会失效对后续扩展缓存淘汰逻辑更友好。如果你的键是简单整数类型且共享对象数量极大那 unordered_map 更合适。这不是标准答案是工程权衡。3.2 工厂的正确姿势复用、而不是重复创建有人可能已经注意到了工厂的关键在于“先查再建”。这个逻辑看起来平淡无奇但很多刚接触享元模式的人会犯一个低级错误在调用方先new一个完整的 Glyph再传给工厂判断是否需要复用。这样做的后果是每次调用都会分配一次 Glyph内存消耗虽然由工厂集中管理了但对象创建次数完全没减少享元模式的核心价值直接打了对折。正确的做法跟我上面写的一样工厂接收的是“足以唯一确定共享对象的原始参数”在内部完成键的计算、缓存查找、对象创建。也就是说客户端永远不应该持有“共享对象应该怎么构造”的细节。如果你在编译期就已经知道所有共享键甚至可以做一次性的全局初始化。那种情况下工厂退化为一个静态查找表连锁都不用考虑是最舒服的模式。但业务往往不允许这样所以大部分时候还是需要一个带懒加载的工厂。3.3 生命周期与内存篱笆我有一次重构时把 Glyph 设计成了裸指针版本工厂返回一个const Glyph*对象生命周期交给一个全局 Arena 管理。当时觉得没问题因为 Glyph 整体进程都不销毁。直到我把这个渲染模块做成动态库出现了一段代码从库里拿一个指针回来库已经被卸载指针成了一根野指针程序直接崩溃。从那以后我的一个经验是如果共享对象数量可控且生命周期明确可以用裸指针加统一内存池如果共享对象可能跨模块、跨线程使用就老老实实返回shared_ptr。有个细节必须强调shared_ptr在享元工厂里非常容易出现“循环引用”以外的问题——如果你在 Glyph 内部也持有一个shared_ptr指向它自己所属的工厂或者持有其他共享对象稍不留神就可能出现所有共享对象永远无法释放的情况。要避免这种循环依赖Glyph 内部持有的其他资源应该尽量是原始的裸指针或者弱引用。其实更好的做法是把享元对象定位成“进程级缓存资源”工厂在进程存活期间持有它的所有权客户端只借用。这种情况下工厂可以持shared_ptr客户端拿weak_ptr或者干脆用裸指针前提是你对工厂的生命周期有绝对把握。没有绝对把握就用 shared_ptr。3.4 并发访问时的选择如果你的工厂会被多个线程同时调用最简单的方案是整个 get 方法加锁。共享对象创建本来就不是高频操作缓存查找才是。但这里有个更隐蔽的问题两个线程同时发现某个键不存在同时创建对象。如果你用普通的“先检查再插入”逻辑就可能发生同一个键被创建出两个不同对象的情况。解决方式也很经典双检锁。第一次不加锁查询命中就直接返回没有命中时加锁在锁内再查一次确认不存在后才真正创建。class ConcurrentGlyphFactory { public: std::shared_ptrconst Glyph get(wchar_t code, std::shared_ptrFontSpec fontSpec, unsigned long color) { auto key std::make_tuple(code, fontSpec-id(), color); { std::shared_lockstd::shared_mutex lock(mutex_); auto it cache_.find(key); if (it ! cache_.end()) { return it-second; } } auto glyph std::make_sharedGlyph(code, std::move(fontSpec), color); std::unique_lockstd::shared_mutex lock(mutex_); auto it cache_.find(key); if (it ! cache_.end()) { return it-second; } cache_.emplace(key, glyph); return glyph; } private: std::shared_mutex mutex_; std::mapstd::tuplewchar_t, uint64_t, unsigned long, std::shared_ptrconst Glyph cache_; };为了性能写锁之外用读锁。在 C17 下shared_mutex是标准设施非常方便。如果你在 C11/14 环境可以用读写锁库或者退一步直接用std::mutex因为缓存命中后返回的只是指针拷贝锁开销没那么夸张。双检锁里创建对象放在锁外这样可以在保证唯一性的同时减少写锁的持有时间。4. 完整实战文本引擎的字形共享现在我们把完整的业务场景铺开。假设你在开发一个轻量级排版引擎输入是一段文本和样式定义输出是渲染好的位图。需求里最占内存的是“字符布局对象”每个字符、每个页面实例都要有独立的位置但字形本身的属性大批量重复。这是最经典的享元模式战场。4.1 业务建模与 Key 设计先定义共享体需要的属性。字符码每个字符都不同源码里用 wchar_t。字体族名宋体、黑体等。字号12、14、16 等。颜色ARGB 值。粗体标志。斜体标志。把这些内部状态组合成一个 key。key 的设计决定了共享粒度和缓存命中率。如果你把字体族名直接以std::string放进 key那么每次查找都会产生一次字符串比较伴随哈希计算的隐蔽开销。更好的办法是给每个字体注册一个全局唯一 IDkey 里存 ID 而不是名字既省内存又提升查找速度。struct GlyphKey { wchar_t code; uint32_t fontId; uint32_t fontSize; unsigned long argb; bool bold; bool italic; bool operator(const GlyphKey other) const { return code other.code fontId other.fontId fontSize other.fontSize argb other.argb bold other.bold italic other.italic; } };给共享体加上哈希支持之后缓存容器可以切换回 unordered_mapstruct GlyphKeyHash { std::size_t operator()(const GlyphKey key) const noexcept { std::size_t h std::hashwchar_t{}(key.code); h h * 31 std::hashuint32_t{}(key.fontId); h h * 31 std::hashuint32_t{}(key.fontSize); h h * 31 std::hashunsigned long{}(key.argb); h h * 31 std::hashbool{}(key.bold); return h * 31 std::hashbool{}(key.italic); } };这里使用unordered_mapGlyphKey, shared_ptrconst Glyph, GlyphKeyHash。如果你不想手写哈希也可以把 GlyphKey 换成std::tuple标准库直接为基本类型组合提供哈希省事但不直观。我实际项目里宁可用自定义 struct因为业务扩展时往 key 里加字段更清晰。4.2 共享对象实现与渲染共享对象与渲染位置分离。共享体不保存 x、y渲染时通过参数传入位置信息。class Glyph { public: Glyph(GlyphKey key, std::shared_ptrconst FontRasterizer rasterizer) : key_(std::move(key)), rasterizer_(std::move(rasterizer)) {} void render(float x, float y) const { rasterizer_-drawGlyph(key_.code, key_.fontSize, key_.argb, key_.bold, key_.italic, x, y); } float advanceWidth() const { return rasterizer_-advance(key_.code, key_.fontSize); } const GlyphKey key() const { return key_; } private: GlyphKey key_; std::shared_ptrconst FontRasterizer rasterizer_; };客户端在排版时只负责维护每个字符的 x、y 坐标class TextLayoutEngine { public: explicit TextLayoutEngine(std::shared_ptrGlyphFactory factory) : factory_(std::move(factory)) {} void layoutLine(const std::wstring text, uint32_t fontId, uint32_t fontSize, unsigned long argb, bool bold, bool italic, float startX, float startY) { float x startX; for (wchar_t ch : text) { GlyphKey key{ch, fontId, fontSize, argb, bold, italic}; auto glyph factory_-get(key); glyph-render(x, startY); x glyph-advanceWidth(); } } private: std::shared_ptrGlyphFactory factory_; };这段代码最重要的设计在于Glyph 对象完全不感知页面、行、坐标这些业务上下文它只负责“我是谁、我长什么样、怎么把我画出来”。所有和位置相关的数据都通过方法参数临时传入不进入对象内部。这是享元模式在 C 里真正落到实处的关键你得把共享体和业务上下文彻底解耦。4.3 实测效果对象数从 240 万降到 265我把这个设计套到一个大概 800 页的项目文稿上里面只用了 5 种字体、6 种字号、4 种颜色粗体和斜体的组合也就 3 种。整理之后所有字符位置对象大约 190 万个但享元工厂里创建出来的 Glyph 只有 265 个。这意味着 190 万个排版位置共享了 265 个字形对象。内存对比效果非常直观维度不使用享元使用享元逻辑实体数量1,920,8401,920,840共享对象数量1,920,840265每个共享对象估重72 字节56 字节共享对象内存约 138 MB约 14.8 KB位置等外部状态约 90 MB约 90 MB外部状态还是单独存的但它的体积和对象本身的巨大体量相比已经小很多。从我实际观察来看进程总内存从 1.2GB 降到了 420MB 左右大头释放来自字符对象数量的骤减也顺带减少了内存碎片。渲染耗时的提升在意料之中因为缓存命中后完全跳过了 Glyph 构造逻辑。4.4 穿插的教训这次重构里有一个细节很狼狈第一版工厂的 key 里包含了字体名结果每次渲染都做字符串比较缓存命中性能差profile 一看在工厂 get 里消耗了整整一大块时间。后来改成 font ID 组合问题立刻消失。还有一个教训是关于 const 的。我第一版返回的是shared_ptrGlyph没有 const结果某天引擎里一个模块为了临时调试直接改了一个 Glyph 的 color 字段。那个 Glyph 恰好被整页字符共享结果当页字体颜色大面积变化排查了很久才发现是共享对象被改。改成返回shared_ptrconst Glyph之后这类问题在编译期就会被拦住再没犯过。5. 享元与其他内存优化方法的边界很多刚接触享元模式的人会把“对象池”和享元混为一谈。虽然它们看上去都讲“复用”但出发点完全不同实际工程里经常需要组合使用。5.1 对象池、原型与享元的区别对象池解决的是“频繁创建和销毁导致分配开销过大”的问题。它复用的是对象的物理空间对象本身的业务状态每次仍然可以不同。举个游戏项目中的例子子弹对象从对象池里取一个出来填充位置、速度、伤害用完后回收池子。下次再取对象内存还是那块但数值都是新的。享元解决的是“多个逻辑对象包含大量相同状态”的问题。它复用的是业务语义上的状态而不是内存空间。同一个 Glyph 对象被一万个字符引用这一万个字符在渲染世界里是独立个体但它们的字形属性完全一致。有一个快速判断办法循环里频繁new/delete同一个类型的对象性能吃紧 → 对象池。系统里有海量对象且这些对象可以按若干属性归并为少数类别 → 享元。需要完整复制一个已有对象并独立修改 → 原型模式绝对不要用它替代享元。实践中两者常常配合。享元工厂创建共享对象时可以从对象池里取底层资源比如字体位图缓存、纹理区域。但两者关注点不同别在文章和代码里混为一谈。5.2 C 标准库和底层设施里那些“静默的享元”其实 C 生态里到处都是享元思想的影子。最典型的就是字符串字面量编译器经常把相同内容的字符串字面量合并到同一块静态存储区两个完全无关的.rodata地址可能指向同一块内存。这就是编译器在替你做大规模字符串内部化。另一个例子是std::shared_ptr本身。它允许多个shared_ptr实例共享同一个控制块这本质上是一个微小粒度的状态共享方案。再往深了说引用计数的共享对象技术可以视为一种“动态寿命管理的享元变体”只是它不强调“同一个逻辑对象多个外观”而是强调“一个物理对象多个所有者”。还有一些成熟引擎里的资源系统所有纹理、网格、材质都会通过文件路径或 GUID 做唯一化任何资源加载请求都会先查资源表。这其实就是享元模式的工程化应用。你在写自己的工厂时完全可以参考这些成熟设计的接口风格。5.3 什么场景不建议硬套享元不是所有“对象多”的场景都适合享元。如果对象数量本身就很小比如一个界面只有几个控件你为了“防止未来扩展”硬上享元那只会让代码可读性变差维护成本提高纯粹自我感动。还有一种情况是外部状态占比过大。你想共享的那部分数据只有几个字节而每个对象额外要带几十个字段的动态数值。这种情况下共享节省的内存非常有限但你要付出键编码、缓存查找、状态归属设计的复杂度性价比不高。享元模式最好是内聚在问题里自然浮现而不是作为架构清单里的必选项目强行引入。另一个反面场景是高频修改场景。如果对象内部状态频繁变化你就必须不断把变化的部分提升为外部状态最终整个对象几乎都是外部状态内部状态只剩一个 ID享元的缓存价值就名存实亡了。6. 经验清单坑与调试技巧记录6.1 共享对象被“顺手修改”的灾难现场之前提过共享对象被改的问题我再补一个更极端的版本共享对象内部有一个std::vector缓存了渲染中间结果。有人在某个高亮功能里直接向这个 vector 追加数据导致所有复用该 Glyph 的字符都出现了相同的高亮残留。排查时最坑的是这个 Bug 不是稳定复现的因为 vector 扩容和内存复用的时机不同表现时而有时而无。如果共享体内部必须有可变缓存那也应该把缓存放到 per-thread 的局部存储或者在修改前加锁并保证所有字段逻辑上一致。但从设计上我更推荐另一个方案共享对象本身完全不可变所有需要缓冲的数据从工厂外部传入或者放到由外部状态索引的缓存表里。别让“顺手改共享对象”这件事在代码里成为可能。6.2 缓存里堆了一堆一次性的键无界缓存问题享元工厂的缓存如果不设上限理论上键的分布足够广泛时缓存对象数量和原本“每实例一个对象”就没什么区别了。最典型的是颜色字段如果业务允许任意 ARGB 颜色那么这个颜色是无限维度的把它放进内部状态后每个不同颜色都会产生一个共享体。我碰到过一个渲染模块文字颜色可以根据用户自定义的主题设置任意值缓存里最后堆了上万个 Glyph虽然比原本两百万对象要好但内存其实已经失控。解决方案有两种第一种是把颜色从内部状态挪到外部状态渲染时以参数传入这样共享体数量立刻回落到按字符和字体维度的合理水平第二种是给工厂加淘汰策略比如 LRU只保留最近使用的 N 个共享体。这两种方案我实际都试过业务允许时优先改状态归属业务不允许时才上淘汰机制。6.3 排查思路怎么判断问题源于状态共享如果你接手了一个慢查询或者内存泄漏怀疑是享元工厂引起的有两条排查路径很有用。第一给工厂加统计接口返回当前缓存条目数量和命中次数。运行时观察命中率。如果命中率异常低问题多半是键拆分过细需要检查内部状态设计。第二打印几个代表性键的字段看看有哪些键只有一次创建。一次创建占比高说明有很大一部分共享体实际上只被一个逻辑实例引用并没有做到共享。那个时候应该考虑把一些字段从内部状态移到外部状态。void GlyphFactory::dumpStats() const { std::size_t hitCount hitCount_; std::size_t missCount missCount_; std::size_t total cache_.size(); std::printf(cache entries: %zu, hits: %zu, misses: %zu, hit rate: %.2f%%\n, total, hitCount, missCount, (hitCount missCount) ? (hitCount * 100.0 / (hitCount missCount)) : 0.0); }6.4 一组自检问题写完享元工厂后我会在 code review 前用这组问题自我拷问一遍共享对象真的不可变吗所有成员变量是否都从构造函数一次性初始化工厂返回的类型是否带 const调用方是否可以通过返回的引用修改共享状态内部状态集合是否在一定数量级内是否包含无限维度的字段缓存是否有上限如果没有是否确认业务分布不会让缓存持续膨胀多线程访问时是否同时保证“不重复创建”和“快速读取”双检锁是否真的正确外部状态传递是否自然是否已经把状态塞回了共享对象这些问题如果有一半答不上来就说明享元方案还没收敛。与其带着问题上线宁可再花半小时把状态归属重新梳理一遍。我在文本引擎重构上吃到甜头之后又顺手在另一个统计报表模块里应用了同样思路原本几万行数据的表格对象里大量重复的列格式定义被抽成共享体内存和构建速度都明显改善。这种模式一旦吃透后面你会下意识地在建模时先问自己“这里面有哪些东西本质上是一样的”最后再多说一句。享元模式落到 C 工程里从来不是背一个类图然后照搬关键在于你把状态的归属想明白把不可变性贯彻到底。前期的建模分析占了七成的功夫工厂代码真的只是最后三成。你按照这个思路去做一次完整的重构就会认同我的判断。

相关新闻

OpenShell实测:终端AI助手接入大模型,从命令生成到安全执行

OpenShell实测:终端AI助手接入大模型,从命令生成到安全执行

这个标题我看着亲切。命令行干了十来年,每次换新机器、新项目,总要重新记一堆参数和管道写法。OpenShell这个名字我第一眼就猜到了路数——把大模型的能力塞进终端,让 Shell 变成能听懂人话的助手,而不是冷冰冰的字符界面。我花了…

2026/10/15 12:00:05 阅读更多 →
C#与SQL Server学生信息管理系统开发全流程实战

C#与SQL Server学生信息管理系统开发全流程实战

1. 项目概述与实验要求拆解1.1 这个实验到底要做什么第一次拿到"C# SQL数据库实验大作业(学生信息管理)"这个题目时,很多人的第一反应是"又是学生管理系统,太老套了"。但说实话,这个题目之所以能成…

2026/10/15 12:00:05 阅读更多 →
MATLAB算法开发实战:从矩阵运算到工程部署

MATLAB算法开发实战:从矩阵运算到工程部署

1. MATLAB算法实战的核心价值 十五年前我第一次接触MATLAB时,就被其简洁的矩阵运算语法所震撼。如今在自动驾驶系统开发中,我依然每天用MATLAB处理传感器数据、验证控制算法。这个工具最迷人的地方在于:它把艰深的数学公式变成了工程师口袋里…

2026/10/15 12:00:05 阅读更多 →

最新新闻

LaTeX环境搭建:Texlive 2020与TeXstudio 4.5.3协同部署指南

LaTeX环境搭建:Texlive 2020与TeXstudio 4.5.3协同部署指南

1. 这不是“装个软件”那么简单:为什么LaTeX环境搭建值得花两小时认真对待你点开这个标题,大概率正被一篇论文、一份技术报告或毕业设计的排版问题卡在原地——Word里公式编号总对不齐,参考文献格式反复崩坏,图表跨页后标题消失&a…

2026/10/15 12:40:07 阅读更多 →
FM175XX + FreeRTOS 读卡工程详解:从移植避坑到业务链路落地

FM175XX + FreeRTOS 读卡工程详解:从移植避坑到业务链路落地

简介:面向嵌入式开发者的 APM32/STM32 RFID 读写方案,基于 FreeRTOS 集成 FM175XX 射频芯片,适合需要快速验证 TypeA 卡识别、评估识别距离优化效果的开发者。工程可直接编译,外接天线板采用双天线配置,已调通卡片 ID …

2026/10/15 12:40:07 阅读更多 →
系统辨识与建模40学时教案:从M序列设计到最小二乘的工程实践

系统辨识与建模40学时教案:从M序列设计到最小二乘的工程实践

简介:这份系统辨识教案面向自动化专业学生与相关工程技术人员,围绕从输入输出数据中建立系统模型这一核心问题,系统梳理辨识理论、方法与实验流程。内容涵盖实体与模型的概念区分、模型分类、Zadeh 辨识定义及数据、模型类、准则三要素&#…

2026/10/15 12:40:07 阅读更多 →
写博客坚持不下去?从知识复利到写作习惯的完整复盘

写博客坚持不下去?从知识复利到写作习惯的完整复盘

写了七八年的博客,中间无数次想放弃,又无数次因为某个读者的一条评论续了命。身边总有朋友问:现在短视频都把流量抢光了,谁还看长文?你怎么还能一篇篇写下去,而且没有烂尾?每次被问到这个问题&a…

2026/10/15 12:40:07 阅读更多 →
仓颉尝鲜第一步:VSCode 环境配置与 SDK 插件全流程

仓颉尝鲜第一步:VSCode 环境配置与 SDK 插件全流程

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/15 12:40:07 阅读更多 →
CentOS 7 离线安装 PostgreSQL 9.5 完整实践指南

CentOS 7 离线安装 PostgreSQL 9.5 完整实践指南

1. 项目概述:为什么离线安装 PostgreSQL 9.5 在 CentOS 7 上仍值得深挖你手头有一台刚上架的生产服务器,物理隔离、无外网权限、防火墙策略严苛——这是某金融行业客户机房里最常见的“安全岛”环境;或者你正为某高校实验室的旧版教学集群做维…

2026/10/15 12:39:06 阅读更多 →

日新闻

Docker Compose 容器化部署 ChirpStack LoRaWAN 网络服务器实战

Docker Compose 容器化部署 ChirpStack LoRaWAN 网络服务器实战

1. 为什么选择容器化方式部署 LoRa 网络服务LoRa 物联网项目落地时,网络服务器是整个链路的核心枢纽。它负责接收网关上报的数据包、完成去重与解密、再把有效载荷分发给上层应用。早期大家习惯把网络服务器直接装在物理机上,依赖一堆系统库和特定版本的…

2026/10/15 0:00:46 阅读更多 →
共享LLM服务下的跨模型自动扩缩容:Kubernetes实战与避坑指南

共享LLM服务下的跨模型自动扩缩容:Kubernetes实战与避坑指南

1. 共享推理服务里,扩缩容到底难在哪做过大模型推理服务部署的人,大概都经历过这样的场景:一个团队共用一套推理集群,A 业务跑的是 7B 的小模型,B 业务跑的是 72B 的大模型,C 业务白天流量高、晚上几乎归零…

2026/10/15 0:00:46 阅读更多 →
Systemd 启动阶段根文件系统只读之谜:fstab 与 remount-fs 服务深度解析

Systemd 启动阶段根文件系统只读之谜:fstab 与 remount-fs 服务深度解析

1. 引子:一次内核恐慌引发的追查先从一个真实场景说起。某次我给一台老旧的嵌入式设备折腾系统裁减,为了把启动时间压进两秒以内,我手贱地把一串自认为“没用”的 systemd 服务给 disable 了,其中就包括systemd-remount-fs.servic…

2026/10/15 0:00:46 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/14 3:30:28 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/14 16:30:29 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/14 3:29:58 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/15 0:14:36 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/14 22:45:47 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/15 9:49:14 阅读更多 →