id-scanner-lib
Version:
Browser-based ID card, QR code, and face recognition scanner with liveness detection
274 lines (242 loc) • 7.07 kB
text/typescript
/**
* @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();
}
}