Опубликован: 02.04.2013 | Уровень: для всех | Доступ: платный
Лекция 6:

Коллекции и элементы управления для вывода коллекций

Оптимизация производительности ListView

Я часто говорю людям, что с ListView можно столько всего делать, о нём можно столько всего узнать, что он достоен отдельной книги! На самом деле, Microsoft могла просто создать базовый элемент управления, который позволял бы вам создавать элементы, настроенные по шаблону и на этом остановиться. Однако, зная, что ListView будет в центре огромного количества приложений (возможно, в составе большинства приложений вне игровых приложений), ожидая, что ListView будет вызываться для управления тысячами, или даже десятками тысяч элементов, высокопрофессиональная и увлеченная своим делом группа инженеров сделала всё возможное для обеспечения множества уровней усовершенствований, которые позволят вашим приложениям наилучшим образом выполнять своё предназначение.

Одна из оптимизаций касается возможности загрузки страниц элементов по запросу, как определено в свойстве loadingBehavior, как описано в следующих двух разделах. Другая оптимизация заключается в использовании функций шаблонов для отложенной загрузки различных частей элементов, таких, как изображения, так же, как и задержка действий, наподобие анимации до тех пор, пока они не оказываются видны, о чём рассказано в третьем разделе ниже. Во всех случаях общая цель этих оптимизаций заключается в том, чтобы помочь ListView отобразить наиболее важные элементы или части элементов так быстро, как только возможно, откладывая загрузку и рендеринг других элементов или менее важных частей элементов до тех пор, пока они действительно не понадобятся.

