В последние 30 лет происходило взрывное развитие индустрии аппаратного обеспечения компьютеров. От мейнфреймов к настольным компьютерам и далее к карманным устройствам аппаратное обеспечение становилось все мощнее даже при уменьшении в размерах. Этот постоянный рост вычислительных мощностей в какой-то мере развратил разработчиков, и теперь они ожидают наличия безграничных аппаратных ресурсов на любом устройстве, для которого они пишут приложения. Многие более молодые разработчики даже не знают тех времен, когда размер и эффективность кода были важнейшими факторами.
Новейший тренд в разработке — стремление использовать рост популярности смартфонов. При кодировании для смартфонов многие разработчики вынуждены подстраиваться под то, что, хотя современные смартфоны несравненно мощнее устройств, выпускавшихся всего несколько лет назад, они все же накладывают ряд ограничений. Эти ограничения связаны с их малым размером, ограниченными вычислительными ресурсами, памятью и поддержкой сетевых соединений. Создавая мобильные приложения, вы должны понимать, как обходить эти ограничения, чтобы обеспечить хорошую скорость работы приложения и его удобство в использовании.
Некоторые из причин менее чем оптимальной производительности приложения прямо связаны с неудачными проектировочными решениями, принятыми разработчиком. Но в других случаях ряд таких факторов неподвластен напрямую разработчику. Малая скорость работы приложения может быть следствием соединения с медленно работающим сторонним сервисом, часто разрываемых соединений по мобильной широкополосной связи или природы данных, с которыми вы работаете (например, потоковые медийные файлы или большие наборы данных).
Какова бы ни была причина, производительность приложения должна быть одной из главных целей любого разработчика. В этой статье мы обсудим некоторые высокоуровневые концепции проектирования надежных, управляемых данными приложений Windows Phone 7, удобных в использовании и способных к корректному масштабированию.
Давайте для начала построим сценарий, в рамках которого мы сможем рассмотреть некоторые варианты проектирования и кодирования. В качестве примера мы будем работать с вымышленным приложением для путешественников, которое, в том числе, сообщает информацию о выбранных пользователем авиарейсах. Как показано на рис. 1, на основном экране приложения отображаются элементы данных, включая текущую погоду и статус рейса. По мере того как приложения становятся все выразительнее и более ориентированными на данными, их разработка усложняется. Просто появляется больше мест, где ваш код может не оправдать ожиданий.
Рис. 1 Приложение-пример
Блокировка UI-потока
Начнем с UI. Здесь очень легко попасть впросак, проектируя мобильное приложение так же, как и настольное, поэтому рассмотрим некоторые проблемы, специфичные для UI смартфонов.
Если приложение не отвечает на команды пользователя с ожидаемой им скоростью, это может драматически повлиять на его впечатления от вашего приложения. Однако, как вы увидите, это весьма простые проблемы, которые легко предвидеть и устранить.
Возьмем, к примеру, ListBox. Когда ItemTemplate содержит изображения или загружает данные из канала, существует очень высокая вероятность блокировки UI-потока, из-за которой UI перестает реагировать до завершения обработки запросов или вычислений. Поэтому один из подходов к разработке UI — выполнять длительные вычисления, в том числе веб-запросы, вне UI-потока. По сути, это хороший подход в любом приложении — мобильном, настольном или каком-то другом.
Другой момент, способный создать проблемы с производительностью, — связывание большого количества элементов с ItemSource без регулировки встраивания в элемент управления ListBox. Лучше выполнять связывание с ObservableCollection и помещать в набор по несколько элементов каждые 20-30 мс.Тогда UI-поток останется «отзывчивым» для пользователя.
В случае нашего приложения-примера мы тоже интенсивно используем изображения на экране. ListBox должен реально загружать изображение, чтобы отобразить эти данные. Хотя на первый взгляд кажется, что тут все нормально, выполнение такой работы в UI-потоке не позволит использовать ввод с помощью жестов. Загрузка изображений в фоновом потоке решает ряд проблем в отношении как объема занимаемой памяти, так и высвобождения UI-потока, в то же время ускоряя работу приложения.
Мы должны выполнять рендеринг всего, что будет выводиться пользователю. Рендеринг требует разметки, выравнивания и вычислений для корректной визуализации. Чем больше уровней добавляется к UI, тем дороже обходятся вычисления и рендеринг в целом. Хотя Silverlight уже виртуализирует UI, это не относится к связываемым данным. То есть, если нам понадобилось бы связать 10 000 элементов с нашим ListBox, Silverlight создала бы экземпляры всех 10 000 ListItem до их рендеринга.
Вы должны понимать, какие именно данные вы связываете, и поддерживать минимально возможный размер связанного набора. Если вам нужно обрабатывать большие наборы элементов, связанных с данными, подумайте о динамической обработке рендеринга в фоне. Это относится и к настольным приложениям — просто влияние такого выбора на смартфоне проявляется намного ярче.
Объекты ValueConverter могут радикально повлиять на скорость рендеринга, так как они определяются с использованием собственного кода, а рендеринг нельзя определить заранее и кешировать до того, как будет создан реальный элемент и выполнена разметка.
Работа с данными
Теперь поговорим о хранилище данных в Windows Phone 7. Скажем прямо: никакой реляционной базы данных нет. Вместе с Windows Phone 7 устанавливается SQL Server Compact (SQL CE), но в настоящее время API для разработчиков отсутствует. Поэтому создать базу данных для хранения данных приложения (в нашем примере информации для путешественников) не получится.
Прояснив этот вопрос, заметим, что у вас есть широкий выбор того, как получать данные в приложении и отправлять их из него. Наиболее распространенный подход — хранение данных через сервис в облаке, например Windows Azure. Создать уровень сервисов в приложении можно с помощью множества технологий, но REST и SOAP относятся к числу самых популярных. Многие разработчики первым делом выбирают SOAP, но мы считаем, что REST обеспечивает более высокую эффективность и упрощает реализацию запросов данных.
Мы применяем несколько методов, которые предоставляют данные приложению и к которым мы можем обращаться, используя выражения REST, такие как:
/Trip/Create/PHL-BOS-SEA/xxxx/2010-04-10
/Flight/CheckStatus/US743
REST позволяет использовать сообщения в формате либо XML, либо JSON.
Для клиентского веб-интерфейса мы выбрали инфраструктуру ASP.NET MVC (asp.net/mvc), так как она позволяет обрабатывать запрос и возвращать любой тип разметки, используя собственное представление.
В нашем приложении-примере нужно обрабатывать информацию как для путешественников, так и об авиарейсах, поэтому мы создаем FlightController и TripController, перехватывающие запросы этой информации:
// GET: /Flight/CheckStatus/US743
public ActionResult CheckStatusByFlight(
string flightNumber) {
return CheckStatus(flightNumber, DateTime.Now);
}
// GET: /Flight/RegisterInterest/US743/2010-04-12
public ActionResult CheckStatus(
string flightNumber, DateTime date) {
Flight f = new Flight(flightNumber, date);
GetFlightStatus(f);
return new XmlResultView<Flight>(f);
}
Если пользователя интересует текущая дата, можно было бы упростить методы доступа и сэкономить несколько байтов пропускной способности соединения, создав сокращенный метод получения этих данных, в котором не требуется явно указывать текущую дату.
Кешированные и сохраненные данные
Сервис статуса авиарейса является элементом нашего приложения, который нам неподвластен и поэтому будет частью головоломки, связанной с производительностью. Поскольку успешное приложение может получать значительное количество запросов, важно продумать стратегию кеширования.
Обычно, чем ближе время вылета, тем больше запросов на эту информацию. Большее количество почти одновременных запросов может не только ухудшить производительность приложения, но и увеличить издержки хранения и манипуляций над данными. Как правило, в приложениях, работающих с Windows Azure, приходится оплачивать использование пропускной полосы и при запросе, и при возврате данных, а значит, сервисы информации по авиарейсам могли бы потребовать оплаты доступа к данным. Возвращать следует ровно столько данных, сколько необходимо приложению.
Платформа Windows Azure предоставляет широкий выбор средств хранения данных — от таблиц, больших двоичных объектов (blobs) и очередей до хранилищ SQL Azure, подобных реляционным базам данных. Мы решили задействовать SQL Azure, потому что оно использует привычные методы программирования SQL Server и позволяет легко сохранять и обращаться как к кешируемым данным по авиарейсам, так и к постоянно хранящейся информации для путешественников.
На рис. 2 показан простой уровень хранения, спроектированный нами с применением Entity Framework.
Рис. 2 Схема хранилища данных по авиарейсам
Возврат данных
Мы возвращаем данные клиенту через собственное представление. Поскольку мы используем ASP.NET MVC, каждое представление должно наследовать от ActionResult и реализовать ExecuteResult.
Как упоминалось, через наш REST-сервис мы можем передать либо XML-, либо JSON-представление информации об авиарейсах. Давайте сначала рассмотрим вариант с XML. Сериализатор, создающий XML, требует передачи типа, поэтому мы создаем класс Generics, как показано на рис. 3.
Рис. 3 Сериализация XML
public class XmlResultView<T> : ActionResult {
object _model = null;
public XmlResultView(object model) {
this._model = model;
}
public override void ExecuteResult(ControllerContext context) {
// Create where to write
MemoryStream mem = new MemoryStream();
// Pack characters as compact as possible,
// remove the decl, do not indent.
XmlWriterSettings settings = new XmlWriterSettings() {
Encoding = System.Text.Encoding.UTF8,
Indent = false, OmitXmlDeclaration = true };
XmlWriter writer = XmlTextWriter.Create(mem, settings);
// Create a type serializer
XmlSerializer ser = new XmlSerializer(typeof(T));
// Write the model to the stream
ser.Serialize(writer, _model);
context.HttpContext.Response.OutputStream.Write(
mem.ToArray(), 0, (int)mem.Length);
}
}
Мы могли бы легко работать с нашими данными и в формате JSON. Единственный элемент нашего решения, который потребовал бы изменения в этом случае, — тело метода ExecuteResult. Используя JsonResult, можно создавать JSON-данные, возвращаемые нашим сервисом, с помощью всего нескольких строк кода:
// Create the serializer
var result = new JsonResult();
// Enable the requests that originate from an HTTP GET
result.JsonRequestBehavior = JsonRequestBehavior.AllowGet;
// Set data to return
result.Data = _model;
// Write the data to the stream
result.ExecuteResult(context);
А как насчет сохранения данных на самом устройстве? Вряд ли есть смысл заставлять приложение извлекать данные из сервиса всякий раз, когда пользователю понадобится доступ к информации для путешественников. Хотя в Windows Phone 7 нет реляционного хранилища данных, у разработчиков, тем не менее, есть доступ к механизму под названием Isolated Storage (изолированное хранилище). Оно работает практически так же, как и изолированное хранилище Silverlight 4, но без ограничений на размер.
Для записи и получения записанных данных на устройстве под управлением Windows Phone 7 существует два основных метода: SaveData и GetSavedData. Примеры, демонстрирующие, как эти методы можно было бы использовать в приложении, информирующем об авиарейсах, приведены на рис. 4.
Рис. 4 Сохранение и получение локальных данных
public static IEnumerable<Trips> GetSavedData() {
IEnumerable<Trips> trips = new List<Trips>();
try {
using (var store =
IsolatedStorageFile.GetUserStoreForApplication()) {
string offlineData =
Path.Combine("TravelBuddy", "Offline");
string offlineDataFile =
Path.Combine(offlineData, "offline.xml");
IsolatedStorageFileStream dataFile = null;
if (store.FileExists(offlineDataFile)) {
dataFile =
store.OpenFile(offlineDataFile, FileMode.Open);
DataContractSerializer ser =
new DataContractSerializer(
typeof(IEnumerable<Trips>));
// Deserialize the data and read it
trips =
(IEnumerable<Trips>)ser.ReadObject(dataFile);
dataFile.Close();
}
else
MessageBox.Show("No data available");
}
}
catch (IsolatedStorageException) {
// Fail gracefully
}
return trips;
}
public static void SaveOfflineData(IEnumerable<Trips> trip) {
try {
using (var store =
IsolatedStorageFile.GetUserStoreForApplication()) {
// Create three directories in the root.
store.CreateDirectory("TravelBuddy");
// Create three subdirectories under MyApp1.
string offlineData =
Path.Combine("TravelBuddy", "Offline");
if (!store.DirectoryExists(offlineData))
store.CreateDirectory(offlineData);
string offlineDataFile =
Path.Combine(offlineData, "offline.xml");
IsolatedStorageFileStream dataFile =
dataFile = store.OpenFile(offlineDataFile,
FileMode.OpenOrCreate);
DataContractSerializer ser =
new DataContractSerializer(typeof(IEnumerable<Trip>));
ser.WriteObject(dataFile, trip);
dataFile.Close();
}
}
catch (IsolatedStorageException) {
// Fail gracefully
}
}
Обработка сбоев в сети
Сети, используемые мобильными устройствами, могут иметь широко варьируемые возможности, и временами оказываются недоступными из-за выхода из зоны покрытия сети, ее переполнения или даже отключения самим пользователем (например, в самолете). Вы должны принять это как факт. Разработчики должны учитывать это, создавая свои мобильные приложения.
Еще один вид ошибки сети — сбой уровня сервисов. Многие мобильные приложения используют данные от сторонних сервисов. Эти сервисы могут предоставляться без соглашений об уровне обслуживания (service-level agreements, SLA), что оставляет ваше приложение на милость провайдера. Иначе говоря, они вне вашего контроля, и вы должны быть готовы к обработке перебоев в работе таких сервисов.
Независимо от источника сбоя в сети вы все равно должны позаботиться о максимальном комфорте для пользователя. На случай отказа сети нужно предоставить некий уровень функциональности. В нашем приложении-примере в случае потери соединения с сетью на серверной или клиентской стороне требуется обеспечить пользователю доступ к максимально возможному объему информации.
Добиться этого можно самыми разнообразными способами. Но мы сосредоточимся на трех простых подходах: получении данных, пока сетевое соединение работает, локальное кеширование данных и кеширование данных на сервере, который вы можете контролировать.
Использование извещающих уведомлений
Когда пользователь вводит информацию о поездке в приложение, она загружается в облачный сервис. Потом сервис будет периодически опрашивать различные сервисы, предоставляющие метеорологические данные и сведения об авиарейсах. Он также ищет изменения в этих данных, например изменение в статусе рейса или уведомление от аэропорта о задержке рейса.
Когда изменение обнаруживается, эту информацию нужно своевременно и эффективно передать пользователю. Один из способов — сервис должен отправлять эту информацию клиентскому приложению. Это обеспечит пользователю доступ к наиболее актуальному набору данных в момент их появления. Поскольку данные «проталкиваются» клиенту, они доступны, даже если сетевое соединение теряется.
Добиться этого можно с помощью нашего сервиса в Windows Azure, используя Windows Phone Push Notification (извещающее уведомление). Средство Windows Phone Push Notification состоит из трех компонентов: сервисов мониторинга, Microsoft Push Notification Service и метода обработки сообщений.
Сервис мониторинга является облачным; он постоянно ищет новую информацию для нашего приложения. О нем мы поговорим немного позже.
Push Notification Service (сервис извещающих уведомлений) является частью сервисов, размещенных в Microsoft, которые используются для трансляции сообщений на устройства с Windows Phone 7. Этот сервис доступен всем разработчикам приложений для Windows Phone 7.
Метод обработки сообщений делает именно то, что предполагает его название: он просто принимает сообщения от Push Notification Service.
В Windows Phone 7 есть три типа уведомлений по умолчанию: Tile, Push и Toast. Уведомления являются важной частью создания комфортных условий для пользователя, и вам нужно тщательно продумать их применение. Часто повторяющиеся или прерывающие уведомления могут привести к падению производительности вашего приложения и других программ, выполняемых на устройстве. Кроме того, они могут просто раздражать пользователя. Подумайте о частоте, с которой будут посылаться уведомления, и о типах событий, к которым следует привлекать внимание пользователя.
В Windows Phone 7 уведомления доставляются пакетно, поэтому транзакция может быть не мгновенной. Своевременность уведомления не гарантируется, и решение о том, как доставлять уведомление клиенту, принимается сервисом; однако сервис прилагает максимум усилий, чтобы определить, насколько быстро он сможет передать сообщение на устройство.
Рабочий процесс в случае извещающих уведомлений выглядит следующим образом.
- Клиентское приложение запрашивает канальное соединение с Push Notification Service.
- Push Notification Service в ответ возвращает URI канала.
- Клиентское приложение посылает сообщение сервису мониторинга с передачей URI канала Push Notification Service.
- Когда сервис мониторинга обнаруживает изменение информации (в нашем примере — отмены вылетов, задержки или оповещения об изменении погоды), он отправляет сообщение в Push Notification Service.
- Push Notification Service транслирует сообщение на устройство с Windows Phone 7.
- Обработчик сообщений принимает и обрабатывает это сообщение на устройстве.
Локальное кеширование данных
Другой способ сделать данные доступными приложению — кешировать их локально, чтобы в UI всегда были какие-то данные. Затем в фоне используются другие средства для обновления локальных данных (если это возможно). Преимущество этого способа в том, что приложение может загружать и им можно быстро пользоваться, даже если обновление информации происходит асинхронно «за кулисами».
Если в двух словах, то вы используете изолированное хранилище для записи самого актуального набора данных. При открытии приложение немедленно извлекает любые данные, доступные в локальном изолированном хранилище и визуализирует их. Тем временем приложение вызывает сервис в Windows Azure для получения обновленной информации. Если новая информация найдена, она сериализуется и передается на устройство, изолированное хранилище обновляется, а вы вновь визуализируете UI с обновленной информацией. Для большего удобства пользователя вы, вероятно, захотите указывать в UI дату и время последнего обновления информации.
Кстати, если приложение использует проектировочный шаблон Model-View-ViewModel (MVVM), обновление UI может происходить автоматически через средства Silverlight для связывания с данными. Подробнее о MVVM и Silverlight см. статью Роберта Маккартера (Robert McCarter) «Problems and Solutions with Model-View-ViewModel» по ссылке msdn.microsoft.com/magazine/ff798279.
Кеширование данных на вашем сервере
Есть промежуточный вариант между прямым «проталкиванием» данных вашему приложению по мере их доступности и хранением данных на устройстве: получение данных от сторонних сервисов и их кеширование в облачном приложении до тех пор, пока их не запросит приложение Windows Phone 7.
Этот подход требует создания нового уровня абстракции в приложении. По сути, здесь цель состоит в том, чтобы исключить из приложения зависимость от стороннего сервиса. Ваш сервис извлекает и кеширует данные от любых сторонних сервисов. Если сторонний сервис выходит из строя, у вас остаются по крайней мере какие-то данные в кеше, откуда их можно предоставить приложению на устройстве.
Сервис наподобие этого можно легко клонировать или расширить на получение данных от большего количества различных сервисов, тем самым сокращая свою зависимость от конкретного провайдера или источника данных.
Подробнее о создании решений, ориентированных на данные, в Windows Azure см. статью Кевина Хоффмана (Kevin Hoffman) и Натана Дудека (Nathan Dudek) «Fueling Your Application’s Engine with Windows Azure Storage» (msdn.microsoft.com/magazine/ee335721). Кроме того, вам стоит почитать статью Пола Стаббса (Paul Stubb) «Create a Silverlight 4 Web Part for SharePoint 2010» — она, хоть и не относится непосредственно к Windows Phone 7, хорошо освещает вопросы связывания с данными применительно к Silverlight и веб-сервисам (msdn.microsoft.com/magazine/ff956224).
Сервис мониторинга
Как упоминалось, механизм уведомления является важной частью нашего приложения. Этот механизм на самом деле состоит из нескольких разных сервисов внутри приложения. По-видимому, наиболее важный из них для удобства использования приложения — сервис мониторинга, который регулярно опрашивает сторонние сервисы и ретранслирует на устройство такую информацию, как задержки рейсов, метеоусловия и др.
В нашем приложении сервис мониторинга считывает текущий список авиарейсов и коды аэропортов и использует эти сведения для сбора релевантных данных. Потом эта информация сохраняется в базе данных SQL Azure как элемент кеша, чтобы ее мог извлечь показанный ранее сервис /Flight/CheckStatus. Наш сервис мониторинга реализуется как рабочая роль в Windows Azure. Основная цель этой рабочей роли — получать информацию о задержках авиарейсов и метеоусловиях в аэропортах для каждого набора авиарейсов, указанных пользователем. Частота опроса для обновления растет по мере приближения времени вылета по расписанию.
Некоторые идеи насчет реализации такого сервиса посмотрите в проекте Azure Publish-Subscribe на CodePlex (azurepubsub.codeplex.com) или почитайте публикацию Джозефа Фултца (Joseph Fultz) в блоге «Migrating Windows Service to Azure Worker Role: Image Conversion Example Using Storage» (bit.ly/aKY8iv).
Заключение
Надеемся, мы сумели дать вам широкий обзор вопросов, которые вам придется обдумать перед проектированием приложения Windows Phone 7, управляемого данными. Отзывчивость UI, равно как и скорость доступа к источникам данных, крайне важны для пользователей вашего приложения.
Чтобы копнуть поглубже, начните со статьи Джошуа Партлоу (Joshua Partlow) «Getting Started with Windows Phone Development Tools» (msdn.microsoft.com/magazine/gg232764). Также посмотрите статью Джима Накашимы (Jim Nakashima), Хани Атасси (Hani Atassi) и Денни Торпа (Danny Thorpe) «Developing and Deploying Windows Azure Apps in Visual Studio 2010» (msdn.microsoft.com/magazine/ee336122).
Чтобы понять, как соединить разработки для Windows Azure и Windows Phone 7, прочитайте статью Рамона Архона (Ramon Arjona) «Windows Phone and the Cloud — an Introduction» (msdn.microsoft.com/magazine/ff872395).