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);
}
}
}