Повышение производительности приложения

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

Существует множество методов повышения производительности и предполагаемой производительности приложений .NET Multi-platform App UI (.NET MAUI). В совокупности эти методы могут значительно сократить объем работы, выполняемой ЦП, и объем памяти, потребляемой приложением.

Использование профилировщика

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

Приложения .NET MAUI можно профилировать с помощью dotnet-trace Android, iOS и Mac, а также Windows и PerfView в Windows. Дополнительные сведения см. в разделе "Профилирование приложений .NET MAUI".

При профилировании приложения следуйте представленным ниже рекомендациям:

  • Избегайте профилирования приложения в симуляторе, так как симулятор может исказить производительность приложения.
  • В идеале профилирование следует проводить на различных устройствах, так как показатели производительности, полученные на одном устройстве, не всегда отражают аналогичные показатели на других устройствах. По крайней мере профилирование следует провести на устройстве с минимальными требуемыми характеристиками.
  • Закройте все остальные приложения, чтобы убедиться, что все влияние профилируемого приложения измеряется, а не другие приложения.

Использование скомпилированных привязок

Скомпилированные привязки повышают производительность привязки данных в приложениях .NET MAUI путем разрешения выражений привязки во время компиляции, а не во время выполнения с отражением. При компиляции выражения привязки создается скомпилированный код, который обычно разрешает привязку в 8–20 раз быстрее, чем при использовании классической привязки. Дополнительные сведения см. в разделе "Скомпилированные привязки".

Сокращение количества ненужных привязок

Не используйте привязки для содержимого, которое можно легко задать статически. В привязке данных, которые не должны быть привязаны, нет никакой выгоды, так как привязки нерентабельны с точки зрения затрат. Например, установка Button.Text = "Accept" связана с меньшими издержками, чем привязка Button.Text к свойству string компонента ViewModel со значением Accept.

Выбор подходящего макета

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

<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="MyMauiApp.MainPage">
    <VerticalStackLayout>
        <Image Source="waterfront.jpg" />
    </VerticalStackLayout>
</ContentPage>

Это расточительно, и VerticalStackLayout элемент должен быть удален, как показано в следующем примере:

<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="MyMauiApp.MainPage">
    <Image Source="waterfront.jpg" />
</ContentPage>

Кроме того, не пытайтесь воспроизвести внешний вид определенного макета с помощью сочетаний других макетов, так как это приводит к выполнению ненужных вычислений макета. Например, не пытайтесь воспроизвести Grid макет с помощью сочетания HorizontalStackLayout элементов. В следующем примере показан пример этой плохой практики:

<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="MyMauiApp.MainPage">
    <VerticalStackLayout>
        <HorizontalStackLayout>
            <Label Text="Name:" />
            <Entry Placeholder="Enter your name" />
        </HorizontalStackLayout>
        <HorizontalStackLayout>
            <Label Text="Age:" />
            <Entry Placeholder="Enter your age" />
        </HorizontalStackLayout>
        <HorizontalStackLayout>
            <Label Text="Occupation:" />
            <Entry Placeholder="Enter your occupation" />
        </HorizontalStackLayout>
        <HorizontalStackLayout>
            <Label Text="Address:" />
            <Entry Placeholder="Enter your address" />
        </HorizontalStackLayout>
    </VerticalStackLayout>
</ContentPage>

Такой метод неэффективен, так как выполняются ненужные вычисления макета. Вместо этого нужный макет можно лучше достичь с помощью Grid, как показано в следующем примере:

<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="MyMauiApp.MainPage">
    <Grid ColumnDefinitions="100,*"
          RowDefinitions="30,30,30,30">
        <Label Text="Name:" />
        <Entry Grid.Column="1"
               Placeholder="Enter your name" />
        <Label Grid.Row="1"
               Text="Age:" />
        <Entry Grid.Row="1"
               Grid.Column="1"
               Placeholder="Enter your age" />
        <Label Grid.Row="2"
               Text="Occupation:" />
        <Entry Grid.Row="2"
               Grid.Column="1"
               Placeholder="Enter your occupation" />
        <Label Grid.Row="3"
               Text="Address:" />
        <Entry Grid.Row="3"
               Grid.Column="1"
               Placeholder="Enter your address" />
    </Grid>
</ContentPage>

Оптимизация графических ресурсов

