Разработка под Windows Phone: Часть 5: Жизненный цикл приложения, фоновые сервисы и многозадачность

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

Жизненный цикл и сохранение состояния приложения

В Windows Phone только одно приложение может быть активно/запущено в каждый момент времени. Когда приложение перестаёт быть активным, операционная система переводит его в спящее состояние (dormant). Если памяти устройства недостаточно для хорошей работы активного приложения, операционная система начинает завершать спящие (dormant) приложения. При этом, последними будет завершены приложения, которые запускались недавно.

Платформа Windows Phone предоставляет разработчику события, методы и объекты, которые он может использовать для необходимых действий на каждом жизненном цикле приложения.

Приложение позволяет разработчику обработать события Launching, Closing, Activated и Deactivated события. Поскольку пользователь покидает какую-то страницу или приходит на какую-то страницу приложения, с этими событиями связаны два доступных для переопределения метода страниц приложения: OnNavigatedTo и OnNavogatedFrom.

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

Пользователь запускает приложение со стартовой страницы телефона или списка приложений, работает с приложением, затем нажимает кнопку Back телефона, находясь на стартовой странице приложения:

  1. Приложение запускается, вызывается событие Launching
  2. Загружается стартовая страница приложения, вызывается её метод OnNavigatedTo
  3. Приложение работает.
  4. Пользователь выходит из приложения, нажимая кнопку Back, находясь на стартовой странице приложения.
  5. Вызывается метод OnNavigatedFrom стартовой страницы приложения
  6. Вызывается событие Closing приложения.

Пользователь запускает приложение со стартовой страницы телефона или списка приложений, работает с приложением, затем нажимает кнопку перехода к стартовой странице телефона, находясь на любой странице приложения, а потом быстро возвращается обратно (операционная система не завершает приложение), используя длинное нажатие кнопки Back и выбирая страницу приложения, с которой он ушёл.

  1. Приложение запускается, вызывается событие Launching
  2. Загружается стартовая страница приложения, вызывается её метод OnNavigatedTo
  3. Приложение работает.
  4. Пользователь выходит из приложения, нажимая кнопку Back, находясь на стартовой странице приложения.
  5. Вызывается метод OnNavigatedFrom стартовой страницы приложения
  6. Вызывается событие Deactivated приложения.
  7. Приложение переходит в спящее (dormant) состояние.
  8. Пользователь не интенсивно пользуется телефоном, так что выгрузки приложения из спящего состояния не происходит.
  9. Пользователь возвращается в приложение, используя длинное нажатие кнопки Back и выбирая страницу приложения, с которой он ушёл.
  10. Вызывается событие Activated приложения.
  11. Загружается страница приложения с которой ушёл пользователь и вызывается её метод OnNavigatedTo
  12. Приложение работает.

Что произойдёт, если операционной системе потребуется больше памяти, когда приложение находится в памяти в спящем состоянии?

Тогда приложение завершит свою работу, но сохранит состояние стека навигации, а также состояние словарей состояния на уровне приложения  (PhoneApplicationService.State) и на уровне страницы (PhoneApplicationPage.State) –  перейдёт в состояние Tombstoned. Обратите внимание, чтоы выход из которого в разрезе возникающих событий и вызываемых методов не отличается от выхода из спящего (dormant) состояния, но при этом, поскольку приложение будет выгружено из памяти, необходимо позаботиться о сохранении данных для восстановления состояния приложения. Узнать об это разработчик может, проверив свойство IsApplicationInstancePreserved у ActivatedEventArgs. В случае true – это восстановление из спящего состояния, в случае же false – из Tombstone состояния.

Одновременно система сохраняет состояние Tombstoned только для пяти приложений. Если пользователь не возвращается к приложению, данные удаляются, и приложение будет запускаться с событием Launching.

Подробнее о жизненом цикле приложения можно прочитать по следующей ссылке: https://msdn.microsoft.com/en-us/library/ff769557(v=VS.92).aspx

Давайте теперь на практике попробуем разобраться с сохранением состояния приложения.

