Опубликован: 15.05.2013 | Доступ: свободный | Студентов: 265 / 9 | Длительность: 24:25:00
Специальности: Системный архитектор
Лекция 14:

Ключевые концепции компонентов WinRT

< Лекция 13 || Лекция 14: 12345 || Лекция 15 >

Массивы, векторы и другие альтернативы

Теперь, когда мы рассмотрели базовую структуру асинхронных методов в WinRT-компонентах, посмотрим, как мы можем создать асинхронный вариант синхронного метода Convert, который мы реализовали ранее. Для целей этого упражнения мы будем придерживаться C#-компонента.

Было бы естественно рассмотреть IAsyncAction в качестве типа метода Convert, так как мы уже возвращаем результаты в выходной массив. Это будет, на самом деле, отличным выбором, если бы мы использовали тип, отличный от массива. Тем не менее, массивы представляют множество проблем для асинхронных методов. Во-первых, хотя мы можем передать в метод и входной и выходной массивы, и метод может выполнить свою работу и заполнить этот выходной массив, данные в настоящее время не будут переданы через границу асинхронной задачи. Поэтому обработчик завершения в приложения будет вызван, как и ожидается, но выходной массив, переданный асинхронному методу, будет пустым.

Слудующее, что мы можем попробовать, это превратить асинхронное действие в операцию, которая производит результат. Мы можем решить выбрать возвращаемый тип IAsyncOperation<Byte[]> (или эквивалент, возвращающий еще и данные о прогрессе операции), где метод создаст и заполнит массив для возврата. Проблема здесь, тем не менее, в том, что приложение , получившее массив, не знает, как получить к нему доступ. Очевидно, некоторая память была выделена для массива, но выделение произошло внутри компонента, а не внутри JavaScript, и в данной ситуации нет четких правил того, как следует поступать. Так как это верный путь для утечек памяти, возврат подобных массивов не поддерживается.

Альтернатива для асинхронного метода заключается в возврате специального типа коллекции WinRT (для которого применены четкие правила освобождения памяти), такого, как IList<Byte> , который будет в JavaScript конвертирован в вектор, к которому, кроме того, можно получить доступ как к массиву. (Обратите внимание на то, что тип IList специфичен для .NET-языков; пошаговый пример по C++ показывает, как использовать вектора напрямую с помощью типа concurrent_vector.) Вот простой пример подобного метода:

 public static IAsyncOperation<IList<Byte>
   > CreateByteListAsync(int size)
   {
   var task = Task.Run<IList<Byte>
     >(() =>
     {
     Byte [] list = new Byte[size];

     for (int i = 0; i < size; i++)
{
list[i] = (Byte)(i % 256);
}
return list.ToList();
});
return task.AsAsyncOperation();
}

Применение этого подхода к процедуре для преобразования в оттенки серого, мы получаем ConvertPixelArrayAsync (смотрите PixelCruncherCS > ConvertGrayscale.cs), где DoGrayscale это основной код процедуры, разбитый на различные функции, третий параметр – это периодически вызываемая функция обратного вызова, которую мы можем использовать для обработки отмены:

 
public IAsyncOperation<IList<Byte>
       > ConvertPixelArrayAsync([ReadOnlyArray()] Byte[] imageDataIn)
       {
       //Используем AsyncInfo для создания IAsyncOperation которая поддерживает отмену
       return AsyncInfo.Run<IList<Byte>
>((token) => Task.Run<IList<Byte>
  >(() =>
  {
  Byte[] imageDataOut = new Byte[imageDataIn.Length]; DoGrayscale(imageDataIn, imageDataOut, () =>
  {
  token.ThrowIfCancellationRequested();
  });
  return imageDataOut.ToList();
  }, token));
  }

Четвертый подход заключается в использовании шаблона, который применяется классом Windows.Graphics.Imaging.PixelDataProvider, который мы уже используем в упражнении "Image Manipulation". В функции setGrayscale (js/default.js), мы открываем файл, полученный от средства выбора файлов, и затем декодируем его с помощью BitmapDecoder.getPixelDataAsync. Результат этой операции – это PixelDataProvider, у которого есть метод detachPixelData который предоставляет нам пиксельный массив (некоторые участки кода опущены для краткости):

