Компания
43,50
рейтинг
5 сентября 2013 в 14:05

Разработка → C# async для iOS и Android перевод

Xamarin добавил поддержку C# 5 Async/await на iOS и Android. Кроме базовых классов .NET Async, появились 174 асинхронных метода в Xamarin.iOS и 337 в Xamarin.Android. Асинхронным так же стал Xamarin Mobile, который предоставляет кроссплатформенный доступ к адресной книге, камере и геолокации. Компоненты вовсю добавляют поддержку async, например, облачный backend Parse.

Под катом расшифровка и перевод вебинара об этом значимом событии.




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

Отзывчивый интерфейс реагирует на действия пользователя не дольше 64 миллисекунд, добиться этого не так просто. Особенно если реакция на действия пользователя требует выполнение долгих операций. Самый простой вариант — запустить долгую операцию в другом потоке. А если вы используете потоки, вам нужно придумывать свою систему для координации их действий. Есть много разных подходов и литературы по этому поводу, но каждый раз вам нужно реализовывать ее заново.

Почти любая операция на смартфоне может быть долгой: работа с файловой системой, масштабирование картинки, запрос к базе данных. А когда вы скачиваете что-нибудь по сети, вы вообще не можете предсказать сколько времени это займет. И чтобы отвечать пользователю не позже чем за 64 миллисекунды мы придумали колл-беки. Они сейчас очень популярны с подачи NodeJS.

Если использовать много колл-беков, код становится нечитаемым и сложно модифицируемым, это называется Callback Hell.



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

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


Эдсгер Дейкстра
Наш мозг заточен для понимания статических структур, наши возможности представлять процессы развивающиеся во времени развиты плохо. Поэтому мы должны сократить разницу между кодом программы и процессом ее выполнения, чтобы понять как будет выполняться программа по ее коду было как можно проще.
Он имел ввиду использование GOTO, с той же самой проблемой мы столкнулись при использовании коллбеков. Но прогресс не стоит на месте, мы создаем новые идиомы, новые языки, учим компилятор делать работу за нас. И async/await в C# это и есть новая идиома которая может придти на смену коллбекам.

async Task SnapAndPostAsync()
{
    try {
        Busy = true;
        UpdateUIStatus ("Taking a picture");
        var picker = new Xamarin.Media.MediaPicker ();
        var mFile = await picker.TakePhotoAsync (new Xamarin.Media.StoreCameraMediaOptions ());
        var tagsCtrl = new GetTagsUIViewontroller (mFile.GetStream ());
        // Call new iOS await API
        await PresentViewControllerAsync (tagsCtrl, true);
        UpdateUIStatus ("Submitting picture to server");
        await PostPicToServiceAsync (mFile.GetStream (), tagsCtrl.Tags);
        UpdateUIStatus ("Success");
    } catch (OperationCanceledException) {
        UpdateUIStatus ("Canceled");
    } finally {
        Busy = false;
    }
}

Вот пример использования async/await. Этот код выглядит последовательным и понятным, добавление новых функций не составляет труда. А вся разница в том, что я добавил слово async в заголовок функции, что помечает метод как асинхронный и потом использовал await. Эти ключевые слова говорят компилятору: «пожалуйста, сделай работу за разработчика, перепиши этот код, раздели его на несколько независимых кусочков». Фактически на выходе получатся те же самые коллбеки, тот же контроль состояния, только сделанные автоматически. А вы можете читать код последовательно, как и завещал Дейкстра.

В примере мы использовали тип Task<T>. Он помогает нам управлять операциями проходящими в фоне. Task инкапсулирует статус задачи (в процессе, завершена, отменена), результат и исключения, возникшие в процессе выполнения. Есть два типа Task: не возвращающий значение и возвращающий значение типа T.



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

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

Давайте рассмотрим еще один пример. Мне он очень нравится своей последовательностью. Этот метод скачивает файл, если он уже есть на диске мы спрашиваем пользователь переписать файл или нет, если он скажет «да» — перезапишем файл, иначе прекращаем работу.

async Task DownloadFile (Uri uri, string target)
{
    if (File.Exists (target)){
        switch (await ShowAlert ("File Exists", "Do you want to overwrite the file?", "yes", "no")){
        case 0:
            break;
        case 1:
            return;
        }
    }
    var stream = await Http.Get (uri);
    await stream.SaveTo(target);
}


Как вы помните в iOS и Android результат системного сообщения нам приходит в коллбеке, то есть без async нам нужно было создать сообщение, сконфигурировать его, назначить коллбек. В случае с async компилятор делает все это за нас. И это прекрасно.

