Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dynamically load and uninstall a library containing Avalonia components #13935

Closed
MakesYT opened this issue Dec 13, 2023 · 4 comments
Closed

Comments

@MakesYT
Copy link
Contributor

MakesYT commented Dec 13, 2023

Describe the bug

I want to dynamically load and uninstall a library containing Avalonia components in an Avalonia project, but I notice that Avalonia components seem to register a lot of things in the main project, is there any way I can remove these registrations so that they can be uninstalled

If don't create Avalonia controls from the library, everything is fine

To Reproduce

//Main Project 
public partial class App : Application
{
    public override void Initialize()
    {
        AvaloniaXamlLoader.Load(this);
        
        var weakReference = test();
        while (weakReference.IsAlive)
        {
            GC.Collect();
            GC.WaitForPendingFinalizers();
        }
        
    }

    
    public  WeakReference test(){
        var assemblyLoadContext = new AssemblyLoadContextH("<dll.path>","test");
        var assembly = assemblyLoadContext.LoadFromAssemblyPath("<dll.path>");
        foreach (var type in assembly.GetTypes())
        {
            if (type.FullName=="AvaloniaPlug.Window1")
            {
                //创建type实例
                Window instance = (Window)Activator.CreateInstance(type);
                Dispatcher.UIThread.InvokeAsync(() =>
                {
                    instance.Show();
                }).Wait();
                //instance.Show();
                while (instance.IsActive)
                { 
                    Task.Delay(100);
                }
            }
        }
        assemblyLoadContext.Unload();
        return new WeakReference(assemblyLoadContext);
    }
}

//dll
image

//Window1.axaml
<Window xmlns="https://github.com/avaloniaui"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
        x:Class="AvaloniaPlug.Window1"
        Title="Window1">
    Welcome to Avalonia!
</Window>

Expected behavior

can be uninstalled

Screenshots

image

Environment

  • OS: Windows
  • Avalonia-Version: 11.0.6

Additional context

Add any other context about the problem here.

@MakesYT MakesYT added the bug label Dec 13, 2023
MakesYT added a commit to MakesYT/Avalonia that referenced this issue Dec 16, 2023
@MakesYT
Copy link
Contributor Author

MakesYT commented Dec 16, 2023

#13974

@maxkatz6 maxkatz6 added enhancement and removed bug labels Dec 16, 2023
maxkatz6 pushed a commit that referenced this issue Apr 6, 2024
…content #13935 (#13974)

* Try fix #13935

* Fix

* Fix

* add sample

* Fix

* try load Style by reflection

* try

* Fixed an error when registering properties when uninstalling assemblies

* Allowed to delete the IAssemblyDescriptorResolver StandardAssetLoader _assemblyNameCache

* Resolving merge conflicts

* Fix

* Add exegesis

* optimize

* fix

* Resolving merge conflicts

* nuke
@MaxwellDAssistek
Copy link
Contributor

MaxwellDAssistek commented Sep 25, 2024

Edit: Opened separate issue #17129.

I would not consider this done for anything but the most trivial application at this point. There are still some things that will keep an assembly loaded forever, even when adding the same Unloading handler as the sample.

The biggest issue is Observables. We need a way to disconnect all observable listeners belonging to an assembly. For example, lets take a rather trivial library like AvaloniaProgressRing, which is basically all implemented using animations. The issue is that the animation system itself connects to Observables that get stuck even though the whole module UserControl, which contains the spinner, is removed from the window before unloading the AssemblyLoadContext. Here is what I can see in dotMemory some time after the unload happens:

image

Another slightly tangential issue that I am encountering is that if I ignore the memory leak I actually can't even reload any of the module's assemblies later because all avares references are still pointing to the old half-unloaded assembly due to how the AssemblyDescriptorResolver functions:

var loadedAssemblies = AppDomain.CurrentDomain.GetAssemblies();
var match = loadedAssemblies.FirstOrDefault(a => name.Equals(a.GetName().Name, StringComparison.InvariantCultureIgnoreCase));

The problem is that it just gets a list of all assemblies in the AppDomain and finds the first assembly with the requested name. Problem being that the half-unloaded assembly is the first on that it finds.

Since AssemblyDescriptorResolver is an internal Avalonia class that I can't extend, the only way I was able to work around the problem is using Harmony to hot-patch the GetAssembly method with a version that resolves assemblies through the correct AssemblyLoadContext.

This is obviously a very bad and fragile workaround, so it would be nice to get the module to fully unload in the first place.

@MakesYT
Copy link
Contributor Author

MakesYT commented Sep 27, 2024

Edit: Opened separate issue #17129.

I would not consider this done for anything but the most trivial application at this point. There are still some things that will keep an assembly loaded forever, even when adding the same Unloading handler as the sample.

The biggest issue is Observables. We need a way to disconnect all observable listeners belonging to an assembly. For example, lets take a rather trivial library like AvaloniaProgressRing, which is basically all implemented using animations. The issue is that the animation system itself connects to Observables that get stuck even though the whole module UserControl, which contains the spinner, is removed from the window before unloading the AssemblyLoadContext. Here is what I can see in dotMemory some time after the unload happens:

image

Another slightly tangential issue that I am encountering is that if I ignore the memory leak I actually can't even reload any of the module's assemblies later because all avares references are still pointing to the old half-unloaded assembly due to how the AssemblyDescriptorResolver functions:

var loadedAssemblies = AppDomain.CurrentDomain.GetAssemblies();
var match = loadedAssemblies.FirstOrDefault(a => name.Equals(a.GetName().Name, StringComparison.InvariantCultureIgnoreCase));

The problem is that it just gets a list of all assemblies in the AppDomain and finds the first assembly with the requested name. Problem being that the half-unloaded assembly is the first on that it finds.

Since AssemblyDescriptorResolver is an internal Avalonia class that I can't extend, the only way I was able to work around the problem is using Harmony to hot-patch the GetAssembly method with a version that resolves assemblies through the correct AssemblyLoadContext.

This is obviously a very bad and fragile workaround, so it would be nice to get the module to fully unload in the first place.

I'm not sure what you mean
Screenshot_20240927_101152_com_github_android_CommitActivity.png

Do you mean in the sample code 'Assemblies.First()'? You can call Assemblies multiple times or iterate over all Assemblies calls to unload

Listening to all Observables seems to be possible by reimplementing the corresponding interface and creating a new class

The current solution is indeed fragile and provides only a minimal offload capability, and it seems complex to implement automatic offloading.

@MaxwellDAssistek
Copy link
Contributor

Do you mean in the sample code 'Assemblies.First()'? You can call Assemblies multiple times or iterate over all Assemblies calls to unload

Listening to all Observables seems to be possible by reimplementing the corresponding interface and creating a new class

The current solution is indeed fragile and provides only a minimal offload capability, and it seems complex to implement automatic offloading.

I understood what you meant in the sample and I did call those functions for all assemblies loaded through the AssemblyLoadContext. The problem is that there are many cases where 3rd party libraries or even Avalonia internals set up observers but never disconnect them. Just today issue #17139 shows it happening with the Avalonia internal animation system, which is out of my control.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants
@maxkatz6 @MakesYT @MaxwellDAssistek and others