UNPKG

id-scanner-lib

Version:

Browser-based ID card, QR code, and face recognition scanner with liveness detection

274 lines (242 loc) 7.07 kB
/** * @file Canvas 对象池 * @description 提供 Canvas 元素的复用机制,减少内存分配和 GC 压力 * @module utils/canvas-pool */ /** * Canvas 池条目 */ interface CanvasPoolItem { /** Canvas 元素 */ canvas: HTMLCanvasElement; /** Canvas 2D 上下文 */ context: CanvasRenderingContext2D; /** 是否正在使用 */ inUse: boolean; /** 最近使用时间戳 */ lastUsed: number; /** Canvas 尺寸标识 */ sizeKey: string; } /** * Canvas 对象池 * * 复用 Canvas 元素,避免频繁创建和销毁导致的内存抖动 * * @example * ```typescript * const pool = CanvasPool.getInstance(); * const { canvas, context } = pool.acquire(100, 200); * // 使用 canvas 进行绘制... * pool.release(canvas); * ``` */ export class CanvasPool { /** 单例实例 */ private static instance: CanvasPool | null = null; /** Canvas 池存储 */ private pool: Map<string, CanvasPoolItem[]> = new Map(); /** 已借出的 Canvas */ private borrowed: Map<HTMLCanvasElement, CanvasPoolItem> = new Map(); /** 最大池大小(每个尺寸) */ private maxPoolSize: number = 4; /** Canvas 尺寸容差(允许一定范围的尺寸复用) */ private sizeTolerance: number = 10; /** * 获取单例实例 */ public static getInstance(): CanvasPool { if (!CanvasPool.instance) { CanvasPool.instance = new CanvasPool(); } return CanvasPool.instance; } /** * 重置单例实例(主要用于测试) */ public static resetInstance(): void { if (CanvasPool.instance) { CanvasPool.instance.dispose(); CanvasPool.instance = null; } } /** * 私有构造函数 */ private constructor() { // 页面卸载前清理 if (typeof window !== 'undefined') { window.addEventListener('beforeunload', () => this.dispose()); } } /** * 生成尺寸键 * @param width 宽度 * @param height 高度 */ private getSizeKey(width: number, height: number): string { return `${width}x${height}`; } /** * 查找匹配的尺寸键(考虑容差) * @param width 宽度 * @param height 高度 */ private findMatchingSizeKey(width: number, height: number): string | null { for (const [key, items] of this.pool.entries()) { const [w, h] = key.split('x').map(Number); if (Math.abs(w - width) <= this.sizeTolerance && Math.abs(h - height) <= this.sizeTolerance) { // 找到可用的 const available = items.filter(item => !item.inUse); if (available.length > 0) { return key; } } } return null; } /** * 从池中获取 Canvas * * @param width 宽度 * @param height 高度 * @returns Canvas 和其上下文 */ public acquire(width: number, height: number): { canvas: HTMLCanvasElement; context: CanvasRenderingContext2D; } { // 先尝试精确匹配 let sizeKey = this.getSizeKey(width, height); let items = this.pool.get(sizeKey); // 如果没有精确匹配,尝试模糊匹配 if (!items || items.every(item => item.inUse)) { const matchedKey = this.findMatchingSizeKey(width, height); if (matchedKey) { sizeKey = matchedKey; items = this.pool.get(sizeKey); } } // 如果没有可用的,创建一个新的 if (!items || items.every(item => item.inUse)) { const canvas = document.createElement('canvas'); canvas.width = width; canvas.height = height; const context = canvas.getContext('2d')!; const item: CanvasPoolItem = { canvas, context, inUse: true, lastUsed: Date.now(), sizeKey: this.getSizeKey(width, height) }; // 如果池已满,移除最老的 if (!items) { items = []; this.pool.set(sizeKey, items); } else if (items.length >= this.maxPoolSize) { // 找到最老的未使用项并移除 let oldestIdx = 0; let oldestTime = Infinity; items.forEach((item, idx) => { if (!item.inUse && item.lastUsed < oldestTime) { oldestTime = item.lastUsed; oldestIdx = idx; } }); const removed = items.splice(oldestIdx, 1)[0]; this.borrowed.delete(removed.canvas); } items.push(item); this.borrowed.set(canvas, item); return { canvas, context }; } // 找到一个空闲的 const available = items.find(item => !item.inUse)!; available.inUse = true; available.lastUsed = Date.now(); // 如果尺寸变化,更新 canvas if (available.canvas.width !== width || available.canvas.height !== height) { available.canvas.width = width; available.canvas.height = height; available.sizeKey = sizeKey; } // 清除之前的上下文状态 available.context.setTransform(1, 0, 0, 1, 0, 0); available.context.clearRect(0, 0, width, height); this.borrowed.set(available.canvas, available); return { canvas: available.canvas, context: available.context }; } /** * 释放 Canvas 回池中 * * @param canvas 要释放的 Canvas */ public release(canvas: HTMLCanvasElement): void { const item = this.borrowed.get(canvas); if (!item) { // 不属于我们管理的 Canvas,忽略 return; } item.inUse = false; item.lastUsed = Date.now(); this.borrowed.delete(canvas); } /** * 批量释放所有借出的 Canvas */ public releaseAll(): void { for (const [, item] of this.borrowed) { item.inUse = false; item.lastUsed = Date.now(); } this.borrowed.clear(); } /** * 预热池(预创建指定尺寸的 Canvas) * * @param sizes 尺寸数组,每项为 [width, height] */ public warmup(sizes: Array<[number, number]>): void { for (const [width, height] of sizes) { this.acquire(width, height); // 立即释放,让它们进入池中 const sizeKey = this.getSizeKey(width, height); const items = this.pool.get(sizeKey); if (items && items.length > 0) { const item = items[items.length - 1]; item.inUse = false; this.borrowed.delete(item.canvas); } } } /** * 获取池统计信息 */ public getStats(): { totalItems: number; borrowedCount: number; poolSizes: Record<string, { total: number; available: number }>; } { let totalItems = 0; let borrowedCount = 0; const poolSizes: Record<string, { total: number; available: number }> = {}; for (const [key, items] of this.pool.entries()) { totalItems += items.length; borrowedCount += items.filter(i => i.inUse).length; poolSizes[key] = { total: items.length, available: items.filter(i => !i.inUse).length }; } return { totalItems, borrowedCount, poolSizes }; } /** * 清理并释放所有资源 */ public dispose(): void { this.pool.clear(); this.borrowed.clear(); } }