AsyncStorage大存储量计算报错及批量方案失效问题求助
问题描述
在Expo环境中使用Zustand结合AsyncStorage存储大于2MB的数据时,计算存储占用遇到以下问题:
- 初始计算代码触发
Row too big to fit into CursorWindow requiredPos=0, totalRows=1错误 - 改用批量获取加重试的方案后,存储量超过4MB时,计算出的存储占用数值自动变为0(仅希望点击清理缓存时置0)
初始计算代码
import AsyncStorage from "@react-native-async-storage/async-storage"; import { logger, LOG_LEVEL } from "../utils/general/logger"; export const checkAsyncStorageMemoryUsage = async (setAsyncStorageMemoryUsage) => { try { const keys = await AsyncStorage.getAllKeys(); let totalSize = 0; for (let i = 0; i < keys.length; i++) { const key = keys[i]; const value = await AsyncStorage.getItem(key); const valueSize = value ? value.length * 2 : 0; totalSize += key.length * 2 + valueSize; } const totalSizeInMB = (totalSize / (1024 * 1024)).toFixed(2); console.log(`AsyncStorage memory usage: ${totalSizeInMB} MB`); logger(`AsyncStorage memory usage: ${totalSizeInMB} MB`, LOG_LEVEL.INFO); setAsyncStorageMemoryUsage(totalSizeInMB); } catch (error) { logger(`Error checking AsyncStorage memory usage: ${error}`, LOG_LEVEL.ERROR); } };
触发的错误
Row too big to fit into CursorWindow requiredPos=0, totalRows=1
尝试的批量重试方案(存在超4MB数值变0问题)
import AsyncStorage from "@react-native-async-storage/async-storage"; import { logger, LOG_LEVEL } from "../utils/general/logger"; const BATCH_SIZE = 5; const MAX_RETRIES = 5; export const checkAsyncStorageMemoryUsage = async (setAsyncStorageMemoryUsage) => { try { const keys = await AsyncStorage.getAllKeys(); let totalSize = 0; for (let i = 0; i < keys.length; i += BATCH_SIZE) { const batchKeys = keys.slice(i, i + BATCH_SIZE); let batchValues = []; let retryCount = 0; while (retryCount < MAX_RETRIES) { try { batchValues = await Promise.all(batchKeys.map(key => AsyncStorage.getItem(key))); break; } catch (error) { retryCount++; console.warn(`Retrying batch retrieval (${retryCount}/${MAX_RETRIES}) due to error: ${error}`); } } const batchData = batchKeys.map((key, j) => ({ key, value: batchValues[j], })); for (const { key, value } of batchData) { const valueSize = value ? value.length * 2 : 0; totalSize += key.length * 2 + valueSize; } } const totalSizeInMB = (totalSize / (1024 * 1024)).toFixed(2); console.log(`AsyncStorage memory usage: ${totalSizeInMB} MB`); logger(`AsyncStorage memory usage: ${totalSizeInMB} MB`, LOG_LEVEL.INFO); setAsyncStorageMemoryUsage(totalSizeInMB); } catch (error) { logger(`Error checking AsyncStorage memory usage: ${error}`, LOG_LEVEL.ERROR); } };
缓存清理代码
export const clearPersistedStore = async () => { try { await AsyncStorage.removeItem("storage"); resetStore(); } catch (error) { logger("Error removing item:" + error, LOG_LEVEL.ERROR); } };
解决方案建议
方案1:单个键值逐个获取(降低CursorWindow负载)
放弃批量Promise.all,改为逐个获取每个键值,避免单个批次包含大体积数据导致CursorWindow溢出,同时捕获单个键的读取错误,跳过异常键继续计算:
import AsyncStorage from "@react-native-async-storage/async-storage"; import { logger, LOG_LEVEL } from "../utils/general/logger"; export const checkAsyncStorageMemoryUsage = async (setAsyncStorageMemoryUsage) => { try { const keys = await AsyncStorage.getAllKeys(); let totalSize = 0; for (const key of keys) { try { const value = await AsyncStorage.getItem(key); const valueSize = value ? value.length * 2 : 0; totalSize += key.length * 2 + valueSize; } catch (keyError) { console.warn(`Failed to read key ${key}: ${keyError}`); logger(`Failed to read key ${key}: ${keyError}`, LOG_LEVEL.WARN); } } const totalSizeInMB = (totalSize / (1024 * 1024)).toFixed(2); console.log(`AsyncStorage memory usage: ${totalSizeInMB} MB`); logger(`AsyncStorage memory usage: ${totalSizeInMB} MB`, LOG_LEVEL.INFO); setAsyncStorageMemoryUsage(totalSizeInMB); } catch (error) { logger(`Error checking AsyncStorage memory usage: ${error}`, LOG_LEVEL.ERROR); } };
方案2:提前维护元数据记录存储大小
在每次写入AsyncStorage时,同步更新专门的元数据键(比如storage-meta),记录每个键的大小,计算总占用时无需读取所有值,直接读取元数据求和:
// 写入数据时同步更新元数据 export const saveDataToStorage = async (key, value) => { try { await AsyncStorage.setItem(key, value); const keySize = key.length * 2; const valueSize = value.length * 2; const totalKeySize = keySize + valueSize; const metaStr = await AsyncStorage.getItem("storage-meta"); const meta = metaStr ? JSON.parse(metaStr) : {}; meta[key] = totalKeySize; await AsyncStorage.setItem("storage-meta", JSON.stringify(meta)); } catch (error) { logger(`Error saving data: ${error}`, LOG_LEVEL.ERROR); } }; // 计算存储占用时直接读取元数据 export const checkAsyncStorageMemoryUsage = async (setAsyncStorageMemoryUsage) => { try { const metaStr = await AsyncStorage.getItem("storage-meta"); const meta = metaStr ? JSON.parse(metaStr) : {}; let totalSize = Object.values(meta).reduce((sum, size) => sum + size, 0); // 加上元数据自身的大小 const metaSize = metaStr ? metaStr.length * 2 + "storage-meta".length * 2 : 0; totalSize += metaSize; const totalSizeInMB = (totalSize / (1024 * 1024)).toFixed(2); console.log(`AsyncStorage memory usage: ${totalSizeInMB} MB`); logger(`AsyncStorage memory usage: ${totalSizeInMB} MB`, LOG_LEVEL.INFO); setAsyncStorageMemoryUsage(totalSizeInMB); } catch (error) { logger(`Error checking AsyncStorage memory usage: ${error}`, LOG_LEVEL.ERROR); } }; // 清理缓存时同时删除元数据 export const clearPersistedStore = async () => { try { await AsyncStorage.removeItem("storage"); await AsyncStorage.removeItem("storage-meta"); resetStore(); } catch (error) { logger("Error removing item:" + error, LOG_LEVEL.ERROR); } };
该方案从根源避免读取大体积数据的问题,性能最优,但需确保所有写入操作同步更新元数据。
方案3:用Expo FileSystem替代大体积数据存储
AsyncStorage设计用于小数据存储,大于2MB的数据建议改用Expo FileSystem存储文件,仅用AsyncStorage记录文件路径和大小:
import * as FileSystem from 'expo-file-system'; import AsyncStorage from "@react-native-async-storage/async-storage"; import { logger, LOG_LEVEL } from "../utils/general/logger"; // 保存大数据到文件系统 export const saveLargeData = async (key, data) => { try { const filePath = `${FileSystem.documentDirectory}${key}.dat`; await FileSystem.writeAsStringAsync(filePath, data); const fileInfo = await FileSystem.getInfoAsync(filePath); await AsyncStorage.setItem(`file-meta-${key}`, JSON.stringify({ path: filePath, size: fileInfo.size })); } catch (error) { logger(`Error saving large data: ${error}`, LOG_LEVEL.ERROR); } }; // 计算总存储占用:AsyncStorage数据 + 文件系统数据 export const checkTotalStorageUsage = async (setStorageMemoryUsage) => { try { const keys = await AsyncStorage.getAllKeys(); let asyncStorageSize = 0; let fileTotalSize = 0; for (const key of keys) { if (key.startsWith("file-meta-")) { const metaStr = await AsyncStorage.getItem(key); const meta = JSON.parse(metaStr); fileTotalSize += meta.size; asyncStorageSize += key.length * 2 + (metaStr ? metaStr.length * 2 : 0); } else { try { const value = await AsyncStorage.getItem(key); asyncStorageSize += key.length * 2 + (value ? value.length * 2 : 0); } catch (keyError) { console.warn(`Failed to read key ${key}: ${keyError}`); } } } const totalSize = asyncStorageSize + fileTotalSize; const totalSizeInMB = (totalSize / (1024 * 1024)).toFixed(2); console.log(`Total storage usage: ${totalSizeInMB} MB`); logger(`Total storage usage: ${totalSizeInMB} MB`, LOG_LEVEL.INFO); setStorageMemoryUsage(totalSizeInMB); } catch (error) { logger(`Error checking storage usage: ${error}`, LOG_LEVEL.ERROR); } }; // 清理缓存时同时删除文件和元数据 export const clearPersistedStore = async () => { try { const keys = await AsyncStorage.getAllKeys(); for (const key of keys) { if (key.startsWith("file-meta-")) { const metaStr = await AsyncStorage.getItem(key); const meta = JSON.parse(metaStr); await FileSystem.deleteAsync(meta.path); await AsyncStorage.removeItem(key); } } await AsyncStorage.removeItem("storage"); resetStore(); } catch (error) { logger("Error removing item:" + error, LOG_LEVEL.ERROR); } };
该方案符合React Native/Expo最佳实践,大文件存储更稳定,避免AsyncStorage的大小限制和CursorWindow问题。
内容的提问来源于stack exchange,提问作者Keerthana Vadivel
相关产品推荐
相关产品推荐

