内存管理

深入学习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: '实现缓存淘汰策略'
  }
}

资源管理器

Canvas资源管理

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()

图像缓存管理

LRU缓存

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内存管理是确保应用稳定运行的关键:

  1. 资源管理:正确释放Canvas、图像等资源
  2. 对象池:复用对象减少GC压力
  3. 缓存策略:实现LRU等淘汰策略
  4. 内存监控:实时监控内存使用情况
  5. 泄漏检测:定期检查和修复内存泄漏

通过良好的内存管理实践,可以确保Canvas应用长时间稳定运行。