Давайте посмотрим как реализован ShowAlert внутри Xamarin.
public static Task<int> ShowAlert(string title, string message, params string [] buttons)
{
    var tcs = new TaskCompletionSource<int> ();
    var alert = new UIAlertView {
            Title = title,
            Message = message
    };
    foreach (var button in buttons)
            alert.AddButton (button);
    alert.Clicked += (s, e) => tcs.TrySetResult (e.ButtonIndex);
    alert.Show ();
    return tcs.Task;
}

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

Как я уже упоминал Xamarin поддерживает базовые классы .NET5 Async и специализированные методы для мобильных платформ. Если быть точным Xamarin.iOS предоставляет 174 асинхронных метода, а Xamarin.Android — 337. Кроме этого асинхронным стал Xamarin Mobile, который предоставляет кроссплатформенный доступ к адресной книге, камере и геолокации. Многие компоненты так же становятся асинхронными, например, облачный backend Parse.

Если вызов метода API может занять больше 50 миллисекунд, мы делаем его асинхронную версию. В зависимости от контекста, вы можете использовать либо синхронную либо асинхронную версию метода.

Поддержка async/await именно та ситуация, когда Xamarin существенно упрощает работу с iOS и Android платформами и перестает быть просто оберткой.

(прим. пер. начинается часть Craig Dunn, похоже, он пересказывает Task-based Asynchronous Pattern)

Посмотрим на более крупные примеры. Весь код доступен на Github.

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

public void DownloadHomepage()
{
	var webClient = new WebClient();

	webClient.DownloadStringCompleted += (sender, e) => {
		if(e.Cancelled || e.Error != null) {
			// do something with error
		}
		string contents = e.Result;

		int length = contents.Length;
		InvokeOnMainThread (() => {
			ResultTextView.Text += "Downloaded the html and found out the length.\n\n";
		});
		webClient.DownloadDataCompleted += (sender1, e1) => {
			if(e1.Cancelled || e1.Error != null) {
				// do something with error
			}
			SaveBytesToFile(e1.Result, "team.jpg");

			InvokeOnMainThread (() => {
				ResultTextView.Text += "Downloaded the image.\n";
				DownloadedImageView.Image = UIImage.FromFile (localPath);
			});

			ALAssetsLibrary library = new ALAssetsLibrary();     
			var dict = new NSDictionary();
			library.WriteImageToSavedPhotosAlbum (DownloadedImageView.Image.CGImage, dict, (s2,e2) => {
				InvokeOnMainThread (() => {
					ResultTextView.Text += "Saved to album assetUrl\n";
				});
				if (downloaded != null)
					downloaded(length);
			});
		};
		webClient.DownloadDataAsync(new Uri("http://xamarin.com/images/about/team.jpg"));
	};

	webClient.DownloadStringAsync(new Uri("http://xamarin.com/"));
}


То же самое с использованием async/await:

public async Task<int> DownloadHomepageAsync()
{
	try {
		var httpClient = new HttpClient();

		Task<string> contentsTask = httpClient.GetStringAsync("http://xamarin.com"); 

		string contents = await contentsTask;

		int length = contents.Length;
		ResultTextView.Text += "Downloaded the html and found out the length.\n\n";

		byte[] imageBytes  = await httpClient.GetByteArrayAsync("http://xamarin.com/images/about/team.jpg"); 
		SaveBytesToFile(imageBytes, "team.jpg");
		ResultTextView.Text += "Downloaded the image.\n";
		DownloadedImageView.Image = UIImage.FromFile (localPath);

		ALAssetsLibrary library = new ALAssetsLibrary();     
		var dict = new NSDictionary();
		var assetUrl = await library.WriteImageToSavedPhotosAlbumAsync 
											(DownloadedImageView.Image.CGImage, dict);
		ResultTextView.Text += "Saved to album assetUrl = " + assetUrl + "\n";

		ResultTextView.Text += "\n\n\n" + contents; // just dump the entire HTML
		return length; 
	} catch {
		// do something with error
		return -1;
	}
}


Сравнили? Новый код линеен, прост для чтения, обработка ошибок собрана в одном месте.

Так же вы можете заметить HttpClient, новый объект API доступный в Xamarin. В коде мы пытаемся скачать html главной страницы xamarin.com с его помощью. После вызова GetStringAsync, сразу же запускается параллельный поток, который скачивает html с сайта. И возвращает нам Task, ссылку на эту операцию. Когда мы вызываем await, мы говорим, что не можем продолжать без результата этой операции, управление передается в главный процесс. Когда строка получена, управление возвращается в наш метод на следующую строчку после await, мы высчитываем ее длину и обновляем пользовательский интерфейс.

Когда мы вызываем httpClient.GetByteArrayAsync, мы не создаем промежуточного Task, а сразу ждем результата запроса. Скачивание картинки произойдет в другом фоновом процессе и когда он закончится управление вернется в наш метод, мы сохраним картинку и обновим пользовательский интерфейс.