Мне хотелось бы отметить, что материал "Использование элемента управления ListView" (http://msdn.microsoft.com/library/windows/apps/Hh781224.aspx) содержит даже больше советов, чем я способен привести здесь. (Мне нужно писать и другие главы!). Я надеюсь, что вы изучите данный материал, и кто знает, может быть вы станете тем, кто напишет всеобъемлющую книгу по ListView! Более того, дополнительное руководство по производительности приложения в целом можно найти в материале "Рекомендации по повышению производительности приложений Магазина Windows" (http://msdn.microsoft.com/library/windows/apps/hh465194.aspx), который содержит и подраздел об использовании ListView.

Произвольный доступ

Если вы похожи на меня и на других членов моей семьи, возможно, у вас есть постоянно увеличивающийся запас цифровых фотографий, который заставляет вас радоваться тому, что жёсткие диски объёмом больше гигабайта падают в цене. Другими словами, для многих пользователей вполне нормально работать с коллекциями, содержащими десятки тысяч элементов, которые они могут захотеть просмотреть в ListView. Но только представьте себе нагрузку от попытки загрузить эскизы для каждого из этих элементов в память для отображения их в списке. На низкопроизводительном аппаратном обеспечении, ограниченном в потреблении электроэнергии, это, возможно, приведет к необходимости срочного завершения работы нескольких приостановленных приложений, и результат может быть вовсе не "быстрым и гибким". Пользователь может столкнуться с необходимостью ждать по-настоящему долго до того момента, пока элемент управления приобретет интерактивность и может по-настоящему устать, глядя на индикатор выполнения.

Именно поэтому свойство loadingBehavior установлено по умолчанию в значение "randomaccess". В этом режиме полоса прокрутки ListView отражает общий размер списка, в итоге, пользователь сможет оценить его размер, но ListView в любой момент полностью хранит в памяти лишь пять полных экранов элементов (с общим лимитом в 1000 элементов). Для большинства страниц это означает видимую страницу (в области просмотра) и две буферных страницы впереди и позади неё. (Если вы просматриваете первую страницу, то буфер простирается на четыре страницы вперед; если вы на последней странице, то буфер простирается на четыре страницы позади неё - вы поняли идею).

Куда бы пользователь ни прокрутил список, любые страницы, не входящие в буферную зону или в область просмотра, отбрасываются (почти - мы сейчас к этому вернемся), после чего начинается загрузка новой видимой страницы и её буферных страниц. Таким образом, свойство ListView loadingState снова принимает значение itemLoading, затем принимает значение viewPortLoaded, когда выводятся видимые элементы, затем - itemsLoaded, когда загружены буферные страницы, и, затем, complete, когда всё выполнено. Опять же, в любое время, лишь пять страниц элементов загружены в память.

Теперь, когда я сказал, что ранее загруженные элементы отбрасываются, когда они выходят из диапазона видимой и буферных страниц, то, что с ними реально происходит можно назвать повторным использованием (recycling). Одна из наиболее ресурсоёмких частей вывода элементов - это создание элементов DOM, таким образом, ListView, на самом деле, перемещает эти элементы на новое место в списке и заполняет их новым содержимым. Это будет важно, когда мы посмотрим на оптимизацию в функциях шаблонов.

Инкрементная загрузка

Не говоря уже о потенциально очень больших, но известных коллекциях, другие коллекции, для любых намерений и целей, обычно границ не имеют, это такие коллекции, как ленты новостей, которые могут иметь миллионы записей, восходящих к Кайнозойской эре (по крайней мере, по подсчётам Интернета!). В случае с подобными коллекциями, вы, возможно, не будете знать, сколько элементов в них содержится. Лучшее, что вы можете сделать - это загрузить очередную порцию данных, когда она понадобится пользователю.

Это то, для чего нужно значение "incremental" свойства loadingBehavior. В этом режиме полоса прокрутки ListView отражает лишь то, что загружено в список, но если пользователь пересечет определенный порог, например, он прокрутит список до конца - ListView запросит у источника данных другие страницы элементов и добавит их в список, обновив полосу прокрутки. В данном случае все загруженные элементы остаются загруженными, обеспечивая очень быстрое перемещение по списку загруженных данных, но с потенциально большим потреблением памяти, чем при работе в режиме произвольного доступа.

Работа инкрементной загрузки показана в Сценариях 2 и 3 примера "Режимы работы загрузки данных ListView" (http://code.msdn.microsoft.com/windowsapps/ListView-loading-behaviors-718a4673) (Сценарий 1 посвящен произвольному доступу, но там нет ничего нового). Режим инкрементной загрузки активирует следующие характеристики:

  • Свойство ListView pagesToLoad показывает, как много страниц или полных экранов загружаются одновременно. Значение по умолчанию - пять.
  • Свойство automaticallyLoadPages показывает, следует ли ListView загружать новые страницы автоматически, когда вы перемещаетесь по списку. Если оно установлено в true (по умолчанию), как показано в Сценарии 2, когда вы прокручиваете список до конца, вы увидите изменение полосы прокрутки при загрузке новых страниц. Если в false, что показано в Сценарии 3, новые страницы не загружаются до тех пор, пока вы не вызовете метод loadMorePages.
  • Если свойство automaticallyLoadPages установлено в true, свойство pagesToLoadThreshold показывает, как близко пользователь должен подойти к текущему концу списка, прежде чем будет запущена загрузка новых страниц. Значение по умолчанию - два.
  • Когда новые страницы начинают загружаться (либо автоматически, либо в ответ на loadMorePages), ListView начинает обновлять свойство loadingState, вызывая события loadingstatechanged, как было уже описано.

Функции шаблонов (Часть 2): Promise-объекты!

Как мы только что обсудили, параметры поведения ListView при загрузке данных имеют отношение к инкрементной загрузке страниц. Будет полезным скомбинировать их с инкрементной загрузкой элементов. Для этого нам нужно взглянуть на то, что называется конвейером визуализации (rendering pipeline), в том виде, в котором это реализовано в функциях шаблонов.

Когда мы ранее впервые смотрели на функции шаблонов (посмотрите "Как, на самом деле, работают шаблоны"), я отмечал, что они дают нам возможность контролировать и то, как конструируется элемент, и то, когда это происходит, и то, что подобные функции называют визуализаторами (renderers). Это - средство, с помощью которого вы можете реализовать пять прогрессивных уровней оптимизации для ListView (и для FlipView, хотя это распространено меньше). Сам факт использования визуализатора, с чем мы уже сталкивались, - это Уровень 1. Теперь мы готовы увидеть оставшиеся четыре уровня. Это захватывающая тема, так как она показывает усовершенствования, которые реализованы для нас в ListView!

В этом рассказе мы можем опираться на пример "Оптимизация производительности HTML ListView" (http://code.msdn.microsoft.com/windowsapps/ListView-performance-39fb71f0), который демонстрирует все эти уровни и позволяет вам видеть их эффект. Вот обзор этих возможностей:

  • Простой (simple) или базовый визуализатор позволяет управлять выводом на поэлементной основе.
  • Визуализатор элементов-заполнителей (placeholder) разделяет создание элементов на два этапа. На первом этапе возвращаются лишь те объекты, которые определяют форму элементов. Это позволяет ListView быстро выполнить вывод шаблона, до того, как будут получены все подробные сведения, особенно когда данные поступают из потенциально медленного источника. Когда данные элемента доступны, начинается вторая фаза, запускается копирование этих данных в элементы и создаются дополнительные элементы, которые не влияют на форму элементов.
  • Визуализатор с повторным использованием элементов-заполнителей (recycling placeholder) добавляет возможность повторного использования существующего содержимого DOM для элементов, что быстрее, чем создание их с нуля. Для этих целей, ListView, зная, что его будут быстро пролистывать, хранит некоторое количество элементов, когда они оказываются не видны. В визуализаторе, вы добавляете ветвь кода для очистки повторно используемых элементов, если они к вам попадают, и для возвращения их в виде элементов-заполнителей. Затем вы заполняете их реальными значениями на второй стадии визуализации.
  • Многошаговый (multistage) визуализатор расширяет визуализатор с повторным использованием элементов-заполнителей отложенной загрузкой изображений и других данных тогда, когда элемент полностью готов в ListView. Кроме того, он задерживает любые действия, связанные с визуальным оформлением списка, такие, как анимации, до тех пор, пока элемент не появится на экране.
  • И, наконец, многошаговый пакетный (multistage batch) визуализатор добавляет возможность пакетного добавления изображений и других ресурсов, тем самым, отрисовывая их и получая возможность анимировать их появление в ListView в виде группы, в итоге ресурсы видеокарты (GPU) могут быть использованы более эффективно.

Используя любой из этих визуализаторов, вы должны стремиться к тому, чтобы сделать их как можно более быстрыми. В особенности это касается минимизации использования вызовов DOM API, которые включают в себя установку индивидуальных свойств. Используйте строку innerHTML, там, где это возможно, для создания элементов, вместо того, чтобы выполнять отдельные вызовы, и сведите к минимуму использование getElementById, querySelector и других вызовов, предусматривающих обход DOM, кэшируя элементы, к которым вы обращаетесь чаще всего. Это значительно улучшит производительность. Для того, чтобы показать эффект от этих улучшений, следующий рисунок показывает пример того, как происходит вывод данных в неоптимизированном ListView:

Жёлтые столбцы показывают исполнение JavaScript-кода приложения - то есть - время, потраченное внутри визуализатора. Бежевые столбцы показывают время, потраченное в DOM-макете, и столбцы цвета морской волны показывают вывод данных на экран. Как вы можете видеть, когда элементы добавляются по одному, есть перерывы в исполнении кода, и сложность здесь в том, что большинство дисплеев обновляются лишь каждые 10-20 миллисекунд (50-100 Гц). В результате мы имеем множество разрывов в процессе визуализации.

После улучшений, график может выглядеть так, как показано ниже, когда работа приложения скомбинирована в одном блоке, таким образом, значительно уменьшилась нагрузка, связанная с DOM-макетом (бежевые столбцы):

Как и другое изображение, это получено от инструмента для анализа производительности, который называется XPerf и является частью Windows SDK (смотрите врезку). Без изучения деталей, самое важное, что мы здесь разобрали - это шаги, которые нужно предпринять для достижения подобного результата. А именно, различные формы визуализиторов, которые вы можете применять, как показано в примере.

Врезка: XPerf и msWriteProfilerMark

Средство XPerf в Windows SDK, документацию по которому можно найти на странице "Инструменты анализа производительности Windows" (http://msdn.microsoft.com/en-US/performance/cc825801.aspx), могут хорошо помочь вам понять реальное поведение вашего приложения в конкретной системе. Помимо прочего, он записывает в журнал вызовы, которые вы выполняете к msWriteProfilerMark (http://msdn.microsoft.com/library/windows/apps/dd433074.aspx), вы можете это заметить, просматривая исходный код WinJS. Для того чтобы отобразить это с помощью xperf, однако, вам нужно запустить протоколирование такой командой:

xperf -start user -on PerfTrack+Microsoft-IE:0x1300

и завершить протоколирование нижеприведенной командой, где <trace_filename> - это любой путь и имя файла по вашему выбору:

xperf -stop user -d <trace_filename>.etl

Открытие .etl-файла, который вы сохранили, приведет к запуску Windows Perfomance Analyzer (Анализатора производительности Windows) и отобразит график событий. Щелчкните правой кнопкой мыши по графику, затем щёлкните Summary Table (Сводная таблица). В этой таблице разверните Microsoft-IE и затем разверните узел Mshtml_DOM_CustomSiteEvents. Столбец Field3 должна содержать текст, который вы передаете msWriteProfilerMark, а столбец Time(s) поможет вам определить, сколько времени заняло действие.

В качестве основы для наших экспериментов, вот простой визуализатор:

function simpleRenderer(itemPromise) {
return itemPromise.then(function (item) {
var element = document.createElement("div");
element.className = "itemTempl";
element.innerHTML = "<img src='" + item.data.thumbnail +
"' alt='Databound image' /><div class='content'>" + item.data.title + "</div>";
return element;
});
}

Эта структура ожидает доступности данных элемента и возвращает promise-объект для элемента, данные которого будут получены.

Визуализатор с повторным использованием элементов-заполнителей создаёт элемент в два этапа. Возвращаемое значение - это объект, который содержит минимальный элемент-заполнитель в свойстве element, и promise-объект renderComplete, который выполняет, если необходимо, остальную работу:

function placeholderRenderer(itemPromise) {	
// создает базовый шаблон для элемента, не зависящий от данных
var element = document.createElement("div");	
element.className = "itemTempl";	
element.innerHTML = "<div class='content'>...</div>";	
// Возвращает элемент в виде заполнителя, и обратный вызов для его обновления, когда данные будут 
//доступны
return {
element: element,

// задаёт promise-объект, который завершит работу, когда завершится визуализация
// itemPromise завершит работу, когда будут доступны данные
renderComplete: itemPromise.then(function (item) {
// изменяет элемент для включения данных
element.querySelector(".content").innerText = item.data.title;
element.insertAdjacentHTML("afterBegin", "<img src='" +
item.data.thumbnail + "' alt='Databound image' />");
})
};
}

Свойство element, коротко говоря, определяет форму элемента и возвращается из визуализатора немедленно. Это позволяет ListView выполнять вывод макета, после чего он будет заполнен с помощью отложенного результата renderComplete. Вы можете видеть, что renderComplete, в целом, содержит то же самое, что возвращает простой визуализатор, за исключением уже созданного элементов-заполнителей. (Другой пример - добавленный Сценарий 8 упражнения FlipView в дополнительных материалах к этой лекции, имеет закомментированный код, который реализует данную возможность).

Визуализатор с повторным использованием элементов-заполнителей теперь знает о существования второго параметра, который называется recycled, и который ListView (но не FlipView) может предоставить вашей функции визуализации, когда параметр ListView loadingBehavior установлен в "randomaccess". Если задан recycled, вы можете просто очистить элементы, вернув их в качестве элементов-заполнителей, и затем заполнить данными внутри promise-объекта renderComplete, как ранее.

Если он не предоставлен (когда ListView только что создан, или если loadingBehavior установлено в значение "incremental"), вы создаёте новый элемент:

function recyclingPlaceholderRenderer(itemPromise, recycled) {
var element, img, label;
if (!recycled) {
// создает базовый шаблон для элемента, не зависящий от данных 
element = document.createElement("div");
element.className = "itemTempl";
element.innerHTML = "<img alt='Databound image' style='visibility:hidden;'/>" + "<div class='content'>...</div>";
}
else {
// очищает элемент, после чего мы можем повторно использовать его 
element = recycled;
label = element.querySelector(".content");
label.innerHTML = "...";
img = element.querySelector("img");
img.style.visibility = "hidden";
}
return {
element: element,
renderComplete: itemPromise.then(function (item) {
// изменяет элемент для включения данных 
if (!label) {
label = element.querySelector(".content");
img = element.querySelector("img");
}
label.innerText = item.data.title; img.src = item.data.thumbnail; img.style.visibility = "visible";
})
};
}

В renderComplete, убедитесь, что проверили существование элементов, которые вы не создавали для нового элемента-заполнителя, таких, как label, и создайте их, если нужно.

Если вы хотите очистить элементы, которые используются повторно, вы можете предоставить функцию для свойства ListView resetItem, которая будет содержать тот же код, как показано выше для данного случая. То же самое справедливо для свойства resetGroupHeader, так как вы можете использовать функции шаблона для заголовков групп, так же, как и для элементов. Мы не говорим много об этом, так как заголовков групп обычно немного, и они обычно не играют в производительности такой же роли. Несмотря на это, подобная возможность присутствует.

Далее, у нас есть многошаговый визуализатор, который добавляет к повторно используемым элементам-заполнителям отложенную загрузку изображений и других данных тогда, когда элемент полностью представлен в DOM, и, далее, задерживает эффекты, наподобие анимации, до тех пор, пока элемент не окажется видимым на экране.

Ключевую роль здесь играют три метода: ready, loadImage и isOnScreen, которые прикрепляются к элементу, предоставленному itemPromise. Следующий код показывает, как этим пользоваться (здесь element.querySelector обходит лишь небольшую часть DOM, в итоге, это не проблема):

renderComplete: itemPromise.then(function (item) {	
// преобразует элемент для обновления его названия (title)	
if (!label) { label = element.querySelector(".content"); }
label.innerText = item.data.title;	
// использует promise-объект item.ready для того, чтобы отложить более
// ресурсоёмкие процедуры
 return item.ready;
// использует возможность объединения promise-объектов в цеочку
// для получения возможности отмены задания
}).then(function (item) {
//использует загрузчик изображений для того, чтобы
//поставить в очередь загрузку изображения
if (!img) { img = element.querySelector("img"); }
return item.loadImage(item.data.thumbnail, img).then(function () {
//как только загружено, проверяет видимость элемента 
return item.isOnScreen();
});
}).then(function (onscreen) {
if (!onscreen) {
//если элемент не видим, его прозрачность не анимируется 
img.style.opacity = 1;
} else {
//если элемент видим, анимировать прозрачность изображения
WinJS.UI.Animation.fadeIn(img);
}
})

Хочу предупредить, что в данной оптимизации производительности используется много promise-объектов! Но всё, что здесь имеется - это стандартная структура promise-объектов, объединенных в цепочку. Первая асинхронная операция в визуализаторе обновляет простые частки элемента, такие, как текст. Затем она возвращает promise-объект в item.ready. Когда данный вызов завершится, или, точнее, если он будет исполнен - вы можете использовать асинхронный метод элемента loadImage для загрузки изображения, возвращая promise-объект item.isOnScreen из его обработчика завершения. Когда и если отложенный результат isOnScreen будет получен, вы можете выполнить необходимые действия, которые нужны только для видимых элементов.

Я выделял "если" в этих описаниях, так как весьме вероятно то, что пользователь будет прокручивать содержимое ListView, пока всё это происходит. Когда все эти promise-объекты объединены в цепочку, ListView может отменить асинхронные операции в любой момент, когда элемент выходит из области видимости или из области буферных страниц. Достаточно сказать, что элемент управления ListView прошёл через великое множество испытаний производительности!

Теперь мы пришли к многошаговому пакетному визуализатору, который комбинирует вставку изображений в DOM для минимизации задач, связанных с макетом и перерисовкой. В нашем примере, для этого используется функция, которая называется createBatch, которая использует метод WinJS.Promise.timeout с 64-миллисекундным периодом для комбинации promise-вызовов, загружающих изображения в многошаговом визуализаторе. Честно говоря, вам придётся поверить мне на слово, так как вам надо стать настоящим экспертом в использовании promise-объектов для того, чтобы понять, как это работает!

//При инициализации (за пределами визуализатора)
thumbnailBatch = createBatch();

//Внутри цепочки renderComplete 
//...

}).then(function () {
return item.loadImage(item.data.thumbnail);
}).then(thumbnailBatch()
).then(function (newimg) {
img = newimg;
element.insertBefore(img, element.firstElementChild);
return item.isOnScreen();
}).then(function (onscreen) {

//...
//Реализация createBatch

function createBatch(waitPeriod) {
var batchTimeout = WinJS.Promise.as();
var batchedItems = [];

function completeBatch() {
var callbacks = batchedItems;
batchedItems = [];
for (var i = 0; i < callbacks.length; i++) {
callbacks[i]();	
}	
}	
return function () {
batchTimeout.cancel();
batchTimeout = WinJS.Promise.timeout(waitPeriod || 64).then(completeBatch);
var delayedPromise = new WinJS.Promise(function (c) {
batchedItems.push(c);	
});	
return function (v) { return delayedPromise.then(function () { return v; }); };
};	
 }

Я ведь предупреждал вас о том, что мы встретимся с promise-объектами в будущем? Но, к счастью, разговор о функциях шаблонов завершен, но это время потрачено с пользой, так как оптимизация производительности ListView, как я уже говорил, значительно улучшит восприятие ваших приложений пользователями.

Что мы только что изучили

  • Коллекциями данных в памяти можно управлять с помощью WinJS.Binding.List, который отлично интегрируется с элементами управления для коллекций, наподобие FlipView и ListView. Данные коллекций, размещенных в памяти, могут поступать из WinJS.Xhr и из файлов.
  • Элемент управления WinJS.UI.FlipView отображает один элемент за раз; WinJS.UI.ListView отображает множество элементов в соответствии с конкретным макетом.
  • Центральная идея, общая для обоих элементов управления, заключается в том, что имеется источника данны, для вывода каждого элемента этого источника используется шаблон элемента. Шаблоны могут быть заданы как декларативно, так и программно.
  • ListView работает с добавленным описанием макета. WinJS предоставляет два встроенных макета. GridView - это двумерный, горизонтально прокручиваемый список; ListLayout предназначен для вывода одномерных вертикально прокручиваемых списков. Возможно реализовать и собственный макет.
  • ListView предоставляет возможность отображения элементов в группах; WinJS.BindingList предоставляет методы для создания сгруппированных, отсортированных и отлфильтрованных проекций элементов из источника данных.
  • Элемент управления для семантического масштабирования (WinJS.UI.SemanticZoom) предоставляет интерфейс, посредством которого вы можете переключаться между двумя различными представлениями источника данных, режимом детализированного представления (zoomed-in), который отображает детали, и режимом общего представления (zoomed-out), который предоставляет более общую информацию. Внешний вид этих двух режимов может очень сильно различаться, но они должны отображать связанные данные. Интерфейс IZoomableView нужен для каждого режима просмотра, таким образом, элемент управления SemanticZoom может переключаться между ними и перемещаться к верному элементу.
  • WinJS предоставляет StorageDataSource для создания коллекций на основе элементов StorageFile.
  • Можно реализовать пользовательский источник данных, как показано в примерах Windows SDK.
  • Программно заданные шаблоны реализованы в виде функций шаблонов, или визуализаторов. Эти функции могут реализовывать прогрессивные уровни оптимизации для отложенной загрузки изображений и пакетного добавления элементов в DOM.
  • И FlipView, и ListView предоставляют множество параметров и возможностей по стилизации. ListView, кроме того, позволяет организовывать выделение объектов и применять различные схемы поведения при выделении.
  • Элемент управления ListView обеспечивает встроенную поддержку оптимизации произвольного доступа к большим источникам данных, и, так же, инкрементный доступ к потенциально бесконечным источникам данных.
  • Элемент управления ListView поддерживает понятие объединения ячеек в gridLayout для поддержки отображения элементов разных размеров, каждый из которых должен быть кратен размеру базовой ячейки.
Владимир Мороз
Владимир Мороз
Украина, Киев, Киевская государственная академия водного транспорта имени Гетмана Петра Конашевича-Сагайдачного, 2012
Сергей Ширяев
Сергей Ширяев
Россия, г. Москва