Создадим новое приложение на базе стандартного шаблона Windows Phone Application и назовём приложение ApplicationStateExample.

Исправьте код XAML страницы приложения на следующий:

<!--TitlePanel contains the name of the application and page title-->
        <StackPanel x:Name="TitlePanel" Grid.Row="0" Margin="12,17,0,28">
            <TextBlock x:Name="ApplicationTitle" Text="STATE SAVER" Style="{StaticResource PhoneTextNormalStyle}"/>
            <TextBlock x:Name="PageTitle" Text="message log" Margin="9,-7,0,0" Style="{StaticResource PhoneTextTitle1Style}"/>
        </StackPanel>
 
        <!--ContentPanel - place additional content here-->
        <Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
            <StackPanel>
                <TextBox Name="Message" Height="200" TextWrapping="Wrap"></TextBox>
                <Button Name="Log" Height="80" Content="Log"></Button>                
            </StackPanel>            
        </Grid>

Соберите приложение и запустите его (F5). Наберите что-нибудь в поле ввода, а затем перейдите на стартовую страницу, нажав среднюю кнопку на эмуляторе. Потом нажмите и удержите кнопку Back отобразится список доступных приложений, выберите наше приложение. Обратите внимание, что состояние страницы сохранилось, т.к. приложение было в спящем (dormant) состоянии без какого либо участия с нашей стороны.

Завершите работу приложения, перейдите в настройки отладки и установите настройку Tombstone upon deactivation while debugging:

Соберите приложение и запустите его (F5). Наберите что-нибудь в поле ввода, а затем перейдите на стартовую страницу, нажав среднюю кнопку на эмуляторе. Потом нажмите и удержите кнопку Back отобразится список доступных приложений, выберите наше приложение. Обратите внимание, что состояние страницы не сохранилось, т.к. приложение было выгружено и перешло в состояние Tombstone, так что  состояние страницы и приложения было утеряно.

Добавим в код страницы MainPage.xaml.cs функции OnNavigatedFrom и OnNavigatedTo, сохранив текст из поля ввода в словарь состояний:

protected override void OnNavigatedFrom(System.Windows.Navigation.NavigationEventArgs e)
        {
            base.OnNavigatedFrom(e);
 
            this.State.Add("text", Message.Text);
        }
 
        protected override void OnNavigatedTo(System.Windows.Navigation.NavigationEventArgs e)
        {
            base.OnNavigatedTo(e);
 
            if (this.State.ContainsKey("text"))
            {
                Message.Text = (string)this.State["text"];
            }
        }

Cоберите приложение и запустите его (F5). Наберите что-нибудь в поле ввода, а затем перейдите на стартовую страницу, нажав среднюю кнопку на эмуляторе. Потом нажмите и удержите кнопку Back отобразится список доступных приложений, выберите наше приложение. Обратите внимание, что состояние страницы сохранилось, несмотря на то, что приложение было выгружено и перешло в состояние Tombstone, т.к. мы сохранили состояние страницы в специальном словаре для сохранения состояния страницы.

Это не совсем правильное поведение кода, т.к. в случае, если мы восстанавливаемся из спящего состояния, нам не стоит модифицировать состояние страниц.

Давайте добавим логическую переменную, чтобы определить, вызвался конструктор (это значить, что, мы либо стартуем первый раз, либо восстанавливается из tomnstone состояния) и модифицируем код соответствующим образом:

bool isNewlyCreatedPage = false;
        
        // Constructor
        public MainPage()
        {
            InitializeComponent();
 
            isNewlyCreatedPage = true;
        }

И

protected override void OnNavigatedTo(System.Windows.Navigation.NavigationEventArgs e)
        {
            base.OnNavigatedTo(e);
 
            if (this.State.ContainsKey("text") && isNewlyCreatedPage)
            {
                Message.Text = (string)this.State["text"];
            }
 
            isNewlyCreatedPage = false;
        }

Запустите приложение (F5) и проверьте, как оно работает.

