Cordova iOS应用:实现无崩溃的IndexedDB数据库占用空间计算方案
我开发了一款基于Cordova的iOS应用,使用IndexedDB在单个数据库的不同对象存储空间(object store)中存储大量数据。为了告知用户该应用的存储空间占用情况,同时由于不同设备上IndexedDB的存储上限并不明确,我希望通过计算数据库占用空间,在应用出现故障时定位存储使用情况,并在问题发生前提醒用户管理离线存储的数据(尽管我可以捕获事务中止事件,但完全不清楚具体的存储上限)。
在开发阶段,我通过添加browser平台,使用以下函数在浏览器中进行数据库大小计算,效果良好:
function showIndexedDbSize(db_name) { "use strict"; var this_db; var storesizes = new Array(); function openDatabase() { return new Promise(function(resolve, reject) { var request = window.indexedDB.open(db_name); request.onsuccess = function (event) { this_db = event.target.result; resolve(this_db.objectStoreNames); }; }); } function getObjectStoreData(storename) { return new Promise(function(resolve, reject) { var trans = this_db.transaction(storename, IDBTransaction.READ_ONLY); var store = trans.objectStore(storename); var items = []; trans.oncomplete = function(evt) { var szBytes = toSize(items); var szMBytes = (szBytes / 1024 / 1024).toFixed(2); storesizes.push({'Store Name': storename, 'Items': items.length, 'Size': szMBytes + 'MB (' + szBytes + ' bytes)'}); resolve(); }; var cursorRequest = store.openCursor(); cursorRequest.onerror = function(error) { reject(error); }; cursorRequest.onsuccess = function(evt) { var cursor = evt.target.result; if (cursor) { items.push(cursor.value); cursor.continue(); } } }); } function toSize(items) { var size = 0; for (var i = 0; i < items.length; i++) { var objectSize = JSON.stringify(items[i]).length; size += objectSize * 2; } return size; } openDatabase().then(function(stores) { var PromiseArray = []; for (var i=0; i < stores.length; i++) { PromiseArray.push(getObjectStoreData(stores[i])); } Promise.all(PromiseArray).then(function() { this_db.close(); console.table(storesizes); }); }); };
当数据库总占用空间在150MB左右及以下时,该函数在设备上也能正常工作,但当数据库增大后,由于函数使用JSON.stringify序列化对象来计算字节数,会导致应用强制重启。我通过XCode监控内存使用情况,发现内存始终维持在25-30MB之间,没有峰值;CPU使用率也低于5%;仅能耗较高,但我认为这只会加快电池消耗,不会导致应用异常。因此我不清楚应用重启的原因。
通过搜索了解到,JSON.parse和JSON.stringify是资源密集型操作,这也是我最初选择IndexedDB存储对象以避免这类操作的原因。现提出以下问题:
- 是否可以修改该函数,降低其执行速度(无需快速执行,仅需可靠)以避免应用重启?
- 为何在XCode未检测到明显内存压力的情况下,应用仍会重启?
- 当前这种计算数据库占用空间的方式是否合适?
- 该函数是否存在隐藏的垃圾回收(GC)问题?(我对GC了解不多,且未发现应用存在内存泄漏)
- 是否有更好的方法来计算IndexedDB数据库的占用空间,以避免当前问题?
我发现现有方案大多依赖JSON相关操作,而navigator.storage Web API在Cordova iOS平台上并不支持(浏览器上表现极佳,十分遗憾),恳请提供相关建议和思路。
1. 能否修改函数降低执行速度避免重启?
完全可以,核心思路是拆分任务,避免主线程长时间阻塞。你的当前实现会一次性读取整个object store的所有数据到内存,然后批量执行JSON.stringify,这会让主线程长时间无法响应系统事件(比如触摸、系统回调),触发iOS的Watchdog机制。
修改方案:
- 不要把所有对象存在
items数组里,而是逐个处理cursor,计算完当前对象大小后立即丢弃,减少内存积累。 - 用
requestIdleCallback或者setTimeout拆分计算任务,让主线程在处理存储计算的间隙有时间响应其他事件。比如每处理100条数据就暂停一下,给系统留时间处理优先级更高的任务。
修改后的核心逻辑示例:
function getObjectStoreData(storename) { return new Promise(function(resolve, reject) { var trans = this_db.transaction(storename, IDBTransaction.READ_ONLY); var store = trans.objectStore(storename); var totalSize = 0; var itemCount = 0; function processCursor(cursor) { if (!cursor) { // 处理完成 var szMBytes = (totalSize / 1024 / 1024).toFixed(2); storesizes.push({'Store Name': storename, 'Items': itemCount, 'Size': szMBytes + 'MB (' + totalSize + ' bytes)'}); resolve(); return; } // 计算当前对象大小,不存储整个对象 var objectSize = JSON.stringify(cursor.value).length * 2; totalSize += objectSize; itemCount++; // 用requestIdleCallback调度下一次处理,避免阻塞主线程 requestIdleCallback(function() { cursor.continue(); }); } var cursorRequest = store.openCursor(); cursorRequest.onerror = reject; cursorRequest.onsuccess = function(evt) { processCursor(evt.target.result); } }); }
2. 为何XCode没检测到内存压力但应用重启?
这几乎可以肯定是iOS Watchdog进程导致的。Watchdog的作用是监控应用的响应性:如果主线程被阻塞超过一定时间(通常是10秒左右),系统会判定应用无响应,强制重启它。
XCode的内存监控只追踪内存占用情况,但不会监控主线程的阻塞状态。你的代码虽然内存占用不高,但长时间占用主线程执行JSON.stringify和数据遍历,导致系统无法和应用交互,最终触发Watchdog杀进程。能耗高其实也是主线程持续忙碌的信号,系统会把这类持续占用资源的应用标记为异常。
3. 当前计算方式是否合适?
不合适,主要有两个问题:
- 准确性差:IndexedDB使用结构化克隆算法存储对象,和JSON.stringify的序列化逻辑不同(比如支持Date、Blob、Map等JSON不兼容的类型),你计算的大小只是近似值,和实际磁盘占用有偏差。
- 资源消耗大:一次性读取所有数据到内存,哪怕内存没爆,也会给GC带来巨大压力,同时阻塞主线程,触发Watchdog。
4. 是否存在隐藏的GC问题?
是的。虽然你没发现内存泄漏,但函数里的items数组会积累大量对象,直到事务完成后才会被GC回收。数据量越大,这个数组占用的内存越多,GC需要频繁触发来清理这些临时对象——而GC操作是在主线程执行的,会进一步加剧主线程阻塞,间接触发Watchdog。
另外,JSON.stringify会创建大量临时字符串,这些字符串也会增加GC的工作量,导致主线程卡顿。
5. 更好的计算方法?
推荐两种更可靠的方案:
方案一:直接读取IndexedDB的存储文件(Cordova iOS专属)
iOS上Cordova应用的IndexedDB数据存储在Library/Application Support/<你的App Bundle ID>/IndexedDB目录下的文件中。你可以使用cordova-plugin-file插件遍历这个目录,统计所有文件的大小总和,这是最准确的方式,完全不需要加载数据到内存。
核心步骤:
- 安装文件插件:
cordova plugin add cordova-plugin-file - 获取应用的
Library目录路径:window.resolveLocalFileSystemURL(cordova.file.applicationLibraryDirectory, ...) - 遍历
Application Support/<Bundle ID>/IndexedDB目录下的所有文件,累加每个文件的size属性。
方案二:优化前端计算逻辑(跨平台)
如果无法访问本地文件,可以大幅优化现有逻辑:
- 逐个处理cursor,不存储对象:如问题1的修改示例,计算完当前对象大小后立即丢弃,不积累到数组中。
- 替换JSON.stringify为更高效的大小计算:比如用
Blob来计算序列化后的大小(虽然还是基于JSON,但比直接字符串长度更准确):function getObjectSize(obj) { const blob = new Blob([JSON.stringify(obj)], {type: 'application/json'}); return blob.size * 2; // 乘以2近似UTF-16的字节数 } - 使用Web Workers:如果Cordova iOS支持,可以把计算逻辑放到Web Worker中,避免占用主线程。不过要注意Web Worker无法直接访问IndexedDB,需要主线程传递数据,所以还是需要拆分任务。
内容的提问来源于stack exchange,提问作者Helen Danger Burns