Изображения являются одними из самых дорогих ресурсов, используемых приложениями, и часто фиксируются при высоких разрешениях. Хотя это создает яркие изображения, полные подробностей, приложения, отображающие такие изображения, обычно требуют больше использования ЦП для декодирования образа и больше памяти для хранения декодированного изображения. Декодировать изображение в высоком разрешении в памяти, если затем при отображении его размер будет уменьшен, бессмысленно. Вместо этого уменьшите потребление ЦП и объем памяти, создавая версии сохраненных образов, близкие к прогнозируемым размерам дисплея. Например, изображение, отображаемое в представлении списка, скорее всего, будет иметь меньшее разрешение, чем изображение, отображаемое на весь экран.

Кроме того, изображения должны создаваться только в том случае, если это необходимо, и их следует освободить, как только приложение больше не требует их. Например, если приложение отображает изображение, считывая данные из потока, убедитесь, что поток создается только в том случае, если это необходимо, и убедитесь, что поток освобождается, когда он больше не требуется. Это можно сделать, создав поток при создании страницы или при возникновении события Page.Appearing, а затем удалив его при возникновении события Page.Disappearing.

При скачивании изображения для отображения с ImageSource.FromUri(Uri) помощью метода убедитесь, что скачанный образ кэшируется в течение подходящего периода времени. Дополнительные сведения см. в разделе "Кэширование изображений".

Уменьшение размера визуального дерева

Сокращение количества элементов на странице способствует ускорению отрисовки страницы. Существует два основных способа решения этой задачи. Первый заключается в скрытии элементов, которые не отображаются. Свойство IsVisible каждого элемента определяет, должен ли элемент быть частью визуального дерева. Таким образом, если элемент не отображается, поскольку он скрыт другими элементами, удалите элемент или задайте его свойству IsVisible значение false.

Второй метод предполагает удаление ненужных элементов. Например, в следующем примере показан макет страницы, содержащий несколько Label элементов:

<VerticalStackLayout>
    <VerticalStackLayout Padding="20,20,0,0">
        <Label Text="Hello" />
    </VerticalStackLayout>
    <VerticalStackLayout Padding="20,20,0,0">
        <Label Text="Welcome to the App!" />
    </VerticalStackLayout>
    <VerticalStackLayout Padding="20,20,0,0">
        <Label Text="Downloading Data..." />
    </VerticalStackLayout>
</VerticalStackLayout>

Тот же макет страницы можно поддерживать с уменьшенным числом элементов, как показано в следующем примере:

<VerticalStackLayout Padding="20,35,20,20"
                     Spacing="25">
    <Label Text="Hello" />
    <Label Text="Welcome to the App!" />
    <Label Text="Downloading Data..." />
</VerticalStackLayout>

Уменьшение размера словаря ресурсов приложения

Все ресурсы, используемые во всем приложении, должны храниться в словаре ресурсов приложения, чтобы избежать дублирования. Это поможет уменьшить объем XAML, который необходимо проанализировать во всем приложении. В следующем примере показан HeadingLabelStyle ресурс, который широко используется приложением и поэтому определен в словаре ресурсов приложения:

<Application xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="MyMauiApp.App">
     <Application.Resources>
        <Style x:Key="HeadingLabelStyle"
               TargetType="Label">
            <Setter Property="HorizontalOptions"
                    Value="Center" />
            <Setter Property="FontSize"
                    Value="Large" />
            <Setter Property="TextColor"
                    Value="Red" />
        </Style>
     </Application.Resources>
</Application>

Однако XAML, относящееся к странице, не должно быть включено в словарь ресурсов приложения, так как ресурсы затем будут проанализированы при запуске приложения, а не при необходимости на странице. Если ресурс используется страницей, которая не является начальной страницей, она должна быть помещена в словарь ресурсов для этой страницы, поэтому помогает уменьшить анализ XAML, который анализируется при запуске приложения. В следующем примере показан HeadingLabelStyle ресурс, который находится только на одной странице и поэтому определен в словаре ресурсов страницы:

<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="MyMauiApp.MainPage">
    <ContentPage.Resources>
        <Style x:Key="HeadingLabelStyle"
                TargetType="Label">
            <Setter Property="HorizontalOptions"
                    Value="Center" />
            <Setter Property="FontSize"
                    Value="Large" />
            <Setter Property="TextColor"
                    Value="Red" />
        </Style>
    </ContentPage.Resources>
    ...
</ContentPage>

Дополнительные сведения о ресурсах приложений см. в статье "Стили приложений с помощью XAML".

Уменьшение размера приложения