Снимите флажок настройки отладки Tombstone upon deactivation while debugging и проверьте, что приложение не восстанавливает состояние в случае выхода из спящего (dormant) состояния.

Добавим в приложение код, чтобы попробовать сохранить состояние приложения.

Добавим TextBlock и обработчик Click в XAML код страницы:

<StackPanel>
                <TextBox Name="Message" Height="200" TextWrapping="Wrap"></TextBox>
                <Button Name="Log" Height="80" Content="Log" Click="Log_Click"></Button>
                <TextBlock Name="AppState" Height="80"></TextBlock>
            </StackPanel>

В файл App.xaml.cs добавим публичное поле AppState:

public string AppState = "";


Модифицируем код файда MainPage.xaml.cs, чтобы при нажатии на кнопку сообщение из TextBox записывалось в AppState и отображалось в TextBlock, а при восстановление приложения AppState отображалось в TextBlock:

protected override void OnNavigatedTo(System.Windows.Navigation.NavigationEventArgs e)
        {
            base.OnNavigatedTo(e);
 
            if (this.State.ContainsKey("text") && isNewlyCreatedPage)
            {
                Message.Text = (string)this.State["text"];
 
                App myApp = App.Current as App;
                AppState.Text = myApp.AppState;
            }
 
            isNewlyCreatedPage = false;
        }
 
        private void Log_Click(object sender, RoutedEventArgs e)
        {
            AppState.Text = Message.Text;
            
            App myApp = App.Current as App;
 
            myApp.AppState = AppState.Text;
        }

Теперь необходимо добавить код сохранения/восстановления в App.xaml.cs:

// Code to execute when the application is activated (brought to foreground)
        // This code will not execute when the application is first launched
        private void Application_Activated(object sender, ActivatedEventArgs e)
        {
            if (!e.IsApplicationInstancePreserved)
            {
                AppState = (string)PhoneApplicationService.Current.State["appState"];
 
            }
        }
 
        // Code to execute when the application is deactivated (sent to background)
        // This code will not execute when the application is closing
        private void Application_Deactivated(object sender, DeactivatedEventArgs e)
        {
            PhoneApplicationService.Current.State.Add("appState", AppState);
        }

Перейдите в настройки отладки и установите флажок Tombstone upon deactivation while debugging, а затем запустите приложение (F5) и проверьте, что состояние приложения  сохраняется.

Обратите внимание, что когда приложение вызывает задачи выбора и запуска, также возникает события Deactivated.

Многозадачность и фоновые сервисы

Несмотря на то, что в Windows Phone только одно приложение может быть активным, оно может воспользоваться специальными возможностями платформы, чтобы выполнять определенные задачи, даже не являясь активным.

Специальные возможности платформы включают в себя возможность создания фоновых сервисов, запускаемые по расписанию, возможность проигрывания музыки и загрузки/выгрузки файлов в фоновом режиме,   а также регистрацию оповещения (Alarms)  и напоминания (Reminders). Об оповещениях и напоминаниях мы поговорим позже, сейчас кратко остановимся на фоновых сервисах.

Фоновые сервисы, запускаемые по расписанию

Можно создавать два типа сервисов:  периодический (PeriodicTask) и интенсивный (ResourceIntensiveTask). Периодический сервис запускается регулярно на небольшое время, интенсивный – запускается на относительно длительное время, но только если выполнятся определённые требования, относящиеся к состоянию телефона. API создания сервисов доступен в пространстве имён Microsoft.Phone.Scheduler.

Есть определенные ограничения, связанные в целом с сервисами, запускаемыми по расписанию, и с каждым типом сервисов в отдельности:

  • приложение может иметь только один фоновый сервис, запускающийся по расписанию, но он может быть зарегистрирован и как периодический  и как интенсивный;
  • не весь API доступный на устройстве доступен в сервисах;
  • сервис необходимо инициализировать  в активном приложении и он будет сохраняться между перезагрузками телефона;
  • у пользователя есть возможность контролировать его работу в настройках телефона;
  • максимальное время работы сервиса – 14 дней (после этого можно инициализировать заново);
  • если агент завершит свою работу два раза подряд из-за превышения ограничений на память или из-за необработанного исключения, он будет удалён из расписания запуска;
  • количество сервисов на устройстве ограничено и зависит от конфигурации устройства.

