AssemblyLoadContext și încărcarea și descărcarea dinamică a asamblărilor
În .NET, pe lângă încărcarea dinamică a asamblărilor, există și posibilitatea de a le descărca, reducând astfel consumul de memorie. Aceasta se realizează prin utilizarea clasei AssemblyLoadContext din spațiul de nume System.Runtime.Loader, care reprezintă contextul de încărcare și descărcare a asamblărilor. Să vedem cum se utilizează acest concept.
Să presupunem că avem un proiect de tip consolă numit MyApp, cu următorul fișier Program.cs:
namespace MyApp
{
class Program
{
static void Main(string[] args)
{
var number = 5;
var result = Square(number);
Console.WriteLine($"Pătratul numărului {number} este {result}");
}
static int Square(int n) => n * n;
}
}
Această aplicație conține metoda Square pentru calculul pătratului unui număr și, implicit, va fi compilată într-o asamblare MyApp.dll. Vom încărca această asamblare pentru a folosi metoda Square.
Pentru a crea un obiect AssemblyLoadContext, se utilizează următorul constructor:
public AssemblyLoadContext(string? name, bool isCollectible = false);
Primul parametru setează numele contextului, iar al doilea parametru, isCollectible, indică dacă asamblările încărcate pot fi descărcate. Valoarea true permite descărcarea asamblărilor.
Clasa AssemblyLoadContext oferă mai multe metode pentru încărcarea asamblărilor. Printre acestea:
- Assembly LoadFromAssemblyName(AssemblyName assemblyName): încarcă o anumită asamblare după numele său, care este reprezentat de tipul System.Reflection.AssemblyName
- Assembly LoadFromAssemblyPath(string assemblyPath): încarcă o asamblare de la un anumit path (calea trebuie să fie absolută)
- Assembly LoadFromStream(System.IO.Stream stream): încarcă o asamblare dintr-un flux Stream
După ce am terminat de utilizat asamblarea, putem apela metoda Unload() a AssemblyLoadContext pentru a descărca contextul împreună cu toate asamblările încărcate, reducând astfel consumul de memorie și sporind performanța generală.
Exemplu complet:
using System.Reflection;
using System.Runtime.Loader;
Square(8);
// curățare memorie
GC.Collect();
GC.WaitForPendingFinalizers();
Console.WriteLine();
// verificăm ce asamblări sunt încărcate după descărcare
foreach (Assembly asm in AppDomain.CurrentDomain.GetAssemblies())
Console.WriteLine(asm.GetName().Name);
void Square(int number)
{
var context = new AssemblyLoadContext(name: "Square", isCollectible: true);
// setarea unui handler pentru descărcare
context.Unloading += Context_Unloading;
// obținem calea către asamblarea MyApp
var assemblyPath = Path.Combine(Directory.GetCurrentDirectory(), "MyApp.dll");
// încărcăm asamblarea
Assembly assembly = context.LoadFromAssemblyPath(assemblyPath);
// obținem tipul Program din asamblarea MyApp.dll
var type = assembly.GetType("MyApp.Program");
if (type != null)
{
// obținem metoda Square
var squareMethod = type.GetMethod("Square", BindingFlags.Static | BindingFlags.NonPublic);
// apelăm metoda
var result = squareMethod?.Invoke(null, new object[] { number });
if (result is int)
{
// afișăm rezultatul metodei în consolă
Console.WriteLine($"Pătratul numărului {number} este {result}");
}
}
// verificăm ce asamblări sunt încărcate
foreach (Assembly asm in AppDomain.CurrentDomain.GetAssemblies())
Console.WriteLine(asm.GetName().Name);
// descărcăm contextul
context.Unload();
}
// handler pentru descărcarea contextului
void Context_Unloading(AssemblyLoadContext obj)
{
Console.WriteLine("Biblioteca MyApp a fost descărcată");
}
Toate aceste acțiuni sunt realizate sub forma unei metode separate, Square(). Ca parametru, aceasta primește numărul al cărui pătrat trebuie calculat.
La început, în metodă se creează un obiect AssemblyLoadContext:
var context = new AssemblyLoadContext(name: "Square", isCollectible: true);
Observați că parametrului isCollectible i se atribuie valoarea true, ceea ce va permite descărcarea adunărilor încărcate anterior.
Clasa AssemblyLoadContext definește un eveniment Unloading, astfel încât putem adăuga un handler și putem determina momentul descărcării contextului.
context.Unloading += Context_Unloading;
Apoi, se utilizează metoda LoadFromAssemblyPath pentru a încărca adunarea MyApp.dll de la un drum absolut. În acest caz, se presupune că fișierul adunării se află în același folder cu aplicația curentă.
assembly: Assembly assembly = context.LoadFromAssemblyPath(assemblyPath);
După ce am obținut adunarea, folosim reflecția pentru a accesa metoda Square și a obține pătratul numărului.
Apoi, verificăm ce adunări sunt încărcate în domeniul curent. Printre ele, vom putea găsi și MyApp.dll. La final, descărcăm contextul:
context.Unload();
Această metodă Square este apelată în metoda Main:
Square(8);
GC.Collect();
GC.WaitForPendingFinalizers();
Dar, rețineți că descărcarea contextului în sine nu înseamnă curățarea imediată a memoriei. Apelul metodei Unload doar inițiază procesul de descărcare, descărcarea reală va avea loc doar atunci când colectorul automat de gunoi va interveni și va elimina obiectele corespunzătoare. Prin urmare, pentru o curățare mai rapidă, la final se apelează metodele GC.Collect() și GC.WaitForPendingFinalizers().
Outputul din consolă:
Pătratul numărului 8 este 64
System.Private.CoreLib
HelloApp
System.Runtime
Microsoft.Extensions.DotNetDeltaApplier
System.IO.Pipes
System.Linq
System.Collections
System.Console
System.Runtime.Loader
MyApp
System.Collections.Concurrent
System.Threading
System.Text.Encoding.Extensions
Biblioteca MyApp a fost descărcată
System.Private.CoreLib
HelloApp
System.Runtime
Microsoft.Extensions.DotNetDeltaApplier
System.IO.Pipes
System.Linq
System.Collections
System.Console
System.Runtime.Loader
System.Threading.Overlapped
System.Collections.Concurrent
System.Threading
System.Text.Encoding.Extensions
După cum se poate observa, după descărcarea contextului AssemblyLoadContext, adunarea MyApp nu mai apare în lista adunărilor încărcate.