C# .NET Core 3.1 中 AssemblyLoadContext 的基本使用
前言
之前使用 AppDomain 寫過一個動態加載和釋放程序的案例,基本實現了自己“兔死狗烹”,不留痕跡的設想。無奈在最新的 .NET Core 3.1 中,已經不支持創建新的 AppDomain 了(據說是因為跨平台實現太重了),改為使用 AssemblyLoadContext 了。不過總體使用下來感覺比原來的 AppDomain 要直觀。
不過這一路查找資料,感覺 .NET Core 發展到 3.1 的過程還是經歷了不少的。比如 2.2 的 API 與 3.1 就不一樣(自己的體會,換了個版本就提示函數參數錯誤), preview版中 AssemblyLoadContext 卸載后無法刪除庫文件,但是版本升級后就好了(github 上的一篇討論)
本文主要是關於 AssemblyLoadContext 的基本使用,加載和釋放類庫。
基本使用
程序的基本功能是:動態加載 Magick 的所需庫,並調用其壓縮圖片的函數壓縮給定圖片。(歪個樓,Magick 和 Android 的 Magisk 這兩個看起來太像了)
using System;
using System.IO;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Loader;
namespace AssemblyLoadContextTest
{
class Program
{
static void Main(string[] args)
{
WeakReference weakReference;
Compress(out weakReference);
for (int i = 0; weakReference.IsAlive && (i < 10); i++)
{
GC.Collect();
GC.WaitForPendingFinalizers();
}
Console.WriteLine($"卸載成功: {!weakReference.IsAlive}");
}
[MethodImpl(MethodImplOptions.NoInlining)]
public static void Compress(out WeakReference weakReference)
{
AssemblyLoadContext alc = new AssemblyLoadContext("CompressLibrary", true); // 新建一個 AssemblyLoadContext 對象
weakReference = new WeakReference(alc);
Assembly assembly0 = alc.LoadFromAssemblyPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Magick.NET.Core.dll"));
Assembly assembly1 = alc.LoadFromAssemblyPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Magick.NET-Q16-AnyCPU.dll"));
string filePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "image_to_compress.jpg");
Console.WriteLine("壓縮前大小:" + new FileInfo(filePath).Length);
var magickImageType = assembly1.GetType("ImageMagick.MagickImage"); // 已知該類定義在 assembly1 中
var magickImageIns = Activator.CreateInstance(magickImageType, new object[] { filePath }); // magickImageIns = new ImageMagick.MagickImage(filePath)
var qualityProperty = magickImageType.GetProperty("Quality");
qualityProperty.SetValue(magickImageIns, 60); // magickImageIns.Quality = 60
var writeMethod = magickImageType.GetMethod("Write", new Type[] { typeof(string) });
writeMethod.Invoke(magickImageIns, new object[] { filePath }); // magickImageIns.Write(filePath)
Console.WriteLine("壓縮后大小:" + new FileInfo(filePath).Length);
var disposeMethod = magickImageType.GetMethod("Dispose");
disposeMethod.Invoke(magickImageIns, null); // magickImageIns.Dispose()
//magickImageIns = null;
alc.Unload();
}
}
}

加載不用多說,創建實例加載即可;卸載時需要注意的是一下幾點:
- 使用 AssemblyLoaderContext 加載和卸載的代碼必須要單獨放在一個方法,不可以寫在 Main 方法中,否則加載的模塊只有等待整個程序退出后才能卸載
- 方法中應加上 [MethodImpl(MethodImplOptions.NoInlining)] 特性,否則可能也不會正常卸載(在本例子中似乎不加也可以),官方示例是這么說的:
It is important to mark this method as NoInlining, otherwise the JIT could decide
to inline it into the Main method. That could then prevent successful unloading
of the plugin because some of the MethodInfo / Type / Plugin.Interface / HostAssemblyLoadContext
instances may get lifetime extended beyond the point when the plugin is expected to be
unloaded.
- 卸載的過程是異步的,調用了以后並不會立刻完成
- 如果一定要等待其完成可以通過創建一個 WeakReference 指向它,通過查看 WeakReference 是否存在來判斷是否完成釋放。 但等待釋放的方法要在“加載卸載的代碼”方法外,否則依然無法查看到它被回收
- 還有一點比較奇怪,如果我在最后不加 magickImageIns = null; 這一句,有時可以卸載,有時又無法卸載。如果類似的情況無法卸載,可以加上試試。
TIPS
在 Visual Studio 中提供了“模塊窗口”,可以及時查看加載了哪些程序集,在 “調試” > “窗口” > “模塊”

簡單對比 AppDomain
AppDomain 似乎是一個大而全的概念,包括了程序運行的方方面面:工作路徑、引用搜索路徑、配置文件、卷影復制 等,而 AssemblyLoadContext 只是一個加載程序集的工具。
參考
官方示例(參看其中的 /Host/Program.cs)
Visual Studio 中的 模塊 窗口
https://docs.microsoft.com/zh-cn/visualstudio/debugger/how-to-use-the-modules-window?view=vs-2019
這篇挺詳細的,很多問題我沒有深入地研究,但是其中的“需要的變量放到靜態字典中.在Unload之前把對應的Key值刪除掉”我不認同,也可能是因為版本原因吧
https://www.cnblogs.com/LucasDot/p/13956384.html
提問者無意間通過 ref 引用了 AssemblyLoadContext 對象而導致無法回收
https://stackoverflow.com/questions/55693269/assemblyloadcontext-did-not-unload-correctly
最后的測試方法應該單獨寫在一個方法中而不是在 Main 函數中(作者沒有顯式指明,我在這困擾了好久)
https://www.cnblogs.com/maxzhang1985/p/10875278.html