Ниже приведена сравнительная таблица ограничений на периодический и интенсивный сервисы:

  Периодический Интенсивный
Интервал запуска Обычно, 30 минут, но в зависимости от состояния телефона (батарейка, запущенные процессы) может сдвигаться +/- 10 минут

Телефон подключён к внешнему источнику питания

Доступ в сеть не через сотовую связь

Заряд батареи не менее 90%

Нет активного звонка

Телефон залочен

Время работы Порядка 25 секунд Порядка 10 минут
Ресурсы Меньше 6 Мб памяти, меньше 10% ресурсов процессора Меньше 6 Мб памяти


Как мы уже говорили выше, не весь API доступен для использования в сервисах. Подробно со списком API можно познакомиться в документации MSDN: https://msdn.microsoft.com/en-us/library/hh202962(v=VS.92).aspx  Ниже небольшая таблица, чтобы можно было получить представление о том, какой API доступен, а какой нет:

API доступен API запрещён
Tiles Работа с UI
Toast XNA
Location Микрофон и камера
Network Сенсоры
Isolated Storage Проигрывание аудио
Sockets Скачивание файлов
Большинство Silverlight API Регистрация новых сервисов


Теперь у нас есть неплохая теоретическая подготовка по фоновым сервисам, которые запускаются по расписанию.

Создайте новое приложение из шаблона Windows Phone Application, назовём его BackgroundAgentExample. В Solution Explorer, щелкнув правой кнопкой мыши по решению, в отобразившемся меню выберите Add, затем Add, затем New Project. Выберите тип проекта Windows Phone Scheduled Task Agent и назовите его ToastAgent.

Перейдите к проекту ToastAgent, двойным щелчком перейдите к редактированию  файла ScheduledAgent.cs.

Добавьте в блок using код:

using Microsoft.Phone.Shell; 

А в процедуру OnInvoke, которая вызывается при запуске сервиса, добавьте код отображения toast сообщения:

protected override void OnInvoke(ScheduledTask task)
        {
            ShellToast toast = new ShellToast();
            toast.Title = "Toast Agent";
            toast.Content = "Сообщение от фонового сервиса";
            toast.Show();
 
#if DEBUG
            ScheduledActionService.LaunchForTest(task.Name, System.TimeSpan.FromSeconds(10));
#endif
            
            NotifyComplete();
        }

Обратите внимание на код внутри директив условной компиляции. Он позволяет не ждать 30 минут запуска сервиса, а запустить его через 10 секунд после его последнего запуска.

Вернёмся теперь к основному проекту.

Сначала добавим в него ссылку на проект ToastAgent. Для этого щелкните левой кнопкой мыши по папке References, в выпадающем меню выберите Add Refernce, в отобразившемся диалоговом окне выберите слева Projects, Solution, в центральной части ToastAgent и нажмите кнопку Add, а потом Close.

Перейдём к редактированию кода MainPage.xaml. Добавим кнопку, чтобы запустить наш периодический сервис и добавим обработчик события Click:

<!--ContentPanel - place additional content here-->
        <Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
            <Button Content="Start Toast" Height="150" Name="StartAgent" Click="StartAgent_Click" />
        </Grid>

Добавим в блок using директиву:

using Microsoft.Phone.Scheduler;

и определим константу с именем сервиса в классе MainPage:

const string ToastAgentName = "Agent-Toast";


Отредактируем обработчик события нажатия кнопки так, чтобы запускался сервис ToastAgent.

