Пишем код

Заметки о .net разработке

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

without comments

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

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

<br />
public Page CreateFormsPage()<br />
{<br />
	var page = new ContentPage ()<br />
        {<br />
		BackgroundColor = Color.Yellow,<br />
	};<br />
	page.Content = new Button ()<br />
       {<br />
		Text = "Test Button"<br />
	};</p>
<p>	return page;<br />
}<br />

И если мы хотим показать эту страницу на каком-то уже существующем экране, то это может выглядеть примерно так (кусок метода UIViewController.ViewDidLoad):
<br />
public override void ViewDidLoad ()<br />
{<br />
	base.ViewDidLoad ();</p>
<p>	var page = CreateFormsPage();<br />
	var vc = page.CreateViewController();<br />
	var view = vc.View;<br />
	view.Frame = new RectangleF(100,150, 200,200);<br />
	View.AddSubview(view);<br />
}<br />

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

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

<br />
public static class XamarinFormsExtensions<br />
{<br />
	public static void DisposePage(this Page page)<br />
	{<br />
		DisposeViewController (page);</p>
<p>		RemovePageFromMessagingCenter (page);</p>
<p>		page.OnDescendantRemoved ();<br />
	}</p>
<p>	private static MethodInfo _onDescendantRemovedMethod = typeof(Element).GetMethod ("OnDescendantRemoved", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public);<br />
	private static void OnDescendantRemoved (this Page page)<br />
	{<br />
		_onDescendantRemovedMethod.Invoke (page, new[] {page});<br />
	}</p>
<p>	private static FieldInfo _callbacksField = typeof(MessagingCenter).GetField("callbacks", BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Public);<br />
	private static Dictionary<Tuple<string, Type, Type>, List<Tuple<WeakReference, Action<object, object>>>> Callbacks {<br />
		get<br />
		{<br />
			return (Dictionary<Tuple<string, Type, Type>, List<Tuple<WeakReference, Action<object, object>>>>)_callbacksField.GetValue (null);<br />
		}<br />
	}</p>
<p>	private static PropertyInfo _platformProperty = typeof(Element).GetProperty("Platform", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public);<br />
	private static IPlatform GetPlatform(this Page page)<br />
	{<br />
		return (IPlatform)_platformProperty.GetValue (page);<br />
	}</p>
<p>	private static FieldInfo _rendererField;<br />
	private static UIViewController GetViewController(this IPlatform platform)<br />
	{<br />
		if (_rendererField == null)<br />
		{<br />
			_rendererField = platform.GetType ().GetField ("renderer", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public);<br />
		}<br />
		return (UIViewController)_rendererField.GetValue (platform);<br />
	}</p>
<p>	private static void DisposeViewController (Page page)<br />
	{<br />
		var platform = page.GetPlatform ();</p>
<p>		if (platform == null)<br />
			return;</p>
<p>		var viewController = platform.GetViewController();</p>
<p>		if (viewController != null)<br />
		{<br />
			viewController.View.RemoveFromSuperview ();<br />
			viewController.Dispose ();<br />
		}<br />
	}</p>
<p>	private static void RemovePageFromMessagingCenter (Page page)<br />
	{<br />
		var platform = page.GetPlatform ();</p>
<p>		if (platform == null)<br />
			return;</p>
<p>		foreach (var subscriptions in Callbacks.Values)<br />
		{<br />
			subscriptions.RemoveAll (x => x.Item1.IsAlive && x.Item1.Target == platform);<br />
		}<br />
	}<br />
}<br />

Опубликовать в Facebook
Опубликовать в Google Plus

Written by Shaddix

Ноябрь 18th, 2014 at 11:00 дп

Posted in .net,xamarin

Leave a Reply