共计 2362 个字符,预计需要花费 6 分钟才能阅读完成。
从内存泄漏案例说起
最近在团队代码审查时,发现一个使用 Cesium 的典型问题:某气象可视化项目在连续切换城市 3 次后,浏览器内存占用从 200MB 飙升到 1.2GB。通过 Chrome DevTools 的内存快照对比发现,未释放的 Entity 对象达到 4000+ 个,而根本原因是未正确清理 Viewer 的 EventToken。下图是问题复现时的内存对比:

左侧是正常情况,右侧是泄漏状态,可以看到残留在内存中的 Callback 函数形成了一条完整的引用链。这种问题在长期运行的 SPA 应用中尤为致命。
Token 的底层原理
Cesium 中的 Token 本质是事件订阅的凭证,主要涉及两个核心机制:
- 事件监听器链表 :每个事件类型维护一个监听器数组,Token 保存着当前监听器的索引位置
- 资源引用链 :Token 会持有对应的回调函数(closure),而回调函数往往引用了外部变量
当 Token 未被销毁时,整个引用链会阻止 GC 回收相关资源。常见的有三种持有关系:
- Viewer → Event → Callback → Entity
- Clock → onTick → Scene → Primitive
- ScreenSpaceEventHandler → Callback → DOM 元素
销毁方法对比
Cesium 提供了两种主要的销毁方式,但适用场景不同:
destroy() 方法
/**
* 完全销毁对象,释放所有关联资源
* @type {Cesium.Event.RemoveCallback}
*/
const token = viewer.scene.postRender.addEventListener(updateCamera);
// 错误示范:直接置空不会断开事件绑定
token = null;
// 正确做法:try {token.destroy(); // 内部会执行 removeEventListener
} catch (e) {console.error('销毁失败', e);
}
特点 :
– 彻底移除事件监听
– 清空所有内部引用
– 对象变为不可用状态
remove() 方法
// 适用于需要重复使用的对象
const handler = new Cesium.ScreenSpaceEventHandler(canvas);
const token = handler.setInputAction(zoomAction,
Cesium.ScreenSpaceEventType.LEFT_DOUBLE_CLICK);
// 临时移除但保留 handler
handler.removeInputAction(token);
// 需要时重新添加
newToken = handler.setInputAction(...);
适用场景 :
– 需要动态启停的交互
– 高频触发的事件
– 可复用的处理器
性能优化实践
通过测试 10000 次事件创建 / 销毁,得到如下数据:
| 方式 | 内存回收时间 | CPU 占用峰值 |
|---|---|---|
| 不做处理 | 未回收 | 82% |
| 仅 remove() | 1200ms | 45% |
| destroy() | 800ms | 38% |
| WeakMap 缓存 | 400ms | 28% |
推荐在高频场景使用 WeakMap 优化:
const tokenMap = new WeakMap();
class EntityController {constructor(viewer) {
this.viewer = viewer;
tokenMap.set(this, []);
}
addEvent() {const tokens = tokenMap.get(this);
tokens.push(this.viewer.clock.onTick.addEventListener(this.update.bind(this))
);
}
cleanup() {tokenMap.get(this).forEach(t => t.destroy());
tokenMap.delete(this);
}
}
生产环境避坑指南
SPA 路由处理
在 Vue Router/Angular 的 routerChange 事件中必须清理:
// Vue3 示例
import {onBeforeUnmount} from 'vue';
export default {setup() {const tokens = [];
const initCesium = () => {tokens.push(viewer.camera.moveStart.addEventListener(...));
};
onBeforeUnmount(() => {tokens.forEach(t => t.destroy());
});
}
}
React Hooks 方案
import {useEffect, useRef} from 'react';
function CesiumViewer() {const tokenRef = useRef([]);
useEffect(() => {
const token = viewer.entities.collectionChanged.addEventListener(handleChange);
tokenRef.current.push(token);
return () => {tokenRef.current.forEach(t => t.destroy());
};
}, []);
}
验证与调试
推荐使用 Cesium 内置的内存监控工具:
// 在控制台直接运行
Cesium.MemoryWatch.start();
// 输出当前内存状态
console.log(Cesium.MemoryWatch.getStats());
/* 典型输出结构
{
total: 1048576,
used: 524288,
entities: 123,
primitives: 45
}
*/
建议大家在实际项目中尝试这些方法,如果在性能优化中有其他发现,欢迎在评论区分享你的实战案例。记住:良好的 Token 管理习惯能让你的 Cesium 应用跑得更稳、更久!
正文完
发表至: 未分类
近一天内