private void StartAgent_Click(object sender, RoutedEventArgs e)
        {
            PeriodicTask myPeriodicTask = ScheduledActionService.Find(ToastAgentName) as PeriodicTask;
 
            if (myPeriodicTask != null)
            {
                try
                {
                    ScheduledActionService.Remove(ToastAgentName);
                }
                catch (Exception ex)
                {
                    MessageBox.Show("Невозможно удалить ранее созданный сервис:"+ex.Message);
                }
            }
 
            myPeriodicTask = new PeriodicTask(ToastAgentName);
       myPeriodicTask.Description = "Agent-Toast";

 
            try
            {
                ScheduledActionService.Add(myPeriodicTask);
                
 
                
#if DEBUG
                ScheduledActionService.LaunchForTest(ToastAgentName, TimeSpan.FromSeconds(10));
#endif
            }
            catch (Exception ex)
            {
                MessageBox.Show("Невозможно создать сервис:" + ex.Message);
            }
 
        }

Поле описание сервиса является обязательным, без него не удастся добавить сервис.

Обратите внимание, что  и здесь присутствует код, который в случае отладки вызывает сервис через 10 секунд.

Запустите приложение (F5) и протестируйте его работу: запустите приложение, нажмите кнопку Start Toast, выйдите из приложения, дождитесь появления сообщения от сервиса.

Перейдите в настройки приложений, в панель background tasks и проверьте, что наш сервис там действительно зарегистрирован

В качестве самостоятельного упражнения, попробуйте обработать вариант, когда сервис отключен пользователем, не используя исключений.

Фоновая загрузка/выгрузка файлов

Для фоновой загрузки/выгрузки файлов существует отдельный API, находящийся в пространстве имён Microsoft.Phone.BackgroundTransfer.

Файлы можно загружать/выгружать из/в изолированное хранилище (Isolated Storage) приложения, при этом процесс будет продолжаться, даже если приложение не запущено. Если приложение запущено, но оно может отлеживать состояние и отображать его статус фоновой загрузки файлов. Пока поддерживаются GET/POST HTTP/HTTPS запросы для скачивания файлов и POST HTTP/HTTPS запросы для загрузки файла на сервер.

Также существуют ограничения на размеры файлов, которые можно скачать/загрузить на сервер. На сервер можно загрузить файл не больше 5 Мб, скачать файл  размером до 20 Мб можно по сотовой связи, файлы размером до 100 Мб – по беспроводной (WiFi) (если нет подключения к источнику питания).

Каждое приложение может иметь до 5 одновременных запросов на фоновую загрузку/выгрузку файлов. Обратите внимание, что при завершении загрузки/выгрузки запросы не удаляются – это необходимо сделать вручную. При этом, всего на устройстве может быть до 500 запросов на фоновую загрузку/выгрузку файлов.

Теперь мы знаем достаточно, чтобы добавить в ранее созданное приложение SimpeRussianRSSReader поддержу фоновой загрузки RSS.

Откройте последнюю версию приложения. Если у нас был получен RSS и сохранён в изолированное хранилище, обновление RSS автоматически не происходит.

Добавим запуск запроса на фоновое скачивание нового RSS файла, если у нас уже есть ранее скачанный.

Для начала в конструктор страницы добавим проверку, что в изолированном хранилище приложения присутствует специальная директория в которую можно скачивать файлы используя фоновые сервисы: /shared/transfers

using (IsolatedStorageFile rssStore = IsolatedStorageFile.GetUserStoreForApplication())
            {
                if (!rssStore.DirectoryExists("/shared/transfers"))
                {
                    rssStore.CreateDirectory("/shared/transfers");
                }
            }

Эта директория создаётся системой для приложения, но может быть им удалена.  Только файлы из этой директории можно загружать на сервер и только в эту директорию их можно выгружать.

В код функции LoadRSS, добавим код создания запроса на фоновую загрузку:

Uri transferUri = new Uri(RSS);
 
                BackgroundTransferRequest transferRequest = new BackgroundTransferRequest(transferUri);
                
                transferRequest.Method = "GET";
                
 
                Uri downloadUri = new Uri("shared/transfers/" + RSSFileName, UriKind.RelativeOrAbsolute);
                transferRequest.DownloadLocation = downloadUri;
 
                transferRequest.TransferStatusChanged += new EventHandler<BackgroundTransferEventArgs>(transferRequest_TransferStatusChanged);
 
                try
                {
                    BackgroundTransferService.Add(transferRequest);
                }
                catch (Exception ex)
                {
                
                    MessageBox.Show("Невозможно запустить фоновую загрузку:" + ex.Message);
                }