Когда .NET MAUI создает приложение, компоновщик с именем ILLink можно использовать для уменьшения общего размера приложения. ILLink уменьшает размер, анализируя промежуточный код, созданный компилятором. Он удаляет неиспользуемые методы, свойства, поля, события, структуры и классы для создания приложения, содержащего только зависимости кода и сборки, необходимые для запуска приложения.

Дополнительные сведения о настройке поведения компоновщика см. в статье "Связывание приложения Android", "Связывание приложения iOS" и "Связывание приложения Mac Catalyst".

Сокращение периода активации приложения

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

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

В течение периода активации приложения выполняют логику активации, которая часто включает загрузку и обработку ресурсов. Период активации можно сократить, упаковав необходимые ресурсы в приложение, а не извлекая их из удаленного источника. Например, в некоторых ситуациях во время периода активации может быть рациональнее загружать локально хранящиеся подстановочные данные. Затем, после того как отобразится начальный пользовательский интерфейс и пользователь сможет начать работать с приложением, подстановочные данные можно постепенно заменять данными из удаленного источника. Кроме того, логика активации приложения должна выполнять только работу, необходимую для того, чтобы пользователь начал использовать приложение. Это может помочь в случае удаленной загрузки дополнительных сборок, когда сборки загружаются при первом использовании.

Тщательный выбор контейнера внедрения зависимостей

Контейнеры внедрения зависимостей вводят дополнительные ограничения производительности в мобильные приложения. Регистрация и разрешение типов в контейнере влечет затраты с точки зрения производительности из-за использования отражения в контейнере для создания каждого типа, особенно если зависимости перестраиваются при каждом переходе по страницам в приложении. При наличии большого числа зависимостей или глубоких зависимостей стоимость создания может значительно возрасти. Кроме того, регистрация типов, которая обычно возникает во время запуска приложения, может оказать заметное влияние на время запуска, зависят от используемого контейнера. Дополнительные сведения о внедрении зависимостей в приложениях .NET MAUI см. в статье об внедрении зависимостей.

В качестве альтернативы можно сделать внедрение зависимостей более производительным, реализовав его вручную с помощью фабрик.

Создание приложений Оболочки

Приложения оболочки .NET MAUI предоставляют возможность навигации с учетом всплывающих меню и вкладок. Если взаимодействие с пользователем приложения можно реализовать с помощью Shell, это полезно для этого. Приложения оболочки помогают избежать плохого запуска, так как страницы создаются по запросу в ответ на навигацию, а не при запуске приложений, которые используют TabbedPageприложения. Дополнительные сведения см. в разделе "Обзор оболочки".

Оптимизация производительности ListView

При использовании элемента управления ListView необходимо оптимизировать несколько механизмов взаимодействия с пользователем.

  • Инициализация — интервал времени, который начинается с момента создания элемента управления, и заканчивается при отображении элементов на экране.
  • Прокрутка — возможность прокрутки списка и проверка того, что пользовательский интерфейс не отстает от сенсорных жестов.
  • Взаимодействие для добавления, удаления и выбора элементов.

Для ListView элемента управления требуется приложение для предоставления данных и шаблонов ячеек. От выполнения этого условия будет в значительной степени зависеть производительность элемента управления. Дополнительные сведения см. в разделе "Кэш данных".

использование асинхронного программирования

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

Основы

