js 闭包导致变量无法被 GC 问题记录

闭包导致变量无法被 GC(垃圾回收),这是早就知道的事情,所以平时也是尽量不用闭包,加上一般业务场景也不会有很高的内存占用和使用闭包的需求,所以也一直没怎么碰到相关问题。

现在写这篇文章是因为这两天碰到了。

场景是这样的,一个函数对图片的 ImageData 进行处理,结束后返回新的 ImageData,并且处理过程可以中断,所以函数运行时会先设置一个停止函数在函数外,用于下次调用的时候先停止之前的任务。这就形成了一个闭包环境。中途产生的 ImageData 就无法被回收。

// 示例代码
export default class FilterManager {
  filters: Filter[] = []
  source?: ImageData
  private taskStopHandler?: () => void

  // ……

  async applyFilters() {
    if (this.taskStopHandler) {
      this.taskStopHandler()
    }

    let stopFlag = false

    // 停止函数
    this.taskStopHandler = () => {
      stopFlag = true
      this.taskStopHandler = undefined
    }

    let result: ImageData | undefined = this.source
    const filters = [...this.filters]
    const cb = callback || this.callback

    // 计算滤镜
    for (let i = index; i < filters.length; i++) {
      if (stopFlag) break
      if (result) {
        result = await filters[i].processor(result, filters[i].params)
      }
    }

    // 回调返回必须有结果
    if (!stopFlag && result) {
      if (typeof cb === 'function') {
        cb(result)
      }
    } else {
      this.status = StatusEnum.NoData
    }
    // 释放闭包
    // this.taskStopHandler?.()
    return result
  }
}

这里的 taskStopHandler 如果不及时调用就会导致 applyFilters 内部变量无法被及时回收。如果是一般业务还无所谓,但这个是图片处理,一个五千万像素图片的 ImageData 就要 200M 内存,而现在很多手机拍照开启完整像素是超过五千万像素的。

上图不是很清晰,但可以从右侧看到这个两百多兆的 ImageData 和 taskStopHandler 造成的闭包有关。当我将上面代码里最后两行注释取消,即函数最后调用一下 taskStopHandler,释放闭包内变量之后,内存占用就如图中左侧二三行所示,少了两百多兆。至于为什么还有三百兆,那是因为原图像数据存在 filterManager 的 source 上。应该……也可以干掉,虽然这样会导致每次处理的时候都需要重新获取 ImageData。