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