Взяв под контроль жизненный цикл переменных — Marcus Technical Services

Недавно я боролся с приложением, которое просто не работало. Представления моделей казались гиперактивными. Когда я нажал на ДОМ кнопку, и приложение ушло в сон, оно часто возвращалось зависшим. Я копал и копал и не мог найти ничего явно неправильного. Поэтому я провел эксперимент, чтобы увидеть, что происходит, когда мы меняем страницы и их модели просмотра в простом приложении:

private void StartFirstTest() { TestCompleted += StartSecondTest; Device.BeginInvokeOnMainThread(async () => { var secondViewModel = SetUpTest(); var firstPage = new FirstPage { BindingContext = new FirstViewModel() }; Debug.WriteLine("About to assign the main page to the first page."); SetMainPage(firstPage); Debug.WriteLine("Finished assigning the main page to the first page."); await Task.Delay(5000); Debug.WriteLine("About to assign the main page to the second page."); SetMainPage(new SecondPage { BindingContext = secondViewModel }); secondViewModel.Message = "Working..."; Debug.WriteLine("Finished assigning the main page to the second page."); Debug.WriteLine("The first view model is now OUT OF SCOPE and should not be active."); }); }

В SetUpTest я только что создал модель представления, которая завершит тест вместе с таймером:

private SecondViewModel SetUpTest() { var secondViewModel = new SecondViewModel { TimeRemaining = TOTAL_BROADCAST_TIME }; _timer = new Timer(DELAY_BETWEEN_BROADCASTS); var timeToStop = DateTime.Now + TOTAL_BROADCAST_TIME; _timer.Elapsed += (sender, args) => { FormsMessengerUtils.Send(new TestPingMessage()); secondViewModel.TimeRemaining -= TimeSpan.FromMilliseconds(DELAY_BETWEEN_BROADCASTS); if (DateTime.Now >= timeToStop) { secondViewModel.Message = "FINISHED"; secondViewModel.TimeRemaining = TimeSpan.FromSeconds(0); Debug.WriteLine("Starting garbage collection"); GC.Collect(); Debug.WriteLine("Finished garbage collection"); _timer.Stop(); _timer.Dispose(); TestCompleted?.Invoke(); } }; _timer.Start(); return secondViewModel; }

Это намного проще, чем кажется:

  1. я создаю Фирствиевмодел который слушает глобальные сообщения. У него нет «крючков» для внешнего мира, за исключением того, что он BindingContext принадлежащий Первая страница.
  2. я установил приложение Главная страница к Первая страница и Фирствиевмодел.
  3. я меняю Главная страница к Вторая страница и его модель представления.

Вопрос в том, что происходит с первой моделью представления? Это просто: выходит за рамки, поэтому перестает работать («спит») пока не придет сборщик мусора и не уберет его. Верно?

_ Не совсем. _Вот что получилось:

// This was expected –- the first view model is still active The First View Model is still listening to events! The First View Model is still listening to events! The First View Model is still listening to events! The First View Model is still listening to events! The First View Model is still listening to events! About to assign the main page to the second page. Finished assigning the main page to the second page. The first view model is now OUT OF SCOPE and should not be active. // I do not want the view model doing anything at this point! The First View Model is still listening to events! The First View Model is still listening to events! The First View Model is still listening to events! The First View Model is still listening to events! The First View Model is still listening to events! The First View Model is still listening to events! The First View Model is still listening to events! The First View Model is still listening to events! The First View Model is still listening to events! The First View Model is still listening to events! The First View Model is still listening to events! The First View Model is still listening to events! The First View Model is still listening to events! The First View Model is still listening to events! The First View Model is still listening to events! The First View Model is still listening to events! The First View Model is still listening to events! The First View Model is still listening to events! The First View Model is still listening to events! The First View Model is still listening to events! The First View Model is still listening to events! The First View Model is still listening to events! The First View Model is still listening to events! The First View Model is still listening to events! The First View Model is still listening to events! // The only reason this dies is that I force-call GC.Collect(). // Otherwise it would have gone on forever. Starting garbage collection

FirstViewModel осиротел, но _ проснулся _ и в качестве _ голодный как младенец _. Сборщик мусора Xamarin.Forms находится в другой части города. Может быть, он зайдет позже; может быть завтра. Никто не знает.

Когда вы видите конец вывода, это не настоящий конец FirstViewModel. Я вручную убиваю его в конце временного теста, сам вызывая сборщик мусора. Как только GC прибывает, он отправляет FirstViewModel _ немедленно _.

Мы тратим много времени на написание исходного кода, чувствуя, что все, что мы делаем, интуитивно логично. Имеет смысл, что когда мы меняем страницы, все просто останавливается. Как еще могло бы работать приложение? Разве это не приведет к тому, что приложение станет нестабильным из-за гиперактивности и реакции? Не будет ли он зависать? Добро пожаловать в Xamarin.Forms.

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

SetMainPage(new SecondPage { BindingContext = secondViewModel }); firstPage.BindingContext = null; firstPage = null;

Всему нужно начало и конец

Любой класс, содержащий действия, должен содержать _ жизненный цикл _для безопасного запуска и остановки. В настоящее время сборщик мусора отвечает за «остановку» вещей, но он слишком ленив, чтобы быть надежным. Так оно и задумано.

Xamarin.Forms создал одно очевидное переопределение конца жизни на ContentPage, но его не существует _ в любом месте** еще**_.

Создание и подключение полного жизненного цикла

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

Вот несколько интерфейсов для создания чистых контрактов:

public interface IReportEndOfLifecycle { event EventUtils.GenericDelegate<object> IsDisappearing; } public interface IReportLifecycle : IReportEndOfLifecycle { event EventUtils.GenericDelegate<object> IsAppearing; }

Затем сам ContextPageWithLifecycle:

public interface IContentPageWithLifecycle : IReportLifecycle { } public class ContentPageWithLifecycle : ContentPage, IContentPageWithLifecycle { public event EventUtils.GenericDelegate<object> IsAppearing; public event EventUtils.GenericDelegate<object> IsDisappearing; protected override void OnAppearing() { base.OnAppearing(); IsAppearing?.Invoke(this); } protected override void OnDisappearing() { base.OnDisappearing(); IsDisappearing?.Invoke(this); this.SendObjectDisappearingMessage(); } }

ContentPage вызывает событие в каждом случае.

Есть два возможных потребителя этих событий:

  • ContentView, который обычно вложен в ContentPage.
  • Модель представления, обеспечивающая бизнес-логику для ContentView или самой страницы.

Интерфейсные контракты:

public interface IHostLifecycleReporter { IReportLifecycle LifecycleReporter { get; set; } } public interface ICanCleanUp { bool IsCleaningUp { get; set; } }

И ContentViewWithLifecycle в разделах для ясности:

public interface IHostLifecycleReporter { IReportLifecycle LifecycleReporter { get; set; } } public interface IContentViewWithLifecycle : IHostLifecycleReporter, IReportEndOfLifecycle, ICanCleanUp { } public class ContentViewWithLifecycle : ContentView, IContentViewWithLifecycle { public static BindableProperty PageLifecycleReporterProperty = CreateContentViewWithLifecycleBindableProperty ( nameof(LifecycleReporter), default(IReportLifecycle), BindingMode.OneWay, (contentView, oldVal, newVal) => { contentView.LifecycleReporter = newVal; } );

IReportСтиль жизни контракт означает, что мы должны поддерживать ссылку на любую сущность, которая предоставляет нам события появления и исчезновения страницы. Это легкая точка соприкосновения — всего несколько событий — но она нас немного «связывает». Это неизбежно.

public ContentViewWithLifecycle(IReportLifecycle lifeCycleReporter = null) { LifecycleReporter = lifeCycleReporter; }

Наш новый ContentView должен сообщать об окончании страницы другим, поэтому для этого предусмотрено событие:

private IReportLifecycle _lifecycleReporter; public ContentViewWithLifecycle(IReportLifecycle lifeCycleReporter = null) { LifecycleReporter = lifeCycleReporter; } public IReportLifecycle LifecycleReporter { get => _lifecycleReporter; set { _lifecycleReporter = value; if (_lifecycleReporter != null) { this.SetAnyHandler ( handler => _lifecycleReporter.IsDisappearing += OnDisappearing, handler => _lifecycleReporter.IsDisappearing -= OnDisappearing, (lifecycle, args) => { } ); } } }

В приведенном выше примере мы слушаем с слабые события чтобы уменьшить перетаскивание, создаваемое ссылкой на нашу «родительскую» ContentPage.

Наконец, мы добавляем IsCleaningUp Логическое значение, чтобы пометить этот класс, чтобы остановить его действия при подготовке к сборщику мусора.

~ContentViewWithLifecycle() { if (!IsCleaningUp) { IsCleaningUp = true; } } private bool _isCleaningUp; public bool IsCleaningUp { get => _isCleaningUp; set { if (_isCleaningUp != value) { _isCleaningUp = value; if (_isCleaningUp) { // Notifies the safe di container and other concerned foreign members this.SendObjectDisappearingMessage(); // Notifies close relatives like view models IsDisappearing?.Invoke(this); } } } } protected virtual void OnDisappearing(object val) { IsCleaningUp = true; }

В приведенном выше коде я добавил финализатор («деструктор») — ~ContentViewWithLifecycle() — на всякий случай IsCleaningUp не вызывается перед сборкой мусора. Это необязательная крайняя мера предосторожности.

ViewModelWithLifecyle здесь не показано, потому что это то же самое, что и ContentView за исключением того, что это «конец линии», поэтому он не обязан уведомлять нижестоящие классы о закрытии.

Вот как эти три класса взаимодействуют во время выполнения:

Потребление и управление жизненным циклом

в ContentView а также ViewModel когда бы ни IsCleaningUp установлено значение true, мы останавливаем все действия _ немедленно: _

public class FirstViewModelWithLifecycle : ViewModelWithLifecycle, IFirstViewModelWithLifecycle { public FirstViewModelWithLifecycle() { Debug.WriteLine("The first view model with lifecycle is being created."); FormsMessengerUtils.Subscribe<TestPingMessage>(this, OnTestPing); } private void OnTestPing(object sender, TestPingMessage args) { if (!IsCleaningUp) { Debug.WriteLine("The first view model with lifecycle is still listening to events!"); } } ~FirstViewModelWithLifecycle() { FormsMessengerUtils.Unsubscribe<TestPingMessage>(this); Debug.WriteLine("The first view model with lifecycle is FINALIZED."); } }

Нам также нужно «связать» эти классы, чтобы они знали друг о друге. Вот расширенное тестовое приложение:

var firstViewModelWithLifecycle = new FirstViewModelWithLifecycle(); var firstPageWithLifecycle = new FirstPageWithLifecycle { BindingContext = firstViewModelWithLifecycle }; firstViewModelWithLifecycle.LifecycleReporter = firstPageWithLifecycle; Debug.WriteLine("About to assign the main page to the first page with Lifecycle."); SetMainPage(firstPageWithLifecycle); Debug.WriteLine("Finished assigning the main page to the first page with Lifecycle."); await Task.Delay(5000); Debug.WriteLine("About to assign the main page to the second page."); SetMainPage(new SecondPage { BindingContext = secondViewModel }); secondViewModel.Message = "Working..."; Debug.WriteLine("Finished assigning the main page to the second page."); Debug.WriteLine("The first view model with Lifecycle is now OUT OF SCOPE and should not be active.");

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

firstViewModelWithLifecycle.LifecycleReporter = firstPageWithLifecycle;

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

Барабанная дробь, пожалуйста!

The first view model with lifecycle is being created. About to assign the main page to the first page with Lifecycle. Finished assigning the main page to the first page with Lifecycle. The first view model with lifecycle is still listening to events! The first view model with lifecycle is still listening to events! The first view model with lifecycle is still listening to events! The first view model with lifecycle is still listening to events! The first view model with lifecycle is still listening to events! About to assign the main page to the second page. Finished assigning the main page to the second page. The first view model with Lifecycle is now OUT OF SCOPE and should not be active. // SUCCESS !

Жесткие доказательства

Я создал мобильное приложение Xamarin.Forms, чтобы продемонстрировать исходный код в этой статье. Исходник доступен на GitHub по адресу Решение называется LifecycleAware.sln. Модульные тесты и полный рабочий пример см. SmartDIWithLifeCycle.sln.

Код публикуется с открытым исходным кодом и без обременений.

Похожие записи

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

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