Интеграция Xamarin.Forms в существующие проекты iOS (и утечки памяти)

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

Типичным способом интеграции Xamarin.Forms в существующее приложение будет создание кроссплатформенных страниц (Page/ContentPage etc) и генерации на их основе платформозависимых UIViewController (пример будет основан на iOS). Например, простейшая кроссплатформенная страница может выглядеть так:

public Page CreateFormsPage() 
{
	var page = new ContentPage () 
        {
		BackgroundColor = Color.Yellow,
	};
	page.Content = new Button () 
       {
		Text = "Test Button"
	};

	return page;
}

И если мы хотим показать эту страницу на каком-то уже существующем экране, то это может выглядеть примерно так (кусок метода UIViewController.ViewDidLoad):

public override void ViewDidLoad ()
{
	base.ViewDidLoad ();

	var page = CreateFormsPage();
	var vc = page.CreateViewController();
	var view = vc.View;
	view.Frame = new RectangleF(100,150, 200,200);
	View.AddSubview(view);	
}

Всё замечательно и удобно, если бы не одно «но». При подобном использовании легко получить утечки памяти.
Классическое использование Xamarin.Forms предполагает, что вы полностью завязываетесь на его инфраструктуру, и вся навигация по вашему приложению происходит с помощью методов Page.Push/PushAsync. В этом случае, конечно, никаких утечек памяти не будет.

В случае же такого «нестандартного» применения, как встраивание в собственные экраны, придется освобождать память в ручную. К сожалению, удобных методов вроде Dispose у классов Page нет, и в принципе API под «ручное» разрушение страниц не адаптировано. Пришлось воспользоваться магией рефлексии и написать extension-метод Page.DisposePage(). Код приведен ниже, смело копируйте его в свой проект, избавляйтесь от утечек памяти и радуйтесь удобству работы с Xamarin.Forms в ваших текущих проектах!

public static class XamarinFormsExtensions
{
	public static void DisposePage(this Page page) 
	{
		DisposeViewController (page);

		RemovePageFromMessagingCenter (page);

		page.OnDescendantRemoved ();
	}

	private static MethodInfo _onDescendantRemovedMethod = typeof(Element).GetMethod ("OnDescendantRemoved", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public);
	private static void OnDescendantRemoved (this Page page)
	{
		_onDescendantRemovedMethod.Invoke (page, new[] {page});
	}

	private static FieldInfo _callbacksField = typeof(MessagingCenter).GetField("callbacks", BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Public);
	private static Dictionary<Tuple<string, Type, Type>, List<Tuple<WeakReference, Action<object, object>>>> Callbacks {
		get 
		{
			return (Dictionary<Tuple<string, Type, Type>, List<Tuple<WeakReference, Action<object, object>>>>)_callbacksField.GetValue (null);
		}
	}

	private static PropertyInfo _platformProperty = typeof(Element).GetProperty("Platform", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public);
	private static IPlatform GetPlatform(this Page page) 
	{
		return (IPlatform)_platformProperty.GetValue (page);
	}

	private static FieldInfo _rendererField;
	private static UIViewController GetViewController(this IPlatform platform) 
	{
		if (_rendererField == null) 
		{
			_rendererField = platform.GetType ().GetField ("renderer", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public);
		}
		return (UIViewController)_rendererField.GetValue (platform);
	}

	private static void DisposeViewController (Page page)
	{
		var platform = page.GetPlatform ();

		if (platform == null)
			return;

		var viewController = platform.GetViewController();

		if (viewController != null) 
		{
			viewController.View.RemoveFromSuperview ();
			viewController.Dispose ();
		}
	}

	private static void RemovePageFromMessagingCenter (Page page)
	{
		var platform = page.GetPlatform ();

		if (platform == null)
			return;

		foreach (var subscriptions in Callbacks.Values) 
		{
			subscriptions.RemoveAll (x => x.Item1.IsAlive && x.Item1.Target == platform);
		}
	}
}
Опубликовать в Facebook
Опубликовать в Google Plus

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *