43 KiB
AecDbEwEquipment 分析:内存、缺陷与性能
目标:在动手改之前,把
AecDbEwEquipment(阀门/设备通用实体)的内存问题、潜在缺陷和性能热点列清楚,给出可分批落地的方案。范围:
SourceCode/Code2026/inc/AecDbEwEquipment.h(578 行)、SourceCode/Code2026/CoreLibrary/AecDbEwEquipment.cpp(14290 行)。关联的AecEqui::XPrimData(inc/EarThwormDef.h:2526)、AecDbEntityCommon的 2D/3D 缓存(AecRxBase/AecDbEntityCommon.cpp)只在必要时提及。状态(2026-10-22):仅分析,未改代码。行号以当前
main为准。
1. 类职责与数据模型
1.1 职责
AecDbEwEquipment 继承 AecDbEntityBase + AecMInterfaces,是"阀门、设备"这类参数化拼装体的宿主:
- 自身只存插入点
m_ptInsert、方向m_vtDir、专业数据m_mapSpecXData、两个图层软指针m_idDash/m_idCenter、竖放标志m_bVert - 几何是
m_arSubPrimPtrs(CPtrArray,元素是堆上的AecEqui::XPrimData*),每个单元描述一个基本体(box / cylinder / cone / revolve / nozzle / extrude / Revit 组合 …) - 3D 显示时逐个
Draw3dSolid()造AcDb3dSolid,按m_bBoolSubtract分成并集/差集两组,做布尔后放进 3D 缓存 - 2D 显示时按阀门类型画符号(25 个
Draw*2d),或把 3D 单元投影到平面(PrimPorjectToPlan)
1.2 几何单元 XPrimData
struct XPrimData {
short m_eType; // 基本体类型
PVOID m_pData; // 参数块(new byte[n] 或 new 具体结构体)
int m_nSize; // 参数块字节数
...
std::vector<XPrimData*> m_Combins; // 组合设备的子单元
};
两个关键点,后文大量引用:
| 事实 | 位置 | 影响 |
|---|---|---|
~XPrimData() 只删 m_Combins,不删 m_pData |
EarThwormDef.h:2539 |
删除 XPrimData 前必须手动 releaseData(),否则参数块全泄漏 |
releaseData() 按类型区分 delete[] / delete |
EarThwormDef.h:2544 |
释放方式与类型绑定,不能统一 delete[] |
1.3 数据流
开图 dwgInFields:1693 ──► dwgInFieldsForXPrim:172(递归读 m_Combins)
└─► subDMesh2NewSolid:3145(Revit 子网格 → 参数化单元)
存盘 dwgOutFields:1807 ──► dwgOutFieldsForXPrim:239(递归写 m_Combins)
绘制 Draw2d:2491 ──► MakeCurves:11063 ──► PrimPorjectToPlan:8314(逐单元投影)
Draw3d:3002 ──► Draw3dSolid:3905 ──► Draw3dSolid(XPrimData):3526(逐单元造体)
└─► 布尔并/减 ──► Add3DBuffer
导出 GetDraw2dables:2782 / GetDraw3dables:2928(克隆 + 投影/并体 + 写 BIM XData)
2. 内存问题
2.1 P0:m_pGripAppData 在 dwgInFields 里重复分配
| 位置 | 代码 |
|---|---|
构造 :1682 |
m_pGripAppData = new AecGripData[GRIP_MAXNUM]; |
dwgInFields:1707 |
m_pGripAppData = new AecGripData[GRIP_MAXNUM]; ← 又分配一次,旧指针直接丢弃 |
析构 :1690 |
delete [] m_pGripAppData;(只删最后一块) |
GRIP_MAXNUM 为 256(EarThwormDef.h:163),AecGripData 含 szTip[256] + szID[256] + szLockId[39] + 3 个 AcGePoint3d + 1 个 AcGeVector3d,sizeof 约 1.2 KB。每个设备实体开图即泄漏约 300 KB,1000 个设备 ≈ 300 MB,且每次重开图纸累加。
同一处还有两个次要泄漏:
:1709读版本号失败、:1712版本过高返回eMakeMeProxy时,新数组同样丢弃:1720pFiler->readString(&pszTmp)分配的字符串没有acutDelString(pszTmp)(对比AecDbEwFitting.cpp:639的正确写法),每次开图泄漏一个 GUID 长度的小块
修复:删掉 :1707 的重复分配(构造函数已经分配过);readString 用完补 acutDelString。改动 3 行,收益最大。
2.2 P0:析构函数不释放几何单元
AecDbEwEquipment::~AecDbEwEquipment(void) // :1686
{
Clean3DBuffer();
Clean2DBuffer();
delete [] m_pGripAppData;
} // m_arSubPrimPtrs 完全没处理
CPtrArray 析构不会删除元素,~XPrimData 也不删 m_pData。所以每个被销毁的设备实体,泄漏全部几何单元 + 参数块。开图后关图、复制粘贴、clone() 的临时对象销毁,都走这条路。
同类 AecDbEwFitting 的析构(:584)同样没处理它的子单元数组,属于同一批遗留写法。
修复:析构里补 FreeSubPrimData();(成员函数,见 2.3 与 2.13)。
2.3 P0:FreeBuffer(CPtrArray&) 只释放数据块,不释放结构体
void FreeBuffer(CPtrArray& arSubPrimPtrs) // :1520
{
for (...) {
AecEqui::XPrimData* pData = (AecEqui::XPrimData*)arSubPrimPtrs.GetAt(i);
if (pData != NULL) {
if (pData->m_nSize > 0)
delete[](byte*)pData->m_pData; // 只删 m_pData
}
}
arSubPrimPtrs.RemoveAll(); // 结构体本身没删
}
被 copyFrom:2181 调用,于是 copyFrom 每次都泄漏被覆盖的那批 XPrimData 结构体。修 2.2 时会连带放大这个问题。
另外这里无条件 delete[](byte*),而 releaseData() 是按类型区分 delete[] / delete 的。
核查 m_pData 的全部分配点后,结论是 releaseData() 的类型判断不可靠——同一个类型在内存里可能是对象,也可能是字节数组:
| 来源 | 分配形式 |
|---|---|
dwgInFieldsForXPrim:206(开图) |
new byte[n],所有类型 |
byJson:833(JSON 导入) |
new PVOID[n],所有类型 |
copyXPrimData:150(深拷贝) |
new byte[n],所有类型 |
CoreTools.cpp:2036 / :4565 / :4662 |
new byte[] / new char[] |
各命令模块的 setData<T>(8 处) |
kExtrude/kRevolve 用 new byte[],其余 new T |
PipelineCmd/command.cpp:18670 等 5 处 |
new AecEqui::XNozz / new AecEqui::XBOX |
例如 kBox 从对话框创建时是 new AecEqui::XBOX,开图读回时却是 new byte[]。按 m_eType 判断,必然有一半场景不匹配(MSVC 下 POD 数组的 delete 与 delete[] 实际都走 free,所以一直没暴露)。
改法:统一表示,再统一释放规则。
- 把 13 处对象形式的分配改成
new unsigned char[sizeof(T)](sizeof(T)与原来一致,后续(T*)m_pData的用法不变) releaseData()去掉 switch,只保留delete[] (unsigned char*)m_pData~XPrimData()改为先调releaseData(),顺带修掉m_Combins子单元的m_pData泄漏
这样 AecDbEwEquipment.cpp 里的 ReleaseXPrimData 就不需要了,直接 pData->releaseData(); DELETE_PTR(pData);。
2.4 P0:subDMesh2NewSolid() 删错元素,且泄漏
void AecDbEwEquipment::subDMesh2NewSolid() // :3145
{
CPtrArray ar, subDs;
std::vector<int> idxs(0);
for (int i(0); i < m_arSubPrimPtrs.GetCount(); ++i) {
AecEqui::XPrimData *p = (AecEqui::XPrimData *)m_arSubPrimPtrs[i];
if (p->m_eType == AecEqui::kSubDMesh) {
idxs.push_back(i); // 记录的是"原始下标"
subDMesh2NewSolid(p, ar);
}
}
for (size_t i(0); i < idxs.size(); ++i)
m_arSubPrimPtrs.RemoveAt(i); // ← 用的是 i,不是 idxs[i]
...
}
两个错误叠加:
- 删错元素:
RemoveAt(i)删的是第 0、1、2… 个元素,而不是记录的 SubDMesh 下标。只要有一个 SubDMesh,就会把第一个正常单元删掉,SubDMesh 本身反而留着(后续Draw3dSolid对kSubDMesh是assert(0))。 - 泄漏:
RemoveAt只摘指针,被摘掉的XPrimData及其m_pData全部泄漏。
正确写法是按下标从大到小删,并先 releaseData() + DELETE_PTR。
这条只在"图纸里含 Revit 子网格单元"时触发,但一旦触发就是几何丢失,属数据正确性问题。
2.5 P1:RemoveSubPrimObj / RemoveSubPrimObjByID 不调 releaseData()
:4276 和 :4291 都是 DELETE_PTR(pSolid),而 ~XPrimData 不删 m_pData。删一个单元泄漏一个参数块(box/cylinder 几百字节,Revit 单元可能几十 KB)。
2.6 P1:GetDraw2dables 克隆后丢弃
:2843 对 2D 缓存里每个实体 clone(),然后:
AcDb3dSolid→OrthoProjected3DSolid后continue(:2850)AcDbSubDMesh→OrthoProjected3DSubMesh后continue(:2857)
OrthoProjected3DSolid 只是把传入实体喂给 HLR 收集器(CoreCommand/HlrCommands.cpp:214),不接管所有权。这两条 continue 的克隆体全部泄漏,每次调用泄漏 N 个 solid。
2.7 P1:剖切相关的克隆泄漏
| 位置 | 问题 |
|---|---|
GetSectionDraw:12691 |
return OldGetSectionDraw(...) 时没有 delete pFit(:12677 克隆的整份设备) |
OldGetSectionDraw:12620 |
逐个子实体 clone(),只对 AcDb3dSolid 做了投影,非 solid 的克隆体既不进结果也不释放 |
OldGetSectionDraw:12609 |
GetDraw3dables(arSubPtrs,false) 产出的整组克隆从头到尾没释放 |
剖切是低频操作,但一次剖切可能泄漏几十个实体。
2.8 P1:GetClosestPointToSUbPrim 泄漏 region,并写库
AcDbObjectId idSH = CoreTools::AddToModelSpace(pSolid); // :3986 把临时体写进模型空间
...
pFace->convertToRegion(regions); // :4032 新建 region
for (...) { ... } // regions 里的对象从不释放
delete pEnt; // :4049 只删了 explode 出来的本体
两个问题:
convertToRegion产出的 region 全部泄漏- 为了算一个最近点,把临时 solid 加进当前图纸的模型空间再 erase。这会触发数据库事务、undo 记录、反应器,是明显的性能与副作用问题
2.9 P2:SortAecComplexCurves 丢弃空曲线
:11230 先把非空曲线挑进 std::vector,然后 arPrimCplPtrs.RemoveAll() 再回填。空曲线指针被直接丢掉,没有 delete。PrimPorjectToPlan:8507 每次都调用,投影为空的单元越多,泄漏越多。
2.10 P2:ByJson 失败路径泄漏
:12853 new XPrimData,byJson 返回 false 时不入数组也不删除,连带已解析出来的 m_pData 一起泄漏。另外 ByJson 没有先清空 m_arSubPrimPtrs,对已有几何的对象再导入会叠加重复单元。
2.11 P2:MakeBlockRefSet 失败路径泄漏
:12540 new AcDbBlockTableRecord,若 getBlockTable 失败(:12559)或 pTbl->add 失败,pRec 既不关闭也不删除。appendAcDbEntity 失败时(:12552)实体也没关闭。
2.12 内存问题汇总
| 级别 | 问题 | 位置 | 触发频率 | 单次量级 | 状态 |
|---|---|---|---|---|---|
| P0 | 夹点数组重复分配 | :1707 |
每个实体每次开图 | ~300 KB | 已修 |
| P0 | 析构不释放几何单元 | :1686 |
每个实体销毁 | 全部参数块 | 已修 |
| P0 | FreeBuffer 不删结构体 |
:1520 |
每次 copyFrom |
N 个结构体 | 已修 |
| P0 | subDMesh2NewSolid 删错+泄漏 |
:3159 |
含 SubDMesh 的图 | N 个单元 + 几何错乱 | 已修 |
| P1 | RemoveSubPrimObj* 漏 releaseData |
:4276 :4291 |
编辑删除单元 | 1 个参数块 | 已修 |
| P1 | GetDraw2dables 克隆泄漏 |
:2850 :2857 |
2D 导出/剖切 | N 个 solid | 已修 |
| P1 | 剖切克隆泄漏 | :12691 :12620 |
每次剖切 | N 个实体 | 已修 |
| P1 | readString 未释放 |
:1720 |
每次开图 | 小 | 已修 |
| P1 | convertToRegion 泄漏 |
:4032 |
最近点查询 | N 个 region | 已修 |
| P1 | GetDraw3dables 合并后不删入参 |
:2975 |
每次导出 3D | N 个 solid | 已修 |
| P2 | 空曲线丢弃 | :11240 |
每次 2D 投影 | 小 | 已修 |
| P2 | ByJson / MakeBlockRefSet 失败路径 |
:12853 :12540 |
异常路径 | 小 | 已修 |
“已修”的改动见 2.13。
2.13 第一批内存修复的实施记录
改动集中在 CoreLibrary/AecDbEwEquipment.cpp,不涉及头文件和存储格式。
| # | 实际改法 | 与原方案的差异 |
|---|---|---|
| 1 | 删掉 dwgInFields 里的 new AecGripData[GRIP_MAXNUM];readString 后补 acutDelString(pszTmp) |
同原方案 |
| 2 | 析构补 FreeSubPrimData() |
同原方案 |
| 3 | FreeSubPrimData() 成员函数:逐个 releaseData() + DELETE_PTR,最后 RemoveAll |
见 2.3,先统一了 m_pData 表示 |
| 4 | subDMesh2NewSolid() 按 idxs 倒序释放 + RemoveAt |
同原方案,另把 size_t i 改成 int i 倒序 |
| 5 | RemoveSubPrimObj / RemoveSubPrimObjByID 改用 releaseData() + DELETE_PTR |
同原方案 |
| 6 | GetDraw2dables 两个 continue 前补 DELETE_PTR(pClnEnt) |
同原方案 |
| 7 | OldGetSectionDraw 循环内补 DELETE_PTR(pSubCln)、循环后补 FreeEntityArray(arSubPtrs);GetSectionDraw 两处 return OldGetSectionDraw 前补 delete pFit |
同原方案 |
| 8 | convertToRegion 的 regions 循环结束后逐个 DELETE_PTR;顺带把内层遮蔽的 i 改为 j |
同原方案 |
| 9 | SortAecComplexCurves 空曲线改为 DELETE_PTR |
同原方案,另补 pCrv == NULL 判断 |
| 10 | ByJson 循环前 FreeSubPrimData(),byJson 失败时 releaseData() + DELETE_PTR |
同原方案 |
| 11 | MakeBlockRefSet 的 getBlockTable / pTbl->add 失败时 pRec->close() 并返回 false |
同原方案 |
| 12 | dwgInFields 的 m_arSubPrimPtrs.RemoveAll() 改为 FreeSubPrimData() |
新增,原注释写着 FreeBufer 但没真释放 |
| 13 | GetDraw3dables 的 booleanOper(kBoolUnite) 之后补 DELETE_PTR(pSolid) |
新增,见 3.15 |
函数命名整理(AecDbEwEquipment.cpp 原来有 4 个文件作用域、外部链接的 FreeBuffer*,与其他 cpp 里的同名 static 函数容易混淆):
| 原名 | 现名 | 说明 |
|---|---|---|
FreeBuffer(CPtrArray&) |
AecDbEwEquipment::FreeSubPrimData() |
只处理 m_arSubPrimPtrs,改成成员函数后参数不再需要 |
FreeBuffer(AcDbVoidPtrArray&) |
static FreeEntityArray() |
通用实体数组,加 static 收回内部链接 |
FreeBuffer2(CPtrArray&) |
static FreeComplexCurveArray() |
AecComplexCurve 数组 |
FreeBuffer3(CPtrArray&) |
static FreeFaceplanArray() |
Aec3dFaceplan 数组 |
尚未处理:
- 2.8 的第二个问题(为了算最近点把临时 solid 写进模型空间再 erase),属于副作用与性能,放在第三批
3. 潜在缺陷
3.1 P0:存盘时把组合设备的子单元清空(已修复,PR #48)
void dwgOutFieldsForXPrim(AecEqui::XPrimData *pSolid, ...) // :239
{
...
if (CurrentVer >= 5)
{
// 做一下容错
if (pSolid->m_eType != AecEqui::kRevitCombination ||
pSolid->m_eType != AecEqui::kRevitCombination) // ← 恒真
pSolid->m_Combins.clear();
...
}
}
x != A || x != A 永远为真(第二个条件显然是想写 kCombination)。结果:
- 任何带
m_Combins的单元,在存盘前子单元被清空,写出去的就是 0 个 - 因为是就地
clear(),当前内存里的对象也被破坏,存完盘再画就没了 - 这段位于
const的dwgOutFields调用链上,等于在"只读"函数里改数据
影响面(2026-10-23 核实):kCombination 仍在用,PipelineCmd/EquipmentMakeDlg.cpp:4357 创建组合设备时会把被组合设备的子单元逐个 push_back 进 m_Combins,toJson:789 与 Draw3dSolid:3844 也都按这个列表递归。也就是说这条不是"可能有用",而是新建的组合设备只要存盘一次,几何就永久丢失(Revit 组合走 kRevitCombination,数据在 m_pData 里,不受影响)。
已修复(PR #48):删掉整段容错,改为按实际数量写出,不再修改对象状态。没有按原方案"修成 &&",也没有把容错挪到读入,原因:
m_Combins只有组合单元才会非空,写端正确后读端不可能读到"非组合类型带子单元"的数据,容错没有作用对象- 写端一旦按
m_eType过滤,就会重现"类型判断错→丢数据"的同类风险 - 存盘函数改对象状态本身就是错的,无论条件对不对
顺带处理:跳过 NULL 指针并单独计数,避免"写了数量但循环少写一条"导致读写错位;删掉原来定义后未使用的 int num。
3.2 P1:subErase 里 Find 当布尔用(已修复)
if (eSubType == eValve || sName.Find(_T("阀")) != -1 || sName.Find(_T("接头"))) // :2119
CString::Find 找不到返回 -1,非 0 即真,所以第三个条件恒真。整句等价于"永远是阀门",删除任何设备都会走合并管线那段逻辑(GetInterface + PushGripMessage)。应写成 != -1。
已修复:补 != -1。删除普通设备不再误走阀门/接头的合并管线分支。
3.3 P1:DrawHandFlg 用 == 赋值(已修复)
if (nVt == 1) sFlag = _T("M");
else if (nVt == 2) sFlag = _T("G");
else sFlag == _T("Y"); // :2656 应为 =
液动(3)时文字为空,画出来是空标注。另外这里直接取 GetInterface(0) / GetInterface(1),接口不足 2 个时 inf2 是默认值,distanceTo 为 0、normalize() 得到零向量,后续 angleTo 结果不可信。
已修复:== 改为 =,液动阀(3)现在会画出 Y。接口不足 2 个时 inf2 取默认值、方向不可信的问题未动,属于显示容错,另行评估。
3.4 P1:ProcessHlrResult 在遍历中删 map 元素(已修复)
for (; it != _preAr.end(); ++it)
{
if (AcDbRegion::cast(it->first))
_preAr.erase(it); // :13133 迭代器失效,下一轮 ++it 是 UB
}
应改为 it = _preAr.erase(it);。这是典型的偶发崩溃点(容器元素少时不容易复现)。
已修复:改成 while + erase(it++)。没有用 it = erase(it),因为 R190 走的是 C++98 标准(cmakelists.txt 按 SDK 版本切换),C++03 的 map::erase(iterator) 返回 void。
3.5 P1:GetSectionDraw 返回值取反(已修复)
int nCount = arRegionPtrs.length();
GetSectionDraw(plan, vtTo, arRegionPtrs);
int nCoundPre = arRegionPtrs.length();
return (nCount - nCoundPre); // :12762 顺序反了
返回的是"调用前 − 调用后",恒为 0 或负数。若调用方用它判断"是否剖到东西",判断必然失败。
已修复:改为 nCoundPre - nCount。全库调用点都忽略这个返回值,所以此前没有实际故障。
注意:AecDbEwFitting.cpp:16523、AecDbECFitting.cpp:8751 的同名函数是一模一样的写法(同样的错误),本次未改,见 5.4。
3.6 P1:HLR_CALC 标志会被存进图纸
Draw3d:3009 一旦读到 HLR_CALC 为真就直接返回,什么都不画。这个标志由 PreprocessForHlr:13204 / :13360 置真、ProcessHlrResult:13107 复位。
但 m_mapSpecXData 的序列化(AecRxBase/AecDbObjXDataMap.cpp:2034)是全量写,没有任何过滤。所以只要在"置真之后、复位之前"存盘(剖切过程中保存、崩溃、取消),标志就会留在 DWG 里,下次开图这个设备在 3D 里永久不可见。
建议:HLR_CALC 这类纯运行时标志不要进 m_mapSpecXData,或序列化时按 key 白名单/黑名单过滤。
3.7 P1:GetInterface 两个重载单位不一致(已修复)
| 重载 | 位置 | 是否乘 dUnit(米制 0.001) |
|---|---|---|
GetInterface(int, AecInterface&) |
:3323 |
是(inf.dSize1 = pNoze->dVal1 * dUnit) |
GetInterface(AcArray<AecInterface>&) |
:3262 |
否(直接 pNoze->dVal1) |
米制图纸里,同一份数据经两个入口读出来的接口尺寸差 1000 倍。Draw*Vlv2d 用下标版,subErase / PreprocessForHlr 用数组版,两边算出来的东西对不上。
已修复:数组版补 * dUnit,与下标版一致。
- 数组版里
ptPos本来就transformBy(xform)缩放过,只有尺寸没缩,属于「位置按米、尺寸按毫米」的自相矛盾 dV/dA是电压电流(EarThwormDef.h:518),不是长度,不参与换算- 影响面:数组版的 4 处内部调用(
getOsnapPoints、subErase、DrawInfArrow、vtAdjust)只用ptPos/vtDir;外部消费dSize1的是CoreCommand/pcf.cpp:1044的pcf_ewEquipment,米制图纸导出的 PCF 管径此前大了 1000 倍 - 只在米制图纸(
INSUNITS == kUnitsMeters)里有差异,毫米图纸dUnit为 1,行为不变
顺带修复:同一函数里 inf.bSizeUnLock = true; 原来写在循环外,循环内的 memset(&inf, 0, sizeof(AecInterface)) 每次都把它清成 false,数组版返回的接口实际一直是「锁定」。已把赋值挪到 memset 之后,循环外那两行初始化(bSizeUnLock / dGasket)随之删掉——memset 之后本来就是 0。bSizeUnLock 目前只被 i_AecDbXEquipment.cpp 序列化,没有读取判断的地方,所以此前没有暴露成故障。
3.8 P1:copyFrom 漏拷字段(已修复)
:2174 只拷了 m_mapSpecXData / m_nInfActive / m_ptInsert / m_vtDir / 几何单元,没有拷 m_bVert、m_idDash、m_idCenter。复制粘贴一个竖放阀门,粘贴出来是横放的;两个图层软指针也丢了(虚线和中心线会跑到默认图层)。
已修复:补拷 m_bVert / m_idDash / m_idCenter。
3.9 P2:MakeBlockRefSet 的范围累加逻辑错(已修复)
for (...) {
pEnt = (AcDbEntity*)arEntPtrs.at(i);
if (!AcDbBlockReference::cast(pEnt))
pEnt->getGeomExtents(extAdd);
if (isValidExtents(ext)) // :12522 判断的是累加器,不是本次结果
ext.addExt(extAdd);
}
- 第一轮
ext还是默认(空)状态,isValidExtents(ext)为假 → 第一个实体的范围永远加不进去 - 若
pEnt是块参照,extAdd保持上一轮的旧值,会被重复累加 - 若整个循环没加进任何范围,
ptIns由默认范围算出,块原点落在原点而不是设备位置
应改成 if (isValidExtents(extAdd)) ext.addExt(extAdd);,块参照分支直接 continue。
已修复:块参照直接 continue;extAdd 每轮重新构造并检查 getGeomExtents 返回值;判断对象改为 extAdd。
3.10 P2:byJson 的 m_pData 尺寸对不上(已修复)
p->m_nSize = it->value.GetInt(); // 字节数(来自 __nSize__)
GEO2JSON::binary br(it->value);
p->m_pData = new PVOID[br._len]; // 按 PVOID 个数分配
::memcpy(p->m_pData, br._data, sizeof(PVOID) * br._len); // :820
分配长度取决于 br._len,使用长度取决于 m_nSize,两者来自不同字段。JSON 被改过或版本不一致时就会越界读写。应按 m_nSize 分配并校验 sizeof(PVOID)*br._len >= m_nSize。
已修复:br._len 本来就是字节数,改为 new byte[br._len] + memcpy(..., br._len),并用 br._len 覆盖 m_nSize(两者不一致时以实际数据长度为准)。
3.11 P2:开图读入没有边界校验(已修复)
dwgInFieldsForXPrim:172:
readInt32(&nSize)后直接new byte[pSolid->m_nSize](:193),超大值没有检查(负值被> 0挡住,不会分配)readBytes的返回值没有检查m_Combins是递归读(:227),没有深度上限,损坏的图纸可以打爆栈
同类的 AcDbX3dSolid 阶段一、二已经补过类似的防护,这里可以照搬。
已修复:加 16MB 上限(kMaxXPrimDataSize),超限 setFilerStatus(Acad::eInvalidInput) 让整个 dwgInFields 退出;readBytes 检查返回值,失败时释放并清零。m_Combins 的递归深度上限未加,损坏图纸仍可能打爆栈,优先级低。
3.12 P2:PreprocessForHlr 的"对面"被变换了两次(确认不改)
AcGeMatrix3d mat4(mat3.inverse()); // mat3 = 沿截面法向平移 -H/2
for (...) {
AcDbEntity *pNew = (AcDbEntity*)pEnt->clone();
pNew->transformBy(mat4);
pNew->transformBy(mat4); // :13353 又乘一次
_other.append(pNew);
}
原实体在 -H/2 位置,克隆体被移到 +H 位置(两次 +H/2),两侧不对称。
2026-10-25 结论:是故意的,不改。 mat3 只移动半个设备高度,乘两次正好凑成一个设备高度,剖切体的前后两面本来就该相距一个完整高度。
3.13 P2:GetBodyBox 恒返回 TRUE(已修复)
:4205 无论有没有取到实体都 return TRUE;且 ar3D 引用在 Draw3d 之前就绑定了(:4207)。当前 Get3DBuffer() 返回成员引用,重新填充不会换对象,所以能工作;但如果哪天改成返回副本就会静默失效。返回值也应反映实际结果。
已修复:返回实际是否取到范围;_ext 改为每轮新建,避免把上一轮的累积结果重复并入。11 处调用点都忽略返回值,行为不变。
3.14 P2:clearGeoBuffer() 是空实现(已修复)
:12861 函数体只有 AssertWriteEnabled();。PipelineCmd/EquipmentMakeDlg.cpp 有 8 处调用,本意应该是清掉 2D/3D 缓存(Clean2DBuffer / Clean3DBuffer)。现在是"调了没反应",改了参数后旧几何还在。
已修复:改为 Clean2DBuffer(); Clean3DBuffer();。调用方(EquipmentMakeDlg、EQU_MODIFY、ParamEquJson)都是改完参数后调它,语义一致。
3.15 P2:booleanOper 的所有权写法在本文件里自相矛盾
同一文件里两种写法并存:
| 位置 | 写法 | 若 SDK 会删除入参 | 若 SDK 不删除入参 |
|---|---|---|---|
:3790 |
booleanOper(...); DELETE_PTR(p); |
二次释放 | 正确 |
:3883 :3980 |
booleanOper(...) 后统一 deleteArrayVoidPtrs |
二次释放 | 正确 |
:14198 |
booleanOper(...); DELETE_PTR(pSolid); |
二次释放 | 正确 |
:2975 |
booleanOper(kBoolUnite, pSolid),克隆体不再处理 |
正确 | 每次泄漏一个克隆 |
本文件 5 处调用里 4 处都主动释放入参,:2975 是唯一的例外。
2026-10-23 结论:按「booleanOper 不删除入参」处理,:2975 补 DELETE_PTR(pSolid)(见 2.13 #13)。依据是同一文件另外 4 处(含失败路径 :2002 的 DELETE_PTR(pSolidInside))都按调用方负责释放来写;若 SDK 实际会删除入参,这些既有代码早就在正常流程里二次释放崩掉了。
另外 CoreLibrary/AcDbX3dSolid.cpp:1998 的 booleanOper 成功路径没有释放 pSolidInside,同一约定下也是一个泄漏点,不在本次范围。
3.16 其他
GRIP_MAXNUM在两处定义:EarThwormDef.h:163是 256,AecWPipe.h:231是 24。当前AecWPipe.h没有被包含,暂时没冲突;一旦谁把它引进包含链,m_pGripAppData的分配与使用边界就会不一致ReadFromFile:4843的Adesk::Int32 id = ::_time32(NULL);是死变量,实际用的是++pX->m_nID,同一秒导入多个单元时 ID 可能重复Draw2d:2498的if (dc.IsDragging()) { /*return*/ }是空壳,拖拽时仍然读旧 2D 缓存,显示滞后一帧
4. 性能
4.1 3D 绘制链路
Draw3d:3002 每次调用:
DrawInfArrow→GetInterface(arInfs)→ 每个管嘴一次GetInfFaceInf- 有 3D 缓存就直接画缓存(快)
- 没有缓存 →
Draw3dSolid:3905:逐单元Draw3dSolid(XPrimData)造体(每个单元一次acisIn/createXxx),有差集时再做一轮并集 + 一轮差集 BoolUnit3DBuffer(0.001):米制时把缓存里每个 solid 再transformBy一次- 逐个
setTransparency Draw3DBuffers
成本主要在 3:N 个单元 = N 次造体 + (N−1) 次布尔,与 AecDbX3dPart 的结论一致。
4.2 P0:拖拽时缓存不清理,且每帧全量重建(已修复,PR #53)
if (!dc.IsDragging()) // 改前
{
if (Has3DBuffer()) { 画缓存; return; }
}
Draw3dSolid(&dc, nColor); // 拖拽时每帧都走到这里
而 Add3DBuffer(AecDbEntityCommon.cpp:146)只是 append,不清理已有内容。于是拖拽过程中:
- 每帧重新造 N 个 solid
- 每帧往 3D 缓存里再追加 N 个
Draw3DBuffers把缓存里所有实体都画一遍
第 k 帧要造 N 个体、画 k×N 个体。内存随帧数线性涨、帧率随帧数线性掉。
2026-10-25 已修复(PR #53):去掉 !dc.IsDragging() 判断,拖拽时也直接走 3D 缓存。原先写这个判断的原因是「解开后崩溃」,实际测试并不崩溃——崩溃来自 4.2 的缓存无限追加,而不是拖拽本身。缓存长度不再随帧数增长,Draw3dSolid 拖拽期间一次都不调用。
原计划的另外两个做法(拖拽时画线框、增量 transformBy)不需要了。
4.3 P1:2D 符号绘制触发 3D 布尔(暂缓)
11 处 Draw*Vlv2d 里调 GetBodyBox(ext)(:5020 :5189 :5357 :5512 :5675 :6307 :6492 :6654 :6816 :6996 :7147),而 GetBodyBox 在没有 3D 缓存时会 Draw3d —— 为了画一个 2D 阀门手柄的方向,把所有单元造一遍体再做布尔。
2026-10-25 核实:原文的「缩放频繁时开销明显」写错了。 Draw2d 开头 if (Has2DBuffer())(:2545)就直接画缓存并返回,根本走不到 Draw*Vlv2d。缩放、平移、重生成都不会清缓存,只有改参数(clearGeoBuffer)、图层变化等才会。
所以准确的触发条件是:2D 缓存失效后的第一次绘制,且此时 3D 缓存也为空(平面图场景下 3D 缓存一直是空的)。是一次性开销,不是每次绘制的持续开销。
结论:暂缓。真要优化,收益只在「改完参数后立刻看平面图」这一下,不值得为此把 25 个 Draw*2d 的取尺寸逻辑改成参数 bbox。
4.4 P1:GetInterface 每次 acedGetVar + 临时实体(不做)
acedGetVar("INSUNITS")每个重载各调一次。2026-10-25 决定不缓存:acedGetVar读的是数据库级变量,开销很小,而且当前图纸单位可以在会话中途变化(切换图纸、INSUNITS被改),缓存反而要处理失效GetInfFaceInf每个面调两次GetTransformbyXform,每次构造一个临时AcDbLine/AcDbPoint再走TransformEntity。一个 6 面 box = 12 个临时实体;夹点显示时每个管嘴都要算
「单元 → 世界矩阵」是什么:TransformEntity(:1552)对实体做的是一串矩阵变换,顺序固定:
rotation(m_ptRotation.x, kXAxis) // 绕 X 轴转
rotation(m_ptRotation.y, kYAxis) // 绕 Y 轴转
rotation(m_ptRotation.z, kZAxis) // 绕 Z 轴转
translation(m_vtMove) // 平移到单元自身的偏移
m_arMatrix[i] // 逐项累乘附加矩阵
rotation(按 vtDir 先绕 Z 再绕法向) // 转到插入方向
translation(ptIns) // 平移到插入点
rotation(90°, kXAxis, ptIns) // 竖放时再翻一次
这串变换只依赖 (pData, ptIns, vtDir, bVert),与具体是什么实体无关。现在为了变换「一个点」或「一个方向」,要先把它们塞进一个临时实体、跑完这一串 transformBy、再把结果读出来——矩阵算了一遍又一遍。
所谓「提成世界矩阵」,就是把这串矩阵按同样顺序 postMult 合成一个 AcGeMatrix3d M,之后直接 ptGe.transformBy(M) / vtDir.transformBy(M)。GetInfFaceInf 里 12 次临时实体可以全部去掉,只剩 12 次向量乘法。
这条2026-10-25 决定不做:GetInfFaceInf 只在夹点显示 / osnap 这类交互路径上跑,12 次临时实体的构造与 transformBy 相对于整条路径的开销占比很小,合成矩阵后省下的时间测不出来,反而多一份需要与 TransformEntity 保持同步的矩阵逻辑(后者一改,这里就错)。收益不明显,维持现状。
4.5 P1:GetDraw2dables 每次建 *U 块 + 跑 HLR(暂缓)
调 CKTCadUtility::CreateBlockTableRecord(ar, ptIns, NULL, ...),pszBlk == NULL 时会在当前图纸里新建一个匿名块表记录(KTCadUtility.cpp:537)。也就是说一次"取 2D 图形"的查询操作会往 DWG 里写数据:块表增长、undo 记录、文件变大。同时 OrthoProjected3DSolid 走完整 HLR(HlrCommands.cpp:214),每个 solid 一次。
2026-10-25 核实:
- 剖切功能已经不再调用它(可以确认)
- 但它是
AecDbEntityCommon的虚函数,CoreCommand/KTEarthWormCmd.cpp里有十几处直接调用(:6776:7884:12377等),AecDbEntityCommon.cpp:1137的entitySet路径也在用,用途不止剖切
所以「剖切不用了」不等于「可以改语义」。暂不动,等确认其余调用方各自的期望。
4.6 P1:开图路径(不做)
dwgInFields 里无条件 subDMesh2NewSolid(),对每个 kSubDMesh 单元做一次 ParseRevitFamily::parseJson(parseFamilyForRevit.cpp:1234:RapidJSON 解析 + 遍历生成 NewSolidBase)。Revit 族转来的设备,这一步是开图耗时的大头之一。
2026-10-25 评估结论:不能照 AecDbX3dPart 的方式惰性化。 两者的性质不同:
AecDbX3dPart |
AecDbEwEquipment |
|
|---|---|---|
| 懒加载的对象 | m_pSolid,布尔运算的派生结果 |
m_arSubPrimPtrs,持久化数据本体 |
| 延迟的代价 | 首次绘制时重算一次 | 对象在解析前处于「半初始化」状态 |
| 数组内容 | 不变 | kSubDMesh 被替换成若干新单元,长度和顺序都变 |
具体风险:
m_arSubPrimPtrs在本文件有 99 处引用、约 40 个读函数(GetInterface×2、GetInterfaceCount���Draw3dSolid、ImpGetGripPoints、GetSubPrimObj*、ActiveInterface、NozzhasFlage、13 个Draw*2d、PrimPorjectToPlan、ToJson…),每一处都要在开头调EnsureSubDMeshParsed(),漏一个就是「少管嘴 / 少几何 / 夹点错位」- 索引语义会变:
GetSubPrimObj(n)、SetActiveSubPrimObj(n)、RemoveSubPrimObj(n)的下标在解析前后指向不同单元。用户如果在解析前拿到下标,解析后再用,就删错/选错 copyFrom/ByJson拷的是「当前数组」,源对象没解析时目标也没解析,需要在拷贝时正确传递标志- 收益不确定:
AcDb开图后可能立刻调subGetGeomExtents,那样惰性化等于没省
建议的替代路径(按优先级):
- 先量:在真实图纸上统计「开图总耗时」与
subDMesh2NewSolid的累计耗时。占比不到 20% 就不值得动 - 若确实是大头,优先做「解析结果缓存」:以
m_pData内容的 hash 为 key 缓存解析出的XPrimData数组,第二次遇到同一个族直接深拷贝。语义完全不变,风险最低 - 最后才考虑惰性化,且必须把所有读入口一次改全
2026-10-25 决定不做:三步里的前两步(先量、解析缓存)都要先有真实图纸的耗时数据支撑,而这一步的改动面(dwgInFields + subDMesh2NewSolid + 可能的解析缓存)远大于收益的不确定性,暂不投入。等实际有用户反馈"开图慢"、并且能量到 subDMesh2NewSolid 占比超过 20% 时再回头做。
4.7 度量建议
| 指标 | 位置 | 目的 |
|---|---|---|
Draw3dSolid(XPrimData) 调用次数 / 累计耗时 |
:3526 |
确认造体占比 |
booleanOper 次数 / 累计耗时 |
:3867 :3964 |
确认布尔占比 |
| 3D 缓存长度峰值 | Draw3d:3036 |
验证 4.2 的膨胀 |
GetBodyBox 触发的 Draw3d 次数 |
:4205 |
验证 4.3 |
GetDraw2dables 建块次数 |
:2909 |
验证 4.5 |
开图总时长 / subDMesh2NewSolid 时长 |
:1791 |
验证 4.6 |
场景:单设备拖拽夹点、全图缩放、图层冻结后重生成、剖切、2D 导出。
5. 优化方案(分批)
5.1 第一批:内存与正确性(不改接口、不改格式)
分两批提交:内存问题已完成(见 2.13),逻辑缺陷已完成(见 3.2~3.14)。
| # | 改动 | 风险 | 状态 |
|---|---|---|---|
| 1 | 删掉 dwgInFields 里的重复分配,readString 补 acutDelString |
低 | 已完成 |
| 2 | 析构补 FreeSubPrimData() |
低 | 已完成 |
| 3 | releaseData() 统一 delete[],FreeSubPrimData() 释放 m_pData 后删结构体 |
低 | 已完成(见 2.3、2.13) |
| 4 | subDMesh2NewSolid 按下标倒序删除并释放 |
中 | 已完成 |
| 5 | RemoveSubPrimObj* 补释放 m_pData |
低 | 已完成 |
| 6 | subErase 的 Find 补 != -1 |
低(行为会变) | 已完成 |
| 7 | DrawHandFlg 的 == 改 = |
低 | 已完成 |
| 8 | ProcessHlrResult 改用 while + erase(it++) |
低 | 已完成 |
| 9 | copyFrom 补 m_bVert / m_idDash / m_idCenter |
低 | 已完成 |
| 10 | GetSectionDraw 返回值改 nCoundPre - nCount |
低 | 已完成 |
| 11 | clearGeoBuffer 实现为清 2D/3D 缓存 |
中(影响所有改参数路径) | 已完成 |
| 12 | MakeBlockRefSet 范围累加改判 extAdd |
低 | 已完成 |
| 13 | byJson 按字节长度分配 m_pData |
低 | 已完成 |
| 14 | dwgInFieldsForXPrim 加尺寸上限与读失败处理 |
低 | 已完成 |
| 15 | GetBodyBox 返回实际结果 |
低 | 已完成 |
| 16 | GetInterface 数组版补 dUnit |
低(仅米制图纸有差异) | 已完成 |
另外补做的 4 处内存修复(2.6、2.7、2.9、2.10、2.11、GetDraw3dables 合并泄漏、dwgInFields 的 RemoveAll)见 2.13。
5.2 第二批:m_Combins 存盘清空(已完成)
| 步骤 | 内容 | 状态 |
|---|---|---|
| 1 | 确认 kCombination 的使用:EquipmentMakeDlg.cpp:4357 创建时填充,toJson:789 / Draw3dSolid:3844 递归使用 |
已确认在用 |
| 2 | 删掉 :279 的恒真条件与 m_Combins.clear(),按实际有效数量写出 |
已完成(PR #48) |
| 3 | 不做读入容错:写端正确后不存在"非组合类型带子单元"的数据,加过滤反而可能误删 | 已取消 |
| 4 | 用含组合设备的样例做"存盘→重开→几何完整"验证 | 待实测 |
没有按原计划"修成 &&":m_eType 判断本身是多余的,m_Combins 只有组合单元才会非空;保留判断等于把正确性押在枚举比较上,和原来的错误同源。也没有数据迁移——受影响的图纸里组合设备的子单元已经被写没了,无法从 dwg 恢复,只能重建。
5.3 第三批:性能(先量后改)
| 优先级 | 改动 | 预期 | 状态 |
|---|---|---|---|
| 高 | 拖拽时走缓存,不重建(4.2) | 拖拽帧率、内存峰值 | 已完成(PR #53) |
| 高 | 2D 路径改用参数 bbox,不触发 3D(4.3) | 一次性开销,收益有限 | 暂缓 |
| 中 | 单元矩阵复用,去掉临时实体(4.4) | 夹点、osnap | 不做,收益不明显 |
| 中 | GetDraw2dables 不建块(4.5) |
导出、DWG 体积 | 暂缓,调用方未确认 |
| 低 | subDMesh2NewSolid 惰性化(4.6) |
开图 | 不做,改动大、收益不确定 |
INSUNITS 缓存(4.4 的第一条)已决定不做。
性能部分到此收尾:只有 4.2(拖拽走缓存)落地,其余全部按上面的结论维持现状。
5.4 需要先确认的事
—— 2026-10-23 按「不删除入参」处理,理由见 3.15booleanOper是否删除入参(3.15)—— 2026-10-25 核实全库调用点都忽略返回值,已按「返回新增个数」修正GetSectionDraw的返回值调用方到底期望什么(3.5)—— 2026-10-25 确认仍在用,见 3.1kCombination单元的实际存量(5.2)—— 2026-10-25 确认是故意的:PreprocessForHlr的mat4双次变换是否刻意(3.12)mat3移动半个设备高度,两次正好一个设备高度,不改HLR_CALC是否从m_mapSpecXData的序列化里排除(3.6):只有在剖切过程中出意外(保存、崩溃、取消)才会写进 dwg,目前没碰到,先放一放—— 2026-10-25 确认是 bug,已补;数组版内部 4 处调用不消费GetInterface数组版是否补dUnit(3.7)dSize,外部只有 PCF 设备导出受影响,见 3.7AecDbEwFitting.cpp:16523/AecDbECFitting.cpp:8751的GetSectionDraw同样写反,是否一起改
6. 风险与验证清单
| 改动 | 验证方式 |
|---|---|
| 夹点数组 / 析构释放 | 打开含 1000+ 设备的图纸,任务管理器看私有字节;关图后应回落 |
releaseData 统一 delete[] |
各类单元(box/cylinder/cone/Revit/组合)各删一个,确认无崩溃;开图→改参数→存盘→重开,几何完整 |
m_Combins 存盘 |
新建组合设备 → 存盘 → 关图 → 重开,组合几何完整;再存盘一次仍完整(验证不是"只活一次") |
| 剖切 / 2D 导出 | 对含阀门、法兰、普通设备的图各剖一次,反复开关剖切对话框,内存不持续上涨 |
subDMesh2NewSolid |
含 SubDMesh 的样例图,开图后单元数量与类型正确,绘制不缺体 |
subErase |
删除阀门/接头/普通设备各一次,确认只有阀门走合并管线 |
HLR_CALC |
剖切过程中保存再重开,设备 3D 仍可见 |
copyFrom |
复制粘贴竖放阀门,方向与图层保持 |
DrawHandFlg |
液动阀(驱动类型 3)2D 符号旁显示 Y |
ProcessHlrResult |
反复开关阀门剖切 2D 显示,不崩溃 |
clearGeoBuffer |
改单元参数(box/cylinder/cone)后关闭对话框,几何立即更新 |
MakeBlockRefSet |
剖切/2D 导出生成的块,插入点位于设备中心而不是原点 |
byJson |
JSON 导入设备,几何与参数正确 |
subErase 的 Find |
删除普通设备后,相邻管线不被合并 |
GetInterface 单位 |
米制图纸(INSUNITS = 6)里插入设备,接口尺寸与下标版一致;PCF 导出的管径不再大 1000 倍 |
| 拖拽优化 | 拖夹点 10 秒,内存不涨、帧率稳定;松手后几何位置正确 |
| 2D bbox | 各阀门 2D 符号方向、手柄位置与改前一致 |
本仓库没有单元测试,验证只能靠"编译 + CAD 内实测"。编译范围按既有约定:Debug + R190,只编 AecRxBase、CoreLibrary_BAL_REMOTE、CoreCommand_BAL_REMOTE、CoreImplement。
7. 明确不做
- 不改
XPrimData的存储布局和kCurrentVersionNumber(会带来新老插件不兼容,与AcDbX3dSolid阶段三取消的理由一致) - 不把 2D 符号系统(25 个
Draw*2d)重构成数据驱动,收益不明确、回归面太大 - 不动
AecDbEwFitting/AecDbHvacFitting等同类实体,除非确认它们的同一批写法也要一起改
8. 实施顺序
第一批 1~5(内存),单独一个 PR,可独立验证—— 已完成,范围扩到 2.1~2.11 全部内存项,见 2.13第二批(—— 已完成(PR #48),见 3.1、5.2m_Combins存盘清空)第一批 6~11(逻辑)—— 已完成,范围扩到 3.2~3.14 共 10 项,见 5.1确认 5.4 剩下的问题—— 3.7 已改(见 3.7),3.12 确认是故意的、不改,3.6 暂缓第三批(性能)—— 4.2 已完成(PR #53,用户自行修改并验证);4.3 / 4.5 暂缓,4.4 / 4.6 不做,性能部分收尾