При использовании TAP следует соблюдать следующие общие рекомендации.

  • Разберитесь в жизненном цикле задачи, представленном перечислением TaskStatus. Дополнительные сведения см. в разделах Значение TaskStatus и Состояние задачи.
  • Используйте метод Task.WhenAll для асинхронного ожидания завершения нескольких асинхронных операций вместо ожидания каждой из ряда асинхронных операций по-отдельности с помощью await. Дополнительные сведения см. в разделе Task.WhenAll.
  • Используйте метод Task.WhenAny для асинхронного ожидания завершения одной из нескольких асинхронных операций. Дополнительные сведения см. в разделе Task.WhenAny.
  • Используйте метод Task.Delay для создания объекта Task, который завершается после определенного времени. Это полезно в различных ситуациях, включая опрос данных и задержку обработки введенных пользователем данных на заданный период времени. Дополнительные сведения см. в разделе Task.Delay.
  • Выполняйте синхронные операции, интенсивно использующие ЦП, в пуле потоков с помощью метода Task.Run. Это ускоренный вариант метода TaskFactory.StartNew с оптимальным набором аргументов. Дополнительные сведения см. в разделе Task.Run.
  • Старайтесь не создавать асинхронные конструкторы. Вместо этого используйте события жизненного цикла или отдельную логику инициализации для правильного ожидания (await) любой инициализации. Дополнительные сведения см. в записи блога Async Constructors (Асинхронные конструкторы) на сайте blog.stephencleary.com.
  • Используйте шаблон отложенной задачи, чтобы избежать ожидания выполнения асинхронных операций во время запуска приложения. Дополнительные сведения см. в разделе AsyncLazy.
  • Создайте оболочку задачи для существующих асинхронных операций, которые не используют асинхронную модель на основе задач, путем создания объектов TaskCompletionSource<T>. Эти объекты обеспечивают возможности программирования Task и позволяют контролировать время существования и завершение связанной задачи Task. Дополнительные сведения см. в записи блога The Nature of TaskCompletionSource (Суть типа TaskCompletionSource).
  • Когда нет необходимости обрабатывать результат асинхронной операции, следует возвращать объект Task вместо ожидаемого объекта Task. Это более производительный подход, так как переключать контекст приходится реже.
  • Используйте библиотеку потоков данных "Библиотека параллельных задач" (TPL) в ситуациях, когда, например, необходимо обрабатывать данные по мере того, как они становятся доступными, или когда несколько операций должны взаимодействовать друг с другом асинхронно. Дополнительные сведения см. в статье Поток данных (библиотека параллельных задач).

UI

При использовании TAP с элементами управления пользовательского интерфейса следует соблюдать следующие рекомендации.

  • Вызывайте асинхронную версию API, если она доступна. Это позволит разблокировать поток пользовательского интерфейса, который поможет улучшить взаимодействие пользователя с приложением.

  • Обновляйте элементы пользовательского интерфейса данными из асинхронных операций в потоке пользовательского интерфейса, чтобы избежать возникновения исключений. Однако изменения свойства ListView.ItemsSource будут автоматически маршалироваться в поток пользовательского интерфейса. Сведения об определении того, выполняется ли код в потоке пользовательского интерфейса, см. в разделе "Создание потока пользовательского интерфейса".

    Внимание

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

Обработка ошибок

При использовании TAP следует следовать следующим рекомендациям по обработке ошибок:

  • Ознакомьтесь с асинхронной обработкой исключений. Необработанные исключения, создаваемые асинхронно выполняемым кодом, распространяются обратно в вызывающий поток, за исключением отдельных сценариев. Дополнительные сведения см. в статье Обработка исключений (библиотека параллельных задач).
  • Избегайте создания методов async void. Вместо этого создавайте методы async Task. Это упрощает обработку ошибок, компоновку и тестирование. Исключением из этого правила являются асинхронные обработчики событий, которые должны возвращать void. Дополнительные сведения см. в разделе Избегайте async void.
  • Не смешивайте блокирующий и асинхронный код путем вызова методов Task.Wait, Task.Result или GetAwaiter().GetResult, так как это может привести к возникновению взаимоблокировки. Однако если это правило необходимо нарушить, предпочтительным подходом является вызов метода GetAwaiter().GetResult, так как он сохраняет исключения задачи. Дополнительные сведения см. в разделах Соблюдайте асинхронность от начала до конца и Обработка исключений задач в .NET 4.5.
  • По возможности используйте метод ConfigureAwait для создания кода, не зависящего от контекста. Код без контекста обеспечивает лучшую производительность мобильных приложений и является полезным способом предотвращения взаимоблокировки при работе с частично асинхронной базой кода. Дополнительные сведения см. в разделе Конфигурируйте контекст.
  • Используйте задачи продолжения для таких функций, как обработка исключений, созданных предыдущей асинхронной операцией, и отмена продолжения перед его запуском либо во время его выполнения. Дополнительные сведения см. в статье Создание цепочки задач с помощью задач продолжения.
  • Используйте асинхронную реализацию ICommand, когда асинхронные операции вызываются из ICommand. Это позволит обрабатывать все исключения в логике асинхронных команд. Дополнительные сведения см. в статье "Асинхронное программирование: шаблоны для асинхронных приложений MVVM: команды".

Задержка затрат на создание объектов

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

Рекомендуется использовать отложенную инициализацию для объектов, которые являются дорогостоящими для создания в следующих сценариях:

  • Приложение может не использовать объект.
  • Перед созданием объекта должны быть выполнены другие ресурсоемкие операции.

Класс Lazy<T> используется для определения неясного инициализированного типа, как показано в следующем примере:

