【问题标题】:How to catch memory leaks in an Angular application?如何在 Angular 应用程序中捕获内存泄漏?
【发布时间】:2014-01-06 10:14:56
【问题描述】:

我有一个用 AngularJS 编写的 webapp,它基本上轮询一个 API 到两个端点。因此,它每分钟都会轮询以查看是否有任何新内容。

我发现它有一个小的内存泄漏,我已尽力找到它,但我无法做到。在此过程中,我设法减少了我的应用程序的内存使用量,这很棒。

不做任何其他事情,每次民意调查您都会看到内存使用量出现峰值(这是正常的),然后它应该会下降,但它总是在增加。我已经将数组的清理从[] 更改为array.length = 0,我认为我确信引用不会持续存在,所以它不应该保留任何这些。

我也试过这个:https://github.com/angular/angular.js/issues/1522

但没有任何运气......

所以,这是两个堆之间的比较:

大部分泄漏似乎来自 (array),如果我打开,它是 API 调用解析返回的数组,但我确定它们没有被存储:

基本上是这样的结构:

poll: function(service) {
  var self = this;
  log('Polling for %s', service);

  this[service].get().then(function(response) {
    if (!response) {
      return;
    }

    var interval = response.headers ? (parseInt(response.headers('X-Poll-Interval'), 10) || 60) : 60;

    services[service].timeout = setTimeout(function(){
      $rootScope.$apply(function(){
        self.poll(service);
      });
    }, interval * 1000);

    services[service].lastRead = new Date();
    $rootScope.$broadcast('api.'+service, response.data);
  });
}

基本上,假设我有一个sellings 服务,那么这就是service 变量的值。

然后,在主视图中:

$scope.$on('api.sellings', function(event, data) {
  $scope.sellings.length = 0;
  $scope.sellings = data;
});

视图确实有一个ngRepeat,它会根据需要呈现它。我花了很多时间试图自己解决这个问题,但我做不到。我知道这是一个难题,但有人知道如何追踪这个问题吗?

编辑 1 - 添加 Promise 展示:

这是makeRequest,这是两个服务使用的函数:

return $http(options).then(function(response) {
    if (response.data.message) {
      log('api.error', response.data);
    }

    if (response.data.message == 'Server Error') {    
      return $q.reject();
    }

    if (response.data.message == 'Bad credentials' || response.data.message == 'Maximum number of login attempts exceeded') {
      $rootScope.$broadcast('api.unauthorized');
      return $q.reject();
    }

    return response;
    }, function(response) {
    if (response.status == 401 || response.status == 403) {
      $rootScope.$broadcast('api.unauthorized');
    }
});

如果我注释掉$scope.$on('api.sellings')部分,泄漏仍然存在,但下降到1%。

PS:我目前使用的是最新的 Angular 版本

编辑 2 - 在图像中打开(数组)树

