AssemblyLoadContext и динамическая загрузка и выгрузка сборок
В статье Динамическая загрузка сборок и позднее связывание рассматривалось, динамически загружать в приложение сборки и использовать их функционал. Но фреймворк .NET также позволяет выгружать сборки, что позволяет уменьшить объем потребляемой памяти. Для этого применяется класс AssemblyLoadContext из пространства имен System.Runtime.Loader, который представляет контекст загрузки и выгрузки сборок. Рассмотрим, как его использовать.
Допустим, у нас есть консольный проект MyApp со следующим файлом Program.cs:
class Program
{
static void Main(string[] args)
{
var number = 5;
var result = Square(number);
Console.WriteLine($"Квадрат {number} равен {result}");
}
static int Square(int n) => n * n;
}
Эта программа содержит метод Square для вычисления квадрата, и по умолчанию она компилируется в сборку MyApp.dll. Загрузим эту сборку, чтобы использовать ее метод Square.
Для создания объекта AssemblyLoadContext применяется следующий конструктор:
public AssemblyLoadContext (string? name, bool isCollectible = false);
В конструкторе первый параметр устанавливает имя контекста - это может произвольная строка. Второй параметр - isCollectible устанавливает, можно ли загруженные сборки выгружать. Значение true указывает, что загруженные сборки можно выгружать.
Для загрузки сборок класс AssemblyLoadContext предоставляет ряд методов. Некоторые из них:
Assembly LoadFromAssemblyName (AssemblyName assemblyName): загружает определенную сборку по имени, которое представлено типом System.Reflection.AssemblyName
Assembly LoadFromAssemblyPath (string assemblyPath): загружает сборку по определенному пути (путь должен быть абсолютным)
Assembly LoadFromStream (System.IO.Stream stream): загружает определенную сборку из потока Stream
Использовав один из этих методов, мы можем получить доступ к сборке через тип Assembly и обращаться к ее функционалу.
После завершения работы со сборкой мы можем вызвать у AssemblyLoadContext метод Unload() и выгрузить контекст со всеми загруженными сборками и тем самым снизить потребление памяти и увеличить общую производительность.
Рассмотрим полный пример:
using System.Reflection;
using System.Runtime.Loader;
Square(8);
// очистка памяти
GC.Collect();
GC.WaitForPendingFinalizers();
Console.WriteLine();
// смотрим, какие сборки после выгрузки
foreach (Assembly asm in AppDomain.CurrentDomain.GetAssemblies())
Console.WriteLine(asm.GetName().Name);
void Square(int number)
{
var context = new AssemblyLoadContext(name: "Square", isCollectible: true);
// установка обработчика выгрузки
context.Unloading += Context_Unloading;
// получаем путь к сборке MyApp
var assemblyPath = Path.Combine(Directory.GetCurrentDirectory(), "MyApp.dll");
// загружаем сборку
Assembly assembly = context.LoadFromAssemblyPath(assemblyPath);
// получаем тип Program из сборки MyApp.dll
var type = assembly.GetType("MyApp.Program");
if (type != null)
{
// получаем его метод Square
var squareMethod = type.GetMethod("Square");
// вызываем метод
var instance = Activator.CreateInstance(type);
var result = squareMethod?.Invoke(instance, new object[] { number });
if (result is int)
{
// выводим результат метода на консоль
Console.WriteLine($"Квадрат числа {number} равен {result}");
}
}
// смотим, какие сборки у нас загружены
foreach (Assembly asm in AppDomain.CurrentDomain.GetAssemblies())
Console.WriteLine(asm.GetName().Name);
// выгружаем контекст
context.Unload();
}
// обработчик выгрузки контекста
void Context_Unloading(AssemblyLoadContext obj)
{
Console.WriteLine("Библиотека MyApp выгружена");
}
Все эти действия оформляются в виде отдельного метода Square(). В качестве параметра он принимает число, квадрат которого надо вычислить.
Вначале в методе создается объект AssemblyLoadContext:
var context = new AssemblyLoadContext(name: "Square", isCollectible: true);
Обратите внимание, что параметру isCollectible передается значение true, что позволит выгружать ранее загруженные сборки.
Класс AssemblyLoadContext определяет событие Unloadig, благодаря чему мы можем повесить обработчик и определить момент выгрузки контекста.
context.Unloading += Context_Unloading;
Далее используется метод LoadFromAssemblyPath для загузки сборки MyApp.dll по абсолютному пути. В данном случае предполагается, что файл сборки находится в одной папке с текущим приложением.
Assembly assembly = context.LoadFromAssemblyPath(assemblyPath);
Получив сборку, с помощью рефлексии обращаемся к методу Square и получаем квадрат числа.
Затем смотрим, какие сборки загружены в текущий домен. Среди них мы сможем найти и MyApp.dll. И в конце выгружаем контекст:
context.Unload();
Данный метод Square вызывается в методе Main:
Square(8);
// очистка
GC.Collect();
GC.WaitForPendingFinalizers();
Но обратите внимание, что выгрузка контекста сама по себе не означает немедленной очистки памяти. Вызов метода Unload только инициирует процесс выгрузки, реальная выгрузка произойдет лишь тогда, когда в дело вступит автоматический сборщик мусора и удалит соответствующие объекты. Поэтому для более быстрой очистки в конце вызываются методы GC.Collect() и GC.WaitForPendingFinalizers().
Консольный вывод:
System.Private.CoreLib
HelloApp
System.Runtime
System.Console
System.Runtime.Loader
MyApp
Библиотека MyApp выгружена
System.Private.CoreLib
HelloApp
System.Runtime
System.Console
System.Runtime.Loader
System.Threading
Microsoft.Win32.Primitives
System.Collections
System.Memory
Как видно, после выгрузки контекста AssemblyLoadContext сборки MyApp в списке загруженных сборок больше не наблюдается.