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