void ProcessData(bool dataRequired = false)
{
    Lazy<double> data = new Lazy<double>(() =>
    {
        return ParallelEnumerable.Range(0, 1000)
                     .Select(d => Compute(d))
                     .Aggregate((x, y) => x + y);
    });

    if (dataRequired)
    {
        if (data.Value > 90)
        {
            ...
        }
    }
}

double Compute(double x)
{
    ...
}

Отложенная инициализация производится при первом обращении к свойству Lazy<T>.Value. При первом доступе заключенный в оболочку тип создается, возвращается и сохраняется для использования в будущем.

Дополнительные сведения об отложенной инициализации см. в статье Отложенная инициализация.

Выпуск ресурсов IDisposable

Интерфейс IDisposable предоставляет механизм для освобождения ресурсов. Он предоставляет метод Dispose, который следует реализовать для освобождения ресурсов явным образом. Интерфейс IDisposable не является деструктором, и его следует реализовывать только в указанных ниже ситуациях:

  • Если класс является владельцем неуправляемых ресурсов. К типичным неуправляемым ресурсам, требующим освобождения, относятся файлы, потоки и сетевые подключения.
  • Если класс является владельцем управляемых ресурсов IDisposable.

Потребители типа могут вызывать реализацию IDisposable.Dispose для освобождения ресурсов, если экземпляр больше не нужен. Для этого могут применяться два подхода:

  • заключение объекта IDisposable в оператор using;
  • заключение вызова метода IDisposable.Dispose в блок try/finally.

Перенос объекта IDisposable в инструкцию using

В следующем примере показано, как упаковать IDisposable объект в инструкцию using :

public void ReadText(string filename)
{
    string text;
    using (StreamReader reader = new StreamReader(filename))
    {
        text = reader.ReadToEnd();
    }
    ...
}

Класс StreamReader реализует интерфейс IDisposable, а оператор using предоставляет удобный синтаксис для вызова метода StreamReader.Dispose объекта StreamReader, прежде чем он окажется вне области действия. В блоке using объект StreamReader доступен только для чтения, и переназначить его нельзя. Кроме того, оператор using обеспечивает вызов метода Dispose даже в случае возникновения исключения, так как компилятор реализует промежуточный язык (IL) для блока try/finally.

Перенос вызова в IDisposable.Dispose в блок try/finally

В следующем примере показано, как упаковать вызов IDisposable.Dispose в try/finally блок:

public void ReadText(string filename)
{
    string text;
    StreamReader reader = null;
    try
    {
        reader = new StreamReader(filename);
        text = reader.ReadToEnd();
    }
    finally
    {
        if (reader != null)
            reader.Dispose();
    }
    ...
}

Класс StreamReader реализует интерфейс IDisposable, и блок finally вызывает метод StreamReader.Dispose для освобождения ресурса. Дополнительные сведения см. в статье, посвященной интерфейсу IDisposable.

Отмена подписки на события

Во избежание утечек памяти следует отменять подписку на события перед удалением объекта-подписчика. До тех пор пока подписка на событие не отменена, делегат события в публикующем объекте будет ссылаться на делегат, инкапсулирующий обработчик событий подписчика. Пока ссылка присутствует в публикующем объекте, память, занимаемая объектом-подписчиком, не будет освобождена при сборке мусора.

В следующем примере показано, как отменить подписку на событие:

public class Publisher
{
    public event EventHandler MyEvent;

    public void OnMyEventFires()
    {
        if (MyEvent != null)
            MyEvent(this, EventArgs.Empty);
    }
}

public class Subscriber : IDisposable
{
    readonly Publisher _publisher;

    public Subscriber(Publisher publish)
    {
        _publisher = publish;
        _publisher.MyEvent += OnMyEventFires;
    }

    void OnMyEventFires(object sender, EventArgs e)
    {
        Debug.WriteLine("The publisher notified the subscriber of an event");
    }

    public void Dispose()
    {
        _publisher.MyEvent -= OnMyEventFires;
    }
}

Класс Subscriber отменяет подписку на событие в методе Dispose.

При использовании обработчиков событий и синтаксиса лямбда-выражений могут также образовываться циклические ссылки, так как лямбда-выражения могут ссылаться на объекты и поддерживать их в активном состоянии. Таким образом, ссылка на анонимный метод может храниться в поле и использоваться для отмены подписки из события, как показано в следующем примере:

public class Subscriber : IDisposable
{
    readonly Publisher _publisher;
    EventHandler _handler;

    public Subscriber(Publisher publish)
    {
        _publisher = publish;
        _handler = (sender, e) =>
        {
            Debug.WriteLine("The publisher notified the subscriber of an event");
        };
        _publisher.MyEvent += _handler;
    }

    public void Dispose()
    {
        _publisher.MyEvent -= _handler;
    }
}

Поле _handler содержит ссылку на анонимный метод и служит для подписки на события и ее отмены.

Избегайте сильных циклических ссылок на iOS и Mac Catalyst

В некоторых ситуациях могут образовываться циклы строгих ссылок, из-за которых память, занимаемая объектами, может не освобождаться сборщиком мусора. Например, рассмотрим случай, когда NSObjectпроизводный от него подкласс, например класс, наследуемый от UIView, добавляется в производный NSObjectконтейнер и строго ссылается на Objective-Cнего, как показано в следующем примере:

class Container : UIView
{
    public void Poke()
    {
        // Call this method to poke this object
    }
}

class MyView : UIView
{
    Container _parent;

    public MyView(Container parent)
    {
        _parent = parent;
    }

    void PokeParent()
    {
        _parent.Poke();
    }
}

var container = new Container();
container.AddSubview(new MyView(container));

Когда в этом коде создается экземпляр Container, объект C# будет иметь строгую ссылку на объект Objective-C. Аналогичным образом, экземпляр MyView будет также иметь строгую ссылку на объект Objective-C.

Кроме того, вызов container.AddSubview будет увеличивать число ссылок в неуправляемом экземпляре MyView. В этом случае среда выполнения .NET для iOS создает GCHandle экземпляр для сохранения MyView объекта в управляемом коде, так как не гарантирует, что все управляемые объекты будут хранить ссылку на него. С точки зрения управляемого кода, память, занимаемая объектом MyView, освобождалась бы после вызова AddSubview(UIView), если бы не экземпляр GCHandle.

Неуправляемый объект MyView будет иметь экземпляр GCHandle, указывающий на управляемый объект, что называется строгой связью. Управляемый объект будет содержать ссылку на экземпляр Container. Экземпляр Container, в свою очередь, будет иметь управляемую ссылку на объект MyView.

В ситуациях, когда содержащийся объект сохраняет ссылку на свой контейнер, есть несколько способов, как поступить с циклической ссылкой:

  • Избегайте циклической ссылки, сохраняя слабую ссылку на контейнер.
  • Вызовите метод Dispose для объектов.
  • Разорвите цикл вручную, присвоив ссылке на контейнер значение null.
  • Вручную удалите содержащийся объект из контейнера.

Использование слабых ссылок

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

class Container : UIView
{
    public void Poke()
    {
        // Call this method to poke this object
    }
}

class MyView : UIView
{
    WeakReference<Container> _weakParent;

    public MyView(Container parent)
    {
        _weakParent = new WeakReference<Container>(parent);
    }

    void PokeParent()
    {
        if (weakParent.TryGetTarget (out var parent))
            parent.Poke();
    }
}

var container = new Container();
container.AddSubview(new MyView container));

Здесь содержащийся объект не проверяет активности родительского элемента. Однако родитель держит ребенка в живых через вызов container.AddSubView.

Это также происходит в API iOS, использующих шаблон делегата или источника данных, где одноранговый класс содержит реализацию. Например, при задании Delegate свойства или DataSourceUITableView класса.

В случае с классами, которые создаются исключительно для реализации протокола, например IUITableViewDataSource, вместо создания подкласса вы можете просто реализовать интерфейс в классе, переопределить метод и присвоить свойство DataSource указателю this.

Удаление объектов с строгими ссылками

Если существует строгая ссылка и трудно удалить зависимость, очистите родительский указатель с помощью метода Dispose.

Для контейнеров переопределите Dispose метод для удаления содержащихся объектов, как показано в следующем примере:

class MyContainer : UIView
{
    public override void Dispose()
    {
        // Brute force, remove everything
        foreach (var view in Subviews)
        {
              view.RemoveFromSuperview();
        }
        base.Dispose();
    }
}

В случае с дочерним объектом, сохраняющим строгую ссылку на свой родительский объект, очистите ссылку на родительский объект в реализации Dispose:

class MyChild : UIView
{
    MyContainer _container;

    public MyChild(MyContainer container)
    {
        _container = container;
    }

    public override void Dispose()
    {
        _container = null;
    }
}