都是这样,所以它是非常没用的恕我直言:(

另外,这里有 4 个堆报告,你可以自己玩:

https://www.dropbox.com/s/ys3fxyewgdanw5c/Heap.zip

编辑 3 - 响应@zeroflagL

编辑指令,对泄漏没有任何影响,尽管闭包部分似乎更好,因为它没有显示 jQuery 缓存的东西?

指令现在看起来像这样

var destroy = function(){
  if (cls){
    stopObserving();
    cls.destroy();
    cls = null;
  }
};

el.on('$destroy', destroy);
scope.$on('$destroy', destroy);

在我看来,(array) 部分似乎发生了什么事。在轮询之间还有3 new heaps

【问题讨论】:

  • 我的赌注是 Promise 挂在你的 then() 回调上。你已经追踪到 Promise 了吗?
  • 谢谢@EzekielVictor - 我已经更新了我的答案。我也是这么想的,但老实说,在那之后我认为它不存在了!
  • 你能展示其中一个应该被 GC 处理的数组的保留树吗?
  • 完成@PieterHerroelen 我还添加了堆报告
  • @AntonioLaguna 无论如何你可以在 plunker/fiddle 中重现这个?

标签: angularjs memory-leaks angularjs-ng-repeat


【解决方案1】:

答案是缓存。

我不知道它是什么,但这东西会长大。它似乎与jQuery有关。也许是 jQuery 元素缓存。每次服务调用后,您是否有机会在一个或多个元素上应用 jQuery 插件?

更新

问题是 HTML 元素被添加,用 jQuery 处理(例如,通过 popbox 插件),但要么根本没有删除,要么没有用 jQuery 删除。在这种情况下进行处理意味着添加事件处理程序之类的东西。只有当 jQuery 知道元素已被删除时,缓存对象中的条目(无论它是什么)才会被删除。那就是必须用 jQuery 删除元素。

更新 2

目前还不清楚为什么缓存中的这些条目没有被删除,因为 Angular 在包含时应该使用 jQuery。但是它们是通过 cmets 中提到的插件添加的,并且包含事件处理程序和数据。 AFAIK Antonio 已更改插件代码以取消绑定事件处理程序并删除插件 destroy() 方法中的数据。这最终消除了内存泄漏。

【讨论】:

  • 是的,我愿意,有一个使用弹出窗口的指令。但是,该指令确实有这个功能:scope.$on('$destroy', function(){ stopObserving(); plugin.destroy(); }); 你可以在这里看到它:github.com/firstandthird/angular-popbox/blob/master/dist/…
  • @AntonioLaguna var $el = $(el); - el 已经应该是一个 jQuery 对象。也许这就是原因。尝试直接使用el
  • 看来不是。如果我删除它,插件会抱怨说 [Object object] 没有方法 popbox
  • @AntonioLaguna 是的,它只是一个 jqLit​​e 对象,恕我直言,这真的很可悲。我想我找到了根本原因。我更新了我的答案。
  • 如果你是对的,我该如何解决?我在 $destroy 上遇到了一些错误,因为该元素已经无效(我怀疑这发生在 ngRepeat 上)所有这些都由 ngRepeat 处理,应该是干净的吗?
【解决方案2】:

修复内存泄漏的标准浏览器方法是刷新页面。而且 JavaScript 垃圾收集有点懒惰,很可能依赖于此。而且由于 Angular 通常是一个 SPA,浏览器永远没有机会刷新。

但我们有一个优势:Javascript 主要是一种自上而下的分层语言。与其自下而上查找内存泄漏,我们或许可以自上而下清除它们。

因此,我想出了这个解决方案,它有效,但可能会或可能不会 100% 有效,具体取决于您的应用。

主页

典型的 Angular 应用主页由一些 Controller 和 ng-view 组成。像这样:

<div ng-controller="MainController as vm"> <div id="main-content-app" ng-view></div> </div>

控制者

然后为了“刷新”控制器中的应用程序,这将是上面代码中的 MainController,我们冗余地调用 jQuery 的 .empty() 和 Angular 的 .empty() 以确保清除任何跨库引用。

function refreshApp() {
var host = document.getElementById('main-content-app');
if(host) {
    var mainDiv = $("#main-content-app");
    mainDiv.empty();
    angular.element(host).empty();
}
}

并在路由开始之前调用上述方法,模拟页面刷新:

$rootScope.$on('$routeChangeStart',
function (event, next, current) {
    refreshApp();
}
);

结果

这是一种“刷新浏览器类型行为”、清除 DOM 并希望有任何泄漏的 hacky 方法。希望对您有所帮助。

【讨论】:

  • 虽然我很欣赏在这个答案和你编写的代码中付出的努力,但我认为这(正如你所说)有点老套。您说得对,浏览器必须消除泄漏的方法是刷新页面。但是,作为开发人员,我们必须创建不会泄漏的代码。现在使用 SPA 比以往任何时候都更重要,因为页面可能不会再次刷新,并且应用程序应该可以正常工作而不会对浏览器造成严重破坏。
  • 我完全同意,但我几乎可以向您保证,随着应用程序变得越来越大,要求越来越高,这将成为缓存构建等问题。问题并不总是在代码中,但在浏览器的垃圾收集中! 我工作的公司最近两周一直在测试一个大型 Angular 应用程序,该应用程序从浏览器中大约 300mb 的数据开始(作为测试)。 IE 11 的垃圾收集目前处理得最好。
猜你喜欢
  • 2014-12-22
  • 2014-04-19
  • 1970-01-01
  • 2020-01-18
  • 2016-03-28
  • 2010-11-27
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多