深入学习Canvas内存管理技术,避免内存泄漏,优化资源使用 Canvas应用中的内存管理至关重要。不当的内存管理会导致内存泄漏、性能下降甚至应用崩溃。
const memoryLeakTypes = {
canvasReferences: {
description: '未释放的Canvas引用',
example: `
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
canvas = null
`,
solution: '同时清除Canvas和Context引用'
},
eventListeners: {
description: '未移除的事件监听器',
example: `
const handler = () => {}
canvas.addEventListener('click', handler)
`,
solution: '在销毁时调用removeEventListener'
},
animationFrames: {
description: '未取消的动画帧',
example: `
const id = requestAnimationFrame(loop)
`,
solution: '保存ID并在销毁时调用cancelAnimationFrame'
},
closures: {
description: '闭包持有大对象引用',
example: `
const largeData = new Array(1000000)
setInterval(() => console.log(largeData.length), 1000)
`,
solution: '避免在闭包中持有大对象'
},
imageCache: {
description: '无限增长的图像缓存',
example: `
const cache = {}
function loadImage(src) {
if (!cache[src]) {
cache[src] = new Image()
}
}
`,
solution: '实现缓存淘汰策略'
}
}
class CanvasResourceManager {
constructor() {
this.canvases = new Map()
this.contexts = new Map()
this.eventListeners = new Map()
this.animationFrames = new Map()
this.intervals = new Map()
this.timeouts = new Map()
}
createCanvas(id, width, height) {
const canvas = document.createElement('canvas')
canvas.width = width
canvas.height = height
this.canvases.set(id, canvas)
this.contexts.set(id, canvas.getContext('2d'))
this.eventListeners.set(id, [])
return canvas
}
getCanvas(id) {
return this.canvases.get(id)
}
getContext(id) {
return this.contexts.get(id)
}
addEventListener(canvasId, type, handler, options) {
const canvas = this.canvases.get(canvasId)
if (!canvas) return
canvas.addEventListener(type, handler, options)
const listeners = this.eventListeners.get(canvasId)
if (listeners) {
listeners.push({ type, handler, options })
}
}
requestAnimationFrame(canvasId, callback) {
const id = requestAnimationFrame(callback)
this.animationFrames.set(id, canvasId)
return id
}
cancelAnimationFrame(id) {
cancelAnimationFrame(id)
this.animationFrames.delete(id)
}
setInterval(callback, delay, canvasId) {
const id = setInterval(callback, delay)
this.intervals.set(id, canvasId)
return id
}
clearInterval(id) {
clearInterval(id)
this.intervals.delete(id)
}
setTimeout(callback, delay, canvasId) {
const id = setTimeout(callback, delay)
this.timeouts.set(id, canvasId)
return id
}
clearTimeout(id) {
clearTimeout(id)
this.timeouts.delete(id)
}
destroyCanvas(id) {
const canvas = this.canvases.get(id)
if (!canvas) return
const listeners = this.eventListeners.get(id)
if (listeners) {
listeners.forEach(({ type, handler, options }) => {
canvas.removeEventListener(type, handler, options)
})
this.eventListeners.delete(id)
}
this.animationFrames.forEach((canvasId, frameId) => {
if (canvasId === id) {
cancelAnimationFrame(frameId)
this.animationFrames.delete(frameId)
}
})
this.intervals.forEach((canvasId, intervalId) => {
if (canvasId === id) {
clearInterval(intervalId)
this.intervals.delete(intervalId)
}
})
this.timeouts.forEach((canvasId, timeoutId) => {
if (canvasId === id) {
clearTimeout(timeoutId)
this.timeouts.delete(timeoutId)
}
})
this.contexts.delete(id)
this.canvases.delete(id)
canvas.width = 0
canvas.height = 0
}
destroyAll() {
this.canvases.forEach((_, id) => {
this.destroyCanvas(id)
})
}
getStats() {
return {
canvases: this.canvases.size,
contexts: this.contexts.size,
eventListeners: Array.from(this.eventListeners.values()).reduce((sum, arr) => sum + arr.length, 0),
animationFrames: this.animationFrames.size,
intervals: this.intervals.size,
timeouts: this.timeouts.size
}
}
}
class ObjectPool {
constructor(factory, reset, initialSize = 100) {
this.factory = factory
this.reset = reset
this.pool = []
this.active = new Set()
this.preallocate(initialSize)
}
preallocate(count) {
for (let i = 0; i < count; i++) {
this.pool.push(this.factory())
}
}
acquire(...args) {
let obj
if (this.pool.length > 0) {
obj = this.pool.pop()
} else {
obj = this.factory()
}
this.reset(obj, ...args)
this.active.add(obj)
return obj
}
release(obj) {
if (this.active.has(obj)) {
this.active.delete(obj)
this.pool.push(obj)
}
}
releaseAll() {
this.active.forEach(obj => {
this.pool.push(obj)
})
this.active.clear()
}
drain() {
this.pool = []
this.active.clear()
}
getStats() {
return {
pooled: this.pool.length,
active: this.active.size,
total: this.pool.length + this.active.size
}
}
}
class Vector2Pool extends ObjectPool {
constructor(initialSize = 100) {
super(
() => ({ x: 0, y: 0 }),
(obj, x = 0, y = 0) => {
obj.x = x
obj.y = y
},
initialSize
)
}
}
class ParticlePool extends ObjectPool {
constructor(initialSize = 500) {
super(
() => ({
x: 0, y: 0,
vx: 0, vy: 0,
life: 0, maxLife: 0,
size: 0, color: ''
}),
(obj, x, y, vx, vy, life, size, color) => {
obj.x = x
obj.y = y
obj.vx = vx
obj.vy = vy
obj.life = life
obj.maxLife = life
obj.size = size
obj.color = color
},
initialSize
)
}
}
class ArrayPool {
constructor(maxArrays = 50, maxArraySize = 1000) {
this.pool = []
this.maxArrays = maxArrays
this.maxArraySize = maxArraySize
}
acquire(size = 0) {
let arr
if (this.pool.length > 0) {
arr = this.pool.pop()
arr.length = size
} else {
arr = new Array(size)
}
return arr
}
release(arr) {
if (this.pool.length < this.maxArrays && arr.length <= this.maxArraySize) {
arr.length = 0
this.pool.push(arr)
}
}
getStats() {
return {
pooled: this.pool.length,
maxArrays: this.maxArrays
}
}
}
const arrayPool = new ArrayPool()
class LRUCache {
constructor(maxSize = 100, maxMemory = 100 * 1024 * 1024) {
this.maxSize = maxSize
this.maxMemory = maxMemory
this.currentMemory = 0
this.cache = new Map()
this.accessOrder = []
}
set(key, value, size = 0) {
if (this.cache.has(key)) {
this.touch(key)
return
}
while (this.cache.size >= this.maxSize || this.currentMemory + size > this.maxMemory) {
this.evict()
}
this.cache.set(key, { value, size })
this.accessOrder.push(key)
this.currentMemory += size
}
get(key) {
const item = this.cache.get(key)
if (item) {
this.touch(key)
return item.value
}
return null
}
has(key) {
return this.cache.has(key)
}
touch(key) {
const index = this.accessOrder.indexOf(key)
if (index > -1) {
this.accessOrder.splice(index, 1)
this.accessOrder.push(key)
}
}
evict() {
if (this.accessOrder.length === 0) return
const key = this.accessOrder.shift()
const item = this.cache.get(key)
if (item) {
this.currentMemory -= item.size
this.cache.delete(key)
if (item.value instanceof HTMLImageElement) {
item.value.src = ''
}
}
}
delete(key) {
const item = this.cache.get(key)
if (item) {
this.accessOrder = this.accessOrder.filter(k => k !== key)
this.currentMemory -= item.size
this.cache.delete(key)
}
}
clear() {
this.cache.forEach(item => {
if (item.value instanceof HTMLImageElement) {
item.value.src = ''
}
})
this.cache.clear()
this.accessOrder = []
this.currentMemory = 0
}
getStats() {
return {
size: this.cache.size,
maxSize: this.maxSize,
memory: this.currentMemory,
maxMemory: this.maxMemory,
utilization: (this.currentMemory / this.maxMemory * 100).toFixed(2) + '%'
}
}
}
class ImageCache {
constructor(maxSize = 100, maxMemory = 100 * 1024 * 1024) {
this.lru = new LRUCache(maxSize, maxMemory)
this.loading = new Map()
this.pendingCallbacks = new Map()
}
load(src) {
return new Promise((resolve, reject) => {
const cached = this.lru.get(src)
if (cached) {
resolve(cached)
return
}
if (this.loading.has(src)) {
if (!this.pendingCallbacks.has(src)) {
this.pendingCallbacks.set(src, [])
}
this.pendingCallbacks.get(src).push({ resolve, reject })
return
}
const img = new Image()
img.crossOrigin = 'anonymous'
img.onload = () => {
const size = img.width * img.height * 4
this.lru.set(src, img, size)
this.loading.delete(src)
resolve(img)
if (this.pendingCallbacks.has(src)) {
this.pendingCallbacks.get(src).forEach(cb => cb.resolve(img))
this.pendingCallbacks.delete(src)
}
}
img.onerror = (err) => {
this.loading.delete(src)
reject(err)
if (this.pendingCallbacks.has(src)) {
this.pendingCallbacks.get(src).forEach(cb => cb.reject(err))
this.pendingCallbacks.delete(src)
}
}
this.loading.set(src, img)
img.src = src
})
}
get(src) {
return this.lru.get(src)
}
has(src) {
return this.lru.has(src)
}
remove(src) {
this.lru.delete(src)
}
clear() {
this.lru.clear()
this.loading.clear()
this.pendingCallbacks.clear()
}
getStats() {
return {
...this.lru.getStats(),
loading: this.loading.size,
pending: this.pendingCallbacks.size
}
}
}
class MemoryMonitor {
constructor(options = {}) {
this.options = {
sampleInterval: 1000,
maxSamples: 60,
warningThreshold: 0.8,
criticalThreshold: 0.9,
...options
}
this.samples = []
this.isMonitoring = false
this.intervalId = null
this.listeners = {
warning: [],
critical: [],
sample: []
}
}
start() {
if (this.isMonitoring) return
this.isMonitoring = true
this.intervalId = setInterval(() => this.sample(), this.options.sampleInterval)
}
stop() {
if (this.intervalId) {
clearInterval(this.intervalId)
this.intervalId = null
}
this.isMonitoring = false
}
sample() {
const memory = this.getMemoryInfo()
this.samples.push({
timestamp: Date.now(),
...memory
})
if (this.samples.length > this.options.maxSamples) {
this.samples.shift()
}
this.checkThresholds(memory)
this.emit('sample', memory)
}
getMemoryInfo() {
const performance = window.performance || {}
const memory = performance.memory || {}
return {
usedJSHeapSize: memory.usedJSHeapSize || 0,
totalJSHeapSize: memory.totalJSHeapSize || 0,
jsHeapSizeLimit: memory.jsHeapSizeLimit || 0,
usedMB: (memory.usedJSHeapSize || 0) / 1024 / 1024,
totalMB: (memory.totalJSHeapSize || 0) / 1024 / 1024,
limitMB: (memory.jsHeapSizeLimit || 0) / 1024 / 1024,
utilization: memory.jsHeapSizeLimit
? memory.usedJSHeapSize / memory.jsHeapSizeLimit
: 0
}
}
checkThresholds(memory) {
if (memory.utilization >= this.options.criticalThreshold) {
this.emit('critical', memory)
} else if (memory.utilization >= this.options.warningThreshold) {
this.emit('warning', memory)
}
}
on(event, callback) {
if (this.listeners[event]) {
this.listeners[event].push(callback)
}
}
off(event, callback) {
if (this.listeners[event]) {
const index = this.listeners[event].indexOf(callback)
if (index > -1) {
this.listeners[event].splice(index, 1)
}
}
}
emit(event, data) {
if (this.listeners[event]) {
this.listeners[event].forEach(callback => callback(data))
}
}
getStats() {
if (this.samples.length === 0) {
return this.getMemoryInfo()
}
const latest = this.samples[this.samples.length - 1]
const avgUsed = this.samples.reduce((sum, s) => sum + s.usedMB, 0) / this.samples.length
const maxUsed = Math.max(...this.samples.map(s => s.usedMB))
const minUsed = Math.min(...this.samples.map(s => s.usedMB))
return {
...latest,
avgUsedMB: avgUsed,
maxUsedMB: maxUsed,
minUsedMB: minUsed,
sampleCount: this.samples.length
}
}
getTrend() {
if (this.samples.length < 2) return 'stable'
const recent = this.samples.slice(-10)
const first = recent[0].usedMB
const last = recent[recent.length - 1].usedMB
const change = (last - first) / first
if (change > 0.1) return 'increasing'
if (change < -0.1) return 'decreasing'
return 'stable'
}
}
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Canvas 内存管理</title>
<style>
body {
margin: 0;
padding: 20px;
background: #1a1a2e;
font-family: system-ui, sans-serif;
}
.container {
max-width: 900px;
margin: 0 auto;
}
h1 {
color: #eee;
text-align: center;
}
.canvas-wrapper {
background: #16213e;
border-radius: 8px;
padding: 10px;
margin-bottom: 20px;
}
canvas {
display: block;
background: #0f0f23;
border-radius: 4px;
margin: 0 auto;
}
.stats-panel {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 10px;
margin-bottom: 20px;
}
.stat-card {
background: #16213e;
border-radius: 8px;
padding: 15px;
text-align: center;
}
.stat-card h3 {
color: #aaa;
margin: 0 0 10px;
font-size: 14px;
}
.stat-card .value {
color: #00ff00;
font-size: 24px;
font-family: monospace;
}
.stat-card.warning .value {
color: #ffaa00;
}
.stat-card.critical .value {
color: #ff4444;
}
.controls {
display: flex;
gap: 10px;
justify-content: center;
flex-wrap: wrap;
}
button {
padding: 10px 20px;
background: #e94560;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
button:hover {
background: #ff6b6b;
}
button.danger {
background: #ff4444;
}
.log {
background: #0f0f23;
border-radius: 4px;
padding: 10px;
max-height: 150px;
overflow-y: auto;
font-family: monospace;
font-size: 12px;
color: #aaa;
}
.log .warning {
color: #ffaa00;
}
.log .critical {
color: #ff4444;
}
</style>
</head>
<body>
<div class="container">
<h1>Canvas 内存管理演示</h1>
<div class="stats-panel">
<div class="stat-card" id="memoryCard">
<h3>内存使用</h3>
<div class="value" id="memoryValue">0 MB</div>
</div>
<div class="stat-card">
<h3>对象池</h3>
<div class="value" id="poolValue">0 / 0</div>
</div>
<div class="stat-card">
<h3>图像缓存</h3>
<div class="value" id="cacheValue">0</div>
</div>
<div class="stat-card">
<h3>趋势</h3>
<div class="value" id="trendValue">稳定</div>
</div>
</div>
<div class="canvas-wrapper">
<canvas id="mainCanvas" width="800" height="300"></canvas>
</div>
<div class="controls">
<button onclick="createParticles()">创建粒子</button>
<button onclick="loadImages()">加载图像</button>
<button onclick="clearCache()">清除缓存</button>
<button onclick="forceGC()">触发GC</button>
<button onclick="toggleAnimation()">开始/暂停</button>
<button class="danger" onclick="simulateLeak()">模拟泄漏</button>
</div>
<div class="log" id="log"></div>
</div>
<script>
const canvas = document.getElementById('mainCanvas')
const ctx = canvas.getContext('2d')
const particlePool = new ParticlePool(500)
const imageCache = new ImageCache(50, 50 * 1024 * 1024)
const memoryMonitor = new MemoryMonitor()
let particles = []
let isRunning = false
let animationId = null
let leakedObjects = []
memoryMonitor.on('warning', (memory) => {
log('内存警告: ' + memory.usedMB.toFixed(2) + ' MB', 'warning')
document.getElementById('memoryCard').classList.add('warning')
})
memoryMonitor.on('critical', (memory) => {
log('内存严重: ' + memory.usedMB.toFixed(2) + ' MB', 'critical')
document.getElementById('memoryCard').classList.add('critical')
})
memoryMonitor.on('sample', (memory) => {
document.getElementById('memoryValue').textContent = memory.usedMB.toFixed(2) + ' MB'
const stats = particlePool.getStats()
document.getElementById('poolValue').textContent = `${stats.active} / ${stats.total}`
const cacheStats = imageCache.getStats()
document.getElementById('cacheValue').textContent = cacheStats.size
const trend = memoryMonitor.getTrend()
document.getElementById('trendValue').textContent =
trend === 'increasing' ? '上升' : trend === 'decreasing' ? '下降' : '稳定'
const card = document.getElementById('memoryCard')
card.classList.remove('warning', 'critical')
})
memoryMonitor.start()
function createParticles() {
const count = 100
for (let i = 0; i < count; i++) {
const particle = particlePool.acquire(
Math.random() * canvas.width,
Math.random() * canvas.height,
(Math.random() - 0.5) * 100,
(Math.random() - 0.5) * 100,
Math.random() * 3 + 1,
Math.random() * 4 + 2,
`hsl(${Math.random() * 360}, 70%, 60%)`
)
particles.push(particle)
}
log(`创建了 ${count} 个粒子`)
}
function loadImages() {
const colors = ['red', 'blue', 'green', 'yellow', 'purple']
const color = colors[Math.floor(Math.random() * colors.length)]
const size = 100 + Math.floor(Math.random() * 400)
const dataUrl = createColorImage(color, size, size)
imageCache.load(dataUrl).then(() => {
log(`加载了 ${size}x${size} ${color} 图像`)
})
}
function createColorImage(color, width, height) {
const tempCanvas = document.createElement('canvas')
tempCanvas.width = width
tempCanvas.height = height
const tempCtx = tempCanvas.getContext('2d')
tempCtx.fillStyle = color
tempCtx.fillRect(0, 0, width, height)
return tempCanvas.toDataURL()
}
function clearCache() {
imageCache.clear()
particles.forEach(p => particlePool.release(p))
particles = []
log('已清除所有缓存')
}
function forceGC() {
if (window.gc) {
window.gc()
log('已触发垃圾回收')
} else {
log('GC不可用,请使用 --expose-gc 启动')
}
}
function simulateLeak() {
for (let i = 0; i < 10000; i++) {
leakedObjects.push({
data: new Array(100).fill(Math.random())
})
}
log('模拟了内存泄漏 (10000 个对象)', 'warning')
}
function toggleAnimation() {
isRunning = !isRunning
if (isRunning) {
animate()
} else if (animationId) {
cancelAnimationFrame(animationId)
animationId = null
}
}
let lastTime = performance.now()
function animate() {
if (!isRunning) return
const currentTime = performance.now()
const dt = (currentTime - lastTime) / 1000
lastTime = currentTime
ctx.fillStyle = 'rgba(15, 15, 35, 0.3)'
ctx.fillRect(0, 0, canvas.width, canvas.height)
particles = particles.filter(p => {
p.x += p.vx * dt
p.y += p.vy * dt
p.life -= dt
if (p.life <= 0) {
particlePool.release(p)
return false
}
const alpha = p.life / p.maxLife
ctx.beginPath()
ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2)
ctx.fillStyle = p.color.replace(')', `, ${alpha})`).replace('hsl', 'hsla')
ctx.fill()
return true
})
animationId = requestAnimationFrame(animate)
}
function log(message, type = '') {
const logEl = document.getElementById('log')
const entry = document.createElement('div')
entry.className = type
entry.textContent = `[${new Date().toLocaleTimeString()}] ${message}`
logEl.appendChild(entry)
logEl.scrollTop = logEl.scrollHeight
}
createParticles()
toggleAnimation()
</script>
</body>
</html>
const memoryOptimizationChecklist = {
resources: [
'及时释放不再使用的Canvas引用',
'清除事件监听器',
'取消动画帧和定时器',
'释放图像资源'
],
caching: [
'实现缓存淘汰策略',
'设置缓存大小限制',
'定期清理过期缓存',
'监控缓存命中率'
],
objects: [
'使用对象池复用对象',
'避免频繁创建临时对象',
'使用TypedArray处理大数据',
'及时释放大对象引用'
],
monitoring: [
'监控内存使用趋势',
'设置内存警告阈值',
'记录内存快照',
'定期检查内存泄漏'
]
}
class CanvasCleanup {
static cleanupCanvas(canvas) {
if (!canvas) return
canvas.width = 0
canvas.height = 0
const ctx = canvas.getContext('2d')
if (ctx) {
ctx.setTransform(1, 0, 0, 1, 0, 0)
ctx.globalAlpha = 1
ctx.globalCompositeOperation = 'source-over'
ctx.clearRect(0, 0, 1, 1)
}
}
static cleanupImage(img) {
if (!img) return
img.src = ''
img.onload = null
img.onerror = null
}
static cleanupObject(obj) {
if (!obj) return
Object.keys(obj).forEach(key => {
delete obj[key]
})
}
static cleanupArray(arr) {
if (!arr) return
arr.length = 0
}
}
Canvas内存管理是确保应用稳定运行的关键:
通过良好的内存管理实践,可以确保Canvas应用长时间稳定运行。