function setGrayscale(componentType) {
imageFile.openReadAsync().then(function (stream) {
return Imaging.BitmapDecoder.createAsync(stream);
}).then(function (decoderArg) {
//Конфиругирование декодера ... [код опущен]
return decoder.getPixelDataAsync();
}).done(function (pixelProvider) {
copyGrayscaleToCanvas(pixelProvider.detachPixelData(),
decoder.pixelWidth, decoder.pixelHeight, componentType);
});
      

Похожая реализация нашей процедуры конвертации в оттенки серого, находится в PixelCruncherCS > ConvertGrayscale.cs, в функции ConvertArraysAsync. Она имеет тип IAsyncAction , так как она работает с массивом Grayscale.inputData (который следует сначала установить). Выходные данные доступны из Grayscale.detatchOutputData(). Вот, как выглядит код JavaScript:

pc1.inputData = pixels;
pc1.convertArraysAsync().done(function () {
var data = pc1.detachOutputData()
copyArrayToImgData(data, imgData);
updateOutput(ctx, imgData, start);
});

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

В течение всего примера мы загружали данные изображения из файла, используя BitmapDecoder, и затем конвертируем полученные пиксели в оттенки серого в массиве, полученном с помощью метода createImageData элемента canvas. Как только данные окажутся внутри объекта данных изображения, мы можем вызвать метод putImageData элемента canvas для их вывода на экран. Все это было изначально реализовано для показа взаимодействия с этим элементом, включая сохранение его содержимого в файл. Это было нормально для Главы 4 курса "Пользовательский интерфейс приложений для Windows 8, созданных с использованием HTML, CSS и JavaScript", где нашей основной темой была работа с графикой. Однако, если нам нужна лишь конверсия файла изображения в оттенки серого, использование элемента canvas – это не лучший подход.

Ключевая проблема, с которой мы здесь столкнулись, заключается в том, что метод putImageData этого элемента принимает только объект ImageData, созданный методом createImageData все того же элемента canvas. Элемент не позволяет вам создать и вывести отдельный пиксельный массив, не позволяет и вставить произвольный массив в свойство ImageData.data. Единственный способ, которым можно воспользоваться, это прямая запись данных в массив ImageData.data.

В асинхронной версии методов нашего компонента можно передать ImageData.data в качестве выходного массива, таким образом, компонент сможет выполнить прямую запись в него. К сожалению, для асинхронной версии это невозможно. Подобные методы могут без проблем предоставить нам сконвертированные данные, но так как мы не можем использовать ImageData.data в роли подобного массива, мы вынуждены использовать вспомогательный механизм, наподобие функции copyArrayToImageData для копирования полученных результатов в ImageData.data, байт за байтом. Да уж. Это в значительной мере нивелирует любые улучшения производительности, которых мы могли достичь, создавая компоненты!

Позвольте мне уточнить, что это – ограничения элемента canvas, а не WinRT или самих компонентов. Перемещение массивов между приложением и компонентов, как мы уже видели, отлично работает в других сценариях. Тем не менее, ограничение принуждает нас задуматься о том, верный подход ли мы избрали.

Возвращаясь назад, можно вспомнить, что общая цель демонстрации была в конвертации файла изображения в оттенки серого и показе результатов на экране. Использование элемента canvas – это лишь деталь реализации – мы можем достигнуть тех же результатов другим путем. Например, вместо конверсии пикселей в массиве в памяти, мы можем создать временный файл, используя вместо этого класс Windows.Graphics.Image.BitmapEncoder, так же, как мы использовали его в функции SaveGrayscale, которая уже есть в приложении. Мы просто передали ей массив сконвертированных пикселей, вместо того, чтобы брать эти пиксели снова из элемента canvas. Тогда мы можем использовать URL.createObjectURL или URI ms-appdata:/// для отображения его в элементе img. Подобное будет производиться гораздо быстрее, так как метод putImageData элемента canvas требует много времени для выполнения, гораздо больше, чем занимают процедуры конверсии в наших компонентах.

В том же духе, нет особой причины, по которой мы не могли бы разместить весь этот процесс внутри компонента. Только та часть, которая работает с пользовательским интерфейсом, должна быть в JavaScript, но все остальное может быть написано на другом языке. Например, зачем беспокоить себя передачей пиксельного массива между JavaScript и WinRT-компонентом? Как только мы получили исходный StorageFile из средства выбора файлов, мы можем передать его напрямую в компонент. Затем он может использовать BitmapDecoder для получения пиксельноого потока, конвертировать его, затем создать временный файл и записать конвертированные пиксели обратно, используя BitmapEncoder, передать обратно объект StorageFile для временного файла, на основе которого мы можем установить img src. Таким образом, пиксельные данные никаогда не покидают компонент и никогда не копируются между буферами в памяти. Это должно привести и к более высокой производительности и к меньшему потреблению памяти.

Для этого в проекте PixelCruncherCS в упражнении "Image Manipulation" есть еще один асинхронный метод, который называется ConvertGrayscalFileAsync и выполняет в точности то, о чем я сказал выше:

public static IAsyncOperation<StorageFile>
   ConvertGrayscaleFileAsync(StorageFile file)
      {
  return AsyncInfo.Run<StorageFile>
  ((token) => Task.Run<StorageFile>(async () =>	
{	
StorageFile fileOut = null;	
try
{
//Открыть файл и прочесть пиксельные данные
using (IRandomAccessStream stream = await file.OpenReadAsync())
{
BitmapDecoder decoder = await BitmapDecoder.CreateAsync(stream); PixelDataProvider pp = await decoder.GetPixelDataAsync();
Byte[] pixels = pp.DetachPixelData();

//Мы знаем, что наш метод может произвести конверсию там же, где находятся данные
//поэтому нам не нужно создавать копию данных
DoGrayscale(pixels, pixels);

//Сохранить временный файл.
ApplicationData appdata = ApplicationData.Current;

fileOut = await appdata.TemporaryFolder.CreateFileAsync
( "ImageManipulation_GrayscaleConversion.png", CreationCollisionOption.ReplaceExisting);

using (IRandomAccessStream streamOut =	
await fileOut.OpenAsync(FileAccessMode.ReadWrite))	
{	
BitmapEncoder encoder = await BitmapEncoder.CreateAsync(
BitmapEncoder.PngEncoderId, streamOut);	

encoder.SetPixelData(decoder.BitmapPixelFormat, decoder.BitmapAlphaMode,
 decoder.PixelWidth, decoder.PixelHeight,
decoder.DpiX, decoder.DpiY, pixels);

await encoder.FlushAsync();
}
}
}
catch
{
//Произошла ошибка; очистим fileOut 
fileOut = null;
}

//Наконец, возвращаем созданный StorageFile, что делает удобным для вызывающей процедуры	
//скопировать его куда угодно, использовать в вызове наподобие URL.createObjectURL, или сослаться
//на него с помощью конструкции "ms-appdata:///temp" + fileOut.Name	


return fileOut;	
}));	
}	

В этом коде, в сравнении с эквивалентным кодом на JavaScript мы можем видеть, что ключевое слово await в C# очень упрощает работу с асинхронными методами – они выглядят так, как будто являются синхронными. Это одно из потенциальных преимуществ написания кода в компоненте! Другая важная деталь, на которую нужно обратить внимание, это использование операций с потоками. Потоки, в сравнении с другими типами, являются высвобождаемыми (disposable) (у них есть интерфейс IDisposable) и должны быть очищены после использования, иначе файлы остаются открытыми и вы можете столкнуться с исключением отказа в доступе или с другим странным поведением. Выражение using самостоятельно реализует подобную логику очистки.

В любом случае, имея этот метод, в JavaScript нам нужно лишь несколько строк кода для того, чтобы выполнить необходимые действия:

PixelCruncherCS.Grayscale.convertGrayscaleFileAsync(imageFile).done(function (tempFile) {
if (tempFile != null) {	
document.getElementById("image2").src = "ms-appdata:///temp/" + tempFile.name;	
}	
});	

Строки с URI можно так же заменить в этом случае:

var uri = URL.createObjectURL(tempFile, { oneTimeOnly: true });
document.getElementById("image2").src = uri;

При запуске тестов с данным подходом к конверсии изображения, приложене показывает гораздо лучшую скорость отклика, все происходит настолько быстро, что кольцевой индикатор прогресса выполнения операции, который обычно отображается при проведении операции, даже не появляется!

Все это иллюстрирует итог всего этого упражнения. Если вам нужна оптимизация, думайте не только о самих операциях, требующих больших объемов вычислений, но и о том, что они могут вовлекать в себя перемещение больших объемов данных. Как мы видели здесь, исследование недостатков нашего первого подхода позволило прийти к гораздо лучшему решению проблемы.

Проекции в JavaScript

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

Начнем с именования. Мы уже видели, что в проекты приложений на JavaScript компоненты добавляются в виде ссылок, после чего пространство имен компонента становится доступным в JavaScript, других объявлений не требуется. Пространство имен и классы в компоненте напрямую доступны в JavaScript. Однако, меняются имена методов, свойств и событий. Хотя пространство имен и имена классов проецируются в том виде, в котором они существуют в компоненте, имена методов и свойств (в том числе – члены структур (struct) и перечислений (enum)) конвертируются с использованием "верблюжьего" стиля: имена TestMethod и TestProperty в компоненте принимают вид testMethod и testProperty в JavaScript. Подобное изменение регистра символов может привести к необычному эффекту, как когда имя компонента начинается с двух заглавных букв, как, например, когда UIProperty, видно в JavaScript как uIProperty.

С другой стороны, имена событий приводятся к использованию только прописных букв, что соответствует соглашению об именовании, принятому в JavaScript. Событие, имеющее имя SignificantValueChanged в компоненте превращается в significantvaluechanged в JavaScript. Это имя, в нижнем регистре, используется с addEventListener, и классу, которые его предоставляет, так же будет задано свойство, имя которого состоит из данного имени с префиксом on, как, например, onsignificantvaluechanged. Важная особенность в работе с событиями заключается в том, что иногда нужно явным образом вызывать removeEventListener для предотвращения утечек памяти. Подробнее об этом – в Главе 3 курса "Введение в разработку приложений для Windows 8 с использованием HTML, CSS и JavaScript". В контексте данной лекции, подобное применимо и к вашим собственным WinRT-компонентам.

Статические члены класса, как мы уже видели, могут быть доступны напрямую, с использованием полностью определенного имени нужного метода или свойства и пространства имен компонента. Нестатические члены, с другой стороны, доступны только с помощью экземпляра данного класса, объявленного с ключевым словом new.

Вот два ограничения, которые мы упоминали ранее, и которые полезно будет повторить сейчас. Первое – это то, что WinRT-компоненты не могут работать с пользовательским интерфейсом приложения, написанного на JavaScript. Это так, потому что приложение не может получить поверхность для вывода любого рода, которую может использовать компонент. Второе – это то, что JavaScript может различать перегруженные методы лишь по их арности (количеству параметров), а не по типу. Если компонент предлагает перегрузки, отличающиеся лишь по типу, JavaScript может получить доступ только к тем из них, которые помечены как варианты по умолчанию.

Далее, мы приходим к вопросу о типах данных, что всегда интересно, когда речь идет о взаимодействии между разными языками. Вообще говоря, то, что вы видите в JavaScript, обычно соответствует тому, что есть в компоненте. Тип WinRT DateTime становится типом JavaScript Date, числовые значения становятся типом Number, bool превращается в Boolean, строки остаются строками и так далее. Некоторые типы WinRT, наподобие IMapView и IPropertySet, просто переходят в JavaScript напрямую как объектные типы, так как у них нет внутренних эквивалентов в нем. Вот еще некоторые, более интересные, преобразования:

  • Асинхронные операции в компоненте, которые возвращают интерфейс наподобие IAsyncOperation, проецируются в JavaScript как promise-объекты.
  • Так как JavaScript не имеет концепции struct, которая есть в C#, VB, и C++, структуры из WinRT-компонентов появляются в JavaScript как объекты с полями структуры в качестве членов. Похожим образом, для вызова WinRT-компонента, который принимает аргумент struct, JavaScript-приложение создает объект с полями, представленными членами и передает этот объект вместо структуры. Обратите внимание, что имена членов struct в JavaScript конвертируются с использованием "верблюжьего" стиля.
  • Некоторые типы коллекций, таких, как IVector, появляеются в JavaScript как массивы, но с различными методами. Таким образом, доступ к коллекции можно получить с помощью оператора массива [ ], но у такого массива будут другие методы. Будьте осторожны, таким образом, передавая эти массивы функциям манипуляции JavaScript, которые подразумевают наличие у массивов стандартных методов.
  • Перечисления преобразовываются в объекты с использованием свойств, соответствующих значениям перечисления, имена которых преобразуются к "верблюжьему" стилю, значения приводятся к JavaScript-типу Number.
  • API WinRT иногда возвращают типы Int64 (в виде отдельных значений или в structs), для которых нет эквивалентов в JavaScript. Однако, 64-битные типы в JavaScript сохраняются, поэтому вы можете передать их в WinRT при другом вызове. Однако, если вы модифицируете переменную, хранящую подобное значение, даже с использованием чего-то простого, вроде оператора ++, она будет конвертирована в тип JavaScript Number. Подобое значение не будет принтято методами, ожидающими Int64.
  • Если метод компонента предоставляет несколько выходных параметров, они видны в JavaScript как объект с этими различными значениями. Четкого стандартна для подобного преобразования в JavaScript нет, поэтому лучше стараться не создавать компоненты, имеющие подобную структуру.

Суть заключается в том, что уровень проекции пытается сделать WinRT-компоненты, написанные на любом другом языке выглядящими и работающими так, как будто они принадлежат JavaScript, и использование которых не будет вызывать сложностей..

< Лекция 13 || Лекция 14: 12345 || Лекция 15 >