You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

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存储对象以避免这类操作的原因。现提出以下问题:

  1. 是否可以修改该函数,降低其执行速度(无需快速执行,仅需可靠)以避免应用重启?
  2. 为何在XCode未检测到明显内存压力的情况下,应用仍会重启?
  3. 当前这种计算数据库占用空间的方式是否合适?
  4. 该函数是否存在隐藏的垃圾回收(GC)问题?(我对GC了解不多,且未发现应用存在内存泄漏)
  5. 是否有更好的方法来计算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插件遍历这个目录,统计所有文件的大小总和,这是最准确的方式,完全不需要加载数据到内存。

核心步骤:

  1. 安装文件插件:cordova plugin add cordova-plugin-file
  2. 获取应用的Library目录路径:window.resolveLocalFileSystemURL(cordova.file.applicationLibraryDirectory, ...)
  3. 遍历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

火山引擎 最新活动