Что такое утечка памяти? Утечка памяти относится к новой части памяти, которая не может быть освобождена или удалена сборщиком мусора. После того, как объект новый, он претендует на кусок памяти кучи.Когда указатель объекта установлен в ноль или покидает область видимости, он будет уничтожен, затем этот кусок памяти будет автоматически собран мусором в JS, если на него никто не ссылается . Однако, если указатель объекта не имеет значение null и нет возможности получить указатель объекта в коде, память, на которую он указывает, не может быть освобождена, что означает утечку памяти. Почему я не могу получить этот указатель объекта в коде?Например:
// module date.js
let date = null;
export default {
init () {
date = new Date();
}
}
// main.js
import date from 'date.js';
date.init();
После инициализации даты в main.js переменная date будет существовать какое-то время, пока вы не закроете страницу, потому что ссылка на дату находится в другом модуле, который можно понимать как модуль, являющийся замыканием и невидимый для внешний мир. Итак, если вы хотите, чтобы объект даты существовал все время и его нужно было использовать все время, тогда нет проблем, но если вы хотите использовать его один раз, а затем не использовать, возникнет проблема. объект не был освобожден в памяти, и происходит утечка памяти.
Еще одна незаметная и распространенная утечка памяти — это связывание событий, которое формирует замыкание и приводит к сохранению некоторых переменных. Как показано в следующем примере:
// 一个图片懒惰加载引擎示例
class ImageLazyLoader {
constructor ($photoList) {
$(window).on('scroll', () => {
this.showImage($photoList);
});
}
showImage ($photoList) {
$photoList.each(img => {
// 通过位置判断图片滑出来了就加载
img.src = $(img).attr('data-src');
});
}
}
// 点击分页的时候就初始化一个图片懒惰加载的
$('.page').on('click', function () {
new ImageLazyLoader($('img.photo'));
});
Это модель ленивой загрузки изображений: каждый раз, когда вы нажимаете на пейджинг, данные предыдущей страницы будут очищаться, а DOM текущей страницы будет обновляться, а механизм ленивой загрузки будет повторно инициализироваться. Он прослушивает событие прокрутки и обрабатывает DOM входящего списка изображений. Каждый раз, когда щелкается страница, создается новая, и здесь происходит утечка памяти, в основном вызванная следующими 3 строками кода:
$(window).on('scroll', () => {
this.showImage($photoList);
});
Поскольку привязка события здесь формирует замыкание, две переменные this/$photoList не освобождаются, это указывает на экземпляр ImageLazyLoader, а $photoList указывает на узел DOM, когда данные предыдущей страницы очищаются. время соответствующие узлы DOM были отделены от дерева DOM, но все еще есть $photoList, указывающий на них, так что эти узлы DOM не могут быть удалены сборщиком мусора и сохранены в памяти, и происходит утечка памяти. Поскольку переменная this также перехвачена замыканием и не была освобождена, также имеет место утечка памяти экземпляра ImageLazyLoader.
Решение относительно простое, пришло время уничтожить экземпляр привязки события, как показано в следующем коде:
class ImageLazyLoader {
constructor ($photoList) {
this.scrollShow = () => {
this.showImage($photoList);
};
$(window).on('scroll', this.scrollShow);
}
// 新增一个事件解绑
clear () {
$(window).off('scroll', this.scrollShow);
}
showImage ($photoList) {
$photoList.each(img => {
// 通过位置判断图片滑出来了就加载
img.src = $(img).attr('data-src');
});
// 判断如果图片已全部显示,就把事件解绑了
if (this.allShown) {
this.clear();
}
}
}
// 点击分页的时候就初始化一个图片懒惰加载的
let lazyLoader = null;
$('.page').on('click', function () {
lazyLoader && (lazyLoader.clear());
lazyLoader = new ImageLazyLoader($('img.photo'));
});
Прежде чем создавать экземпляр ImageLazyLoader каждый раз, очищайте предыдущий экземпляр и отвязывайте его в очистке.Поскольку в JS есть конструктор, но нет деструктора, вам нужно написать очистку самостоятельно и вручную настроить очистку снаружи. При этом событие автоматически отвязывается в нужный момент во время выполнения события.Вышесказанное позволяет судить о том, что если отображаются все картинки, то нет необходимости прослушивать событие прокрутки и отвязывать его напрямую. Это решает проблему утечек памяти и запускает автоматическую сборку мусора.
Почему нет ссылки на закрытие, когда событие не привязано? Поскольку движок JS обнаруживает, что замыкание бесполезно, он уничтожает замыкание, и внешние переменные, на которые ссылается замыкание, естественно, будут пустыми.
Что ж, основы объясняются здесь Теперь используйте инструмент обнаружения памяти Chrome devtools, чтобы снова запустить его, чтобы найти некоторые утечки памяти на странице. Чтобы избежать влияния некоторых подключаемых модулей, установленных в браузере, используйте страницу режима инкогнито Chome, которая отключит все подключаемые модули.
Затем откройте devtools, перейдите на вкладку «Память» и выберите моментальный снимок кучи, как показано ниже:
Что такое моментальный снимок кучи? Перевод представляет собой моментальный снимок кучи, который делает снимок текущей кучи памяти. Поскольку динамически применяемая память находится в куче, а локальные переменные — в стеке памяти, они выделяются и управляются операционной системой, поэтому утечек памяти не будет. Так что хорошо позаботиться о ситуации с кучей.
Затем выполните некоторые операции по добавлению, удалению и изменению DOM, например:
(1) Откройте окно, а затем закройте всплывающее окно.
(2) Нажмите на одну страницу, чтобы перейти к другому маршруту, а затем нажмите назад, чтобы вернуться
(3) Нажмите на пейджинг, чтобы вызвать динамическое изменение DOM.
Сначала нужно добавить DOM, затем удалить эти DOM и посмотреть, есть ли в этих удаленных DOM объекты, ссылающиеся на них.
Вот сценарий второго метода, определяющий, есть ли утечка памяти на странице маршрутизации одностраничного приложения. Сначала откройте домашнюю страницу, перейдите на другую страницу, затем нажмите «Назад», а затем нажмите кнопку «Сборка мусора»:
Запустите сборку мусора, чтобы избежать ненужных помех.
Затем снова нажмите кнопку фото:
Он сканирует кучу памяти текущей страницы и отображает ее снова, как показано на следующем рисунке:
Затем найдите detached в поле поиска Class Filter посередине выше:
Он отобразит все узлы DOM, которые разделили дерево DOM, сосредоточив внимание на значении расстояния, которое не является пустым, это расстояние представляет собой расстояние от корневого узла DOM. Что именно представляют собой эти элементы div, показанные на картинке выше? Мы наводим на него мышь и ждем 2 с, он отобразит информацию DOM этого div:
Благодаря className и другой информации вы можете узнать, что это DOM-узел проверяемой страницы.В следующем окне объекта разверните его родительский узел по очереди, и вы увидите, что его самый внешний родительский узел является экземпляром VueComponent:
Желтый шрифт native_bind ниже указывает на то, что на него указывает событие, а желтый цвет указывает на то, что ссылка все еще действует.Наведите мышь на native_bind и задержитесь на 2 секунды:
Вам будет предложено указать функцию getScale, привязанную к окну в файле homework-web.vue.Проверьте, что этот файл имеет привязку:
mounted () {
window.addEventListener('resize', this.getScale);
}
Поэтому, хотя Vue Component удаляет DOM, все еще есть ссылка, которая приводит к тому, что экземпляр компонента не будет выпущен, а еще один $ EL в компонентных точках до DOM, поэтому DOM не выпускается.
Но, глядя на код, он не связан в beforeDestroyed:
beforeDestroyed () {
window.removeEventListener('resize', this.getScale);
}
Так что проблем быть не должно?
Присмотревшись повнимательнее, я был ошарашен, оказалось, что имя функции неверное, оно должно быть:
beforeDestroy () {
window.removeEventListener('resize', this.getScale);
},
Я нашел ошибку, которую скрывали много дней, потому что это относительно скрыто, даже если это неправильно, явного восприятия не будет.
Измените это место, повторите операцию и сделайте еще один снимок памяти. Мы обнаружили, что количество свободных узлов div по-прежнему равно 74, а расстояние не пусто, улучшений нет, как показано на следующем рисунке:
Просто изменилось? Продолжайте просматривать второй узел прямо сейчас:
Можно обнаружить, что на этот разПривязка события шины событий EventBus указывает на него, что указывает на то, что в дополнение к привязке события изменения размера только что было еще одно событие EventBus, которое не было выпущено, и имя события — gToNextHomworkTask. Давайте поищем, где это событие связано, и мы можем найти, что оно связано в подкомпоненте компонента маршрутизации:
mounted () {
EventBus.$on('goToNextHomeworkTask', this.go2NextQuestion);
}
Разумеется, у этого компонента есть только $on и нет $off, так что ссылка на событие, когда компонент будет удален, все еще есть. Итак, вам нужно указать $off при уничтожении этого компонента:
destroyed () {
EventBus.$off('goToNextHomeworkTask', this.go2NextQuestion);
}
После изменения обновите страницу в третий раз и сделайте еще один снимок памяти.Смущает то, что ситуация остается прежней:
Укажите, что кто-то все еще цитирует его, и продолжайте видеть, кто процитировал его и не опубликовал:
может оказатьсяСлушатель часов $store Vuex не выпущен, с помощью атрибута cb Watcher вы можете узнать, какая функция слушателя. Используйте простой текстовый поиск, чтобы найти, что часы находятся в дочернем компоненте:
mounted () {
this.$store.watch(state => state.currentIndex, (newIndex, oldIndex) => {
if (this.$refs.animation && newIndex === this.task.index - 1) {
this.$refs.animation.beginElement();
}
});
}
В часах есть указатель this, который указывает на элемент DOM компонента.Поскольку дочерний компонент не освобождается, родительский компонент, содержащий его, не будет освобожден, поэтому слой за слоем самый внешний компонент маршрутизации не будет освобожден. . . .
Это нужно не смотреть при уничтожении:
mounted () {
this.unwatchStore = this.$store.watch(state => state.currentIndex, (newIndex, oldIndex) => {
// 代码略
});
},
destroyed () {
this.unwatchStore();
}
После обработки сделайте снимок памяти, как показано на следующем рисунке:
Хотя дистанций еще 74, но дистанция уже пустая, по сравнению с первыми тремя шагами, дистанция не пустая, и желтая часть не найдена в расширении Объекта ниже, а это значит, что проблема утечки памяти этой маршрутизации Компонент решен.
Мы продолжаем рассматривать другие узлы div, расстояние которых не пустое, как показано на следующем рисунке, которое можно отсортировать по расстоянию:
Один из них — .animate-container:
Это DOM-контейнер для анимации лотереи, и на него все еще ссылаются в объекте лотереи:
Это анимация загрузки, сделанная с помощью лотти. Когда загрузка закончится, я вручную настрою его стоп-апи, чтобы остановить анимацию и удалить .animte-контейнер, но почему лотти все еще не хочет его отпускать? Мой код написан так:
let loadingAnimate = null;
let bodymovinAnimate = {
// 显示loading动画
showLoading () {
loadingAnimate = bodymovinAnimate._showAnimate();
return loadingAnimate;
},
// 停止loading动画
stopLoading () {
loadingAnimate && bodymovinAnimate._stopAnimate(loadingAnimate);
},
// 开始lottie动画
_showAnimate () {
const animate = lottie.loadAnimation({
// 参数省略
});
return animate;
}
// 结束lottie动画
_stopAnimate (animate) {
animate.stop();
let $container = $(animate.wrapper).closest('.bodymovin-container');
$container.remove();
},
};
export default bodymovinAnimate;
Я предполагаю, что lottie все равно не выпускает ссылку на DOM после настройки стопа, потому что он может поддерживать перезапуск после стопа, поэтому он должен кусать DOM, поэтому, если вы хотите полностью закончить анимацию, его не следует вызывать stop, проверено. У него также есть метод destroy, замените stop на destroy:
// 结束lottie动画
_stopAnimate (animate) {
animate.destroy();
let $container = $(animate.wrapper).closest('.bodymovin-container');
$container.remove();
},
После этого изменения референс лотти выпустит его, проблема будет решена, а затем снова сделает фото:
На него все еще указывает exports.default, который является модулем веб-пакета.Я думаю, это из-за примера, упомянутого в начале этой статьи, модуль формирует замыкание, и его переменные не освобождаются, вызывая память утечки, поэтому в stopLoading для параметра put установлено значение null:
// 停止loading动画
stopLoading () {
loadingAnimate && bodymovinAnimate._stopAnimate(loadingAnimate);
loadingAnimate = null;
},
После этой попытки объект DOM .animate-container больше не упоминается.
Наконец, осталось 3 дива с расстоянием:
Два из них — это jq $.support.boxSizingReliable, который представляет собой div, созданный jq для определения доступности boxszing:
Существует также один для Vue:
Это все утечки памяти, вызванные используемыми библиотеками, так что давайте пока их проигнорируем.
Переход к анализу других тегов также имеет аналогичную ситуацию.
Поэтому, исходя из приведенного выше анализа, следующие ситуации могут вызвать утечку памяти:
(1) Мониторинг таких событий, как окно/тело, без отвязки
(2) События, привязанные к EventBus, не являются несвязанными.
(3) Нет unwatch после просмотра $store of Vuex
(4) Внутренние переменные замыкания, формируемого модулем, не обнуляются после использования.
(5) Создан с использованием сторонней библиотеки без вызова правильной функции уничтожения
И вы можете использовать инструмент анализа памяти Chrome для быстрого устранения неполадок. В этой статье в основном используется основная функция моментальных снимков кучи памяти. Читатели могут попытаться проанализировать, есть ли утечки памяти на их собственных страницах. Метод заключается в выполнении некоторых операций, таких как извлечение box и закройте его, сделайте снимок кучи, найдите отсоединенные, отсортируйте по расстоянию, разверните родителя непустых узлов и найдите слова, отмеченные желтым, чтобы указать, что есть ссылки, которые не были выпущены. То есть этот метод в основном анализирует свободные узлы DOM, на которые еще есть ссылки. Поскольку утечка памяти страницы обычно связана с DOM, обычные переменные JS, как правило, не представляют проблемы из-за сборки мусора, если только переменные не перехватываются замыканиями и не используются и не пусты.
Утечки памяти, связанные с DOM, часто также вызываются замыканиями и привязками событий. После привязки (глобального) события вам нужно отвязать его, когда оно вам не нужно. Конечно, если вы напрямую привязаны к div, вы можете удалить div напрямую, и события, привязанные к нему, естественно не будут привязаны.
【Дополнительный】"Эффективный интерфейс" напечатано во второй раз