Мы берем URL   по которому отдаётся RSS, создаём URI, указываем имя файла в директории /shared/transfers в которую мы хотим скачать RSS методом GET, создаём загрузку, регистрируем обработчик события изменения статуса загрузки и добавляем запрос на загрузку с систему.

Теперь, добавим в обработчик изменения статуса код, чтобы удалить запрос на загрузку, скопировать файл в корневую директорию и обновить пользовательский интерфейс:

void transferRequest_TransferStatusChanged(object sender, BackgroundTransferEventArgs e)
        {
            //если скачивание прошло успешно - копируем файл и обновляем UI
            if (e.Request.TransferStatus == TransferStatus.Completed)
            {
                try
                {
                    BackgroundTransferService.Remove(e.Request);
                }
                catch (Exception ex)
                {
 
                    MessageBox.Show("Невозможно удалить завершённую фоновую загрузку:" + ex.Message);
                }
 
                using (IsolatedStorageFile rssStore = IsolatedStorageFile.GetUserStoreForApplication())
                {
                    if (rssStore.FileExists("/shared/transfers/" + RSSFileName))
                    {
                        rssStore.CopyFile("/shared/transfers/" + RSSFileName, RSSFileName, true);
                        
                        RSSString = LoadRSSFromIsolatedStorage();
                        ParseRSSAndBindData(RSSString);
                    }
                }
 
            }
        }

Чтобы протестировать приложение, добавим ещё одно закоментированное значение константы, которая используется в качестве URL для скачивания RSS:

//const string RSS = "https://blogs.msdn.com/b/rustudents/rss.aspx";

Закройте эмулятор, чтобы удалились ранее загруженные в него данные и запустите приложение (F5) в первый раз, чтобы RSS скатался в изолированное хранилище и отобразился в интерфейсе пользователя. После того, как отобразится список заголовков RSS, завершите работу приложения в Visual Studio (не закрывайте эмулятор).

Чтобы увидеть, что произошла фоновая загрузка, закомментируем оригиналньый URL и раскоментируем другой:

//const string RSS = "https://blogs.msdn.com/b/rudevnews/rss.aspx";
const string RSS = "https://blogs.msdn.com/b/rustudents/rss.aspx";

Запустите приложение (F5) подождите некоторое время – заголовки RSS обновятся соответствующим образом.

Фоновое проигрывание музыки

Также существует отдельный API для проигрывания локальной и потоковой музыки, находящийся в пространстве имён Microsoft.Phone.BackgroundAudio. Создание приложений, которые используют эту возможность аналогично созданию приложений для фоновых сервисов запускаемых по расписанию, с поправкой на  специфику проигрывания аудио.

В поставке средств разработки находятся два шаблона проектов Windows Audio Playback Agent и Windows Audio Streaming Agent для создания сервисов фонового проигрывания локальной и потоковой музыки соответственно.

Особенностью данных сервисов является их тесная интеграция с платформой Windows Phone.

Подробнее с архитектурой сервисов фонового проигрывания музыки можно познакомиться в документации MSDN: https://msdn.microsoft.com/ru-ru/library/hh394039(v=VS.92).aspx

По ссылке https://msdn.microsoft.com/ru-ru/library/hh202978(v=VS.92).aspx доступно пошаговое руководство по созданию простого приложения, проигрывающего локальные аудиофайлы в фоновом режиме.

Итоги и следующие шаги

В этой статье мы познакомились с жизненным циклом приложения, фоновыми сервисами и многозадачностью,  в следующей части мы познакомимся с уведомлениями и напоминаниями, а также с Live Tiles.

Файлы для загрузки

Проект ApplicationStateExample
Проект BackgroundAgentExample
Проект SimpleRussianRSSReader с Background File Transfer