Код последователен и понятен.

Я не делал никакой обработки ошибок, но это несложно. Неважно произойдет ошибка в коде метода или в фоновом потоке HttpClient мы получим ее в блоке catch. Компилятор делает эту работу за нас.

Я пометил разные операции в коде цветами, давайте сравним старую и новую версии:


Обратите внимание как непоследователен старый код, как по всему коду размазана обработка ошибок и обращение к главному потоку. Версия с async/await гораздо лучше с точки зрения читаемости и поддержки кода.

Переходим на async/await


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


Async методы могут возвращать void, Task или Task<T>. Void применим только для обработчиков событий. Если ваш метод возвращает значение используйте Task<T>, иначе — просто Task. Если вы используйте Task<T> просто возвращайте значение как из обычного метода, компилятор сделает остальное.

Await можно использовать только в методах помеченных как async. Await нельзя использовать в Catch и Finally блоках.

Обработка ошибок


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

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

try {
    //исключения произошедшие в t будут переброшены наверх
    await t;
    ResultTextView.Text += "** Downloaded " + t.Result.Length + " bytes\n";
}
catch(OperationCancelledException) {//если t был отменен}
catch(Exception exc) {
    //поймает в том числе исключения в t
    ResultTextView.Text += "--Download Error: " + exc.Messsage + "\n";
}


Отмена задач


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

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

var cts = new CancellationTokenSource();
var intResult = await DownloadHomepageAsync(cts.Token);
//позже в приложении или даже в другом потоке
cts.Cancel();


Отображение прогресса


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

public class Progress<T> : IProgress<T>
{
    public Progress;
    public Progress(Action<T> handler);
    protected virtual void OnReport(T value);
    public event EventHandler<T> ProgressChanged; //событие вызывается при обновлении прогресса задачей
}


Комбинации задач


Несколько задач можно объединить в одну методами Task.WhenAny и Task.WhenAll. Например, вы скачиваете несколько файлов, объединив задачи по скачиванию файлов в одну, вы можете дождаться пока все они будут загружены или продолжить выполнение после первого загруженного файла.

while(tasks.Count > 0) {
    //создаем задачу, которая завершится когда любая из задач будет завершена
    //t — первая выполненная задача
    var t = await Task.WhenAny(tasks);
    tasks.Remove(t);
    try {
        await t; //получаем результат выполнения t и исключения
        ResultTextView.Text += "++ Downloaded " + t.Result.length + " bytes\n";
    }
   catch(OperationCancelledException) {}
   catch(Exception exc) {
        //исключения сработавшие во время выполнения t
        ResultTextView.text += "-- Download Error: " + exc.Message + "\n";
   }
}


В этом примере мы скачиваем три картинки с сайта, ссылка на одну из них вызовет 403 ошибку. Итак, добавим все задачи в список tasks и будем обрабатывать их в цикле пока список не опустеет. Создаем составную задачу через Task.WhenAny и ожидаем ее завершения, она завершится когда любая из вложенных задач выполнится и вернет нам ее в t. Удалим выполненную задачу из списка и выполним wait t, чтобы получить результат задачи. Исключения сработавшие в процессе выполнения t всплывут после вызова await. Комбинации задач мощный инструмент для однотипных операций, присмотритесь к нему.

Подробнее о C# async в документации Microsoft, о поддержке API iOS и Android на сайте Xamarin. Примеры приложений в репозитории к вебинару.


Подписывайтесь на наш хабра-блог (кнопка справа вверху). Каждый четверг полезные статьи о мобильной разработке, маркетинге и бизнесе мобильной студии. Следующая статья (12 сентября) «Как устроены продажи в мобильной студии»: исповедь нашего продажника о процессах, отчетах, мегаплане, самописной CRM и пути его профессионального роста.
Автор: @junk Miguel de Icaza, Craig Dunn
Touch Instinct
рейтинг 43,50
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Похожие публикации

Комментарии (4)

  • +2
    На самом деле от callback hell под iOS можно спастись в помощью ReactiveCocoa. Конечно такой сахар как async/await все равно не получится, но хотя бы что-то.

    Под Java (и соответственно Android) есть подобный проект RxJava.
    • +2
      Используем ReactiveCocoa в продакшене, порог входа местами высоковат, но код выглядит местами (не везде) на порядок чище.
  • +2
    Статья неплохая, но некоторые места откровенно косячные. Например такие:

    «Async методы могут возвращать void, Task или Task. Void применим только для обработчиков событий. Если ваш метод возвращает значение используйте Task, иначе — просто Task. Если вы используйте Task просто возвращайте значение как из обычного метода, компилятор сделает остальное.»

    В целом твердая «4», думаю.
    • 0
      о, Хабр съел <T>, поправил, спасибо!

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

Самое читаемое Разработка