镜像站点 · 本页由第三方 GitHub 只读镜像提供,非 GitHub 官方站点,不接受任何登录或凭据输入。前往 github.com
Skip to content

All app assemblies are loaded on startup #1443

Closed

Description

@kzu

Steps to Reproduce

Given code like the following:

// Some static dictionary somewhere:
public static Dictionary<string, Func<object>> Services { get; } = new Dictionary<string, Func<object>>();

// From MainActivity.cs;
Services.Add(nameof(BarServices.Bar), CreateBar);
Services.Add(nameof(FooServices.Foo), CreateFoo);

And given that the two methods are defined to explicitly not be inlined or otherwise loaded until invoked:

[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)]
static object CreateBar() => new BarServices.Bar();

[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)]
static object CreateFoo() => new FooServices.Foo();

And given FooServices.dll and BarServices.dll are two completely independent assemblies from the main app's, it would be highly desirable for the assemblies to only be loaded when the dictionary entry is accessed and the delegate is invoked, instead of happening on app start, which slows down the process "unnecessarily".

The ~same code in a .NET app properly lazy loads the assemblies as needed:

        public static Dictionary<string, Func<object>> Services { get; } = new Dictionary<string, Func<object>>(StringComparer.OrdinalIgnoreCase);

        static void Main(string[] args)
        {
            // Both this approach as well as the inline lambda properly delay-load the assemblies
            //Services.Add(nameof(BarServices.Bar), CreateBar);
            //Services.Add(nameof(FooServices.Foo), CreateFoo);

            Services.Add(nameof(BarServices.Bar), () => new BarServices.Bar());
            Services.Add(nameof(FooServices.Foo), () => new FooServices.Foo());

            var line = "";
            while ((line = Console.ReadLine()).Length > 0)
            {
                Console.WriteLine(Services[line.Trim()].Invoke().ToString());
            }
        }

        [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)]
        static object CreateBar() => new BarServices.Bar();

        [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)]
        static object CreateFoo() => new FooServices.Foo();

This is important for apps that use assembly-partitioning to split logic across modules, which should only be loaded whenever needed (i.e. when the UI that consumes them is navigated to, say).

Expected Behavior

Assemblies are lazy-loaded.

Actual Behavior

Both assemblies are loaded up-front:

03-19 15:36:33.243    nexus_5x    Debug    13769    Mono    Assembly FooServices[0x9de4ed60] added to domain RootDomain, ref_count=1

Version Information

Currently verified against 15.7 pre2

Activity

  1. kzu commented on Mar 19, 2018

    @kzu
    ContributorAuthor
  2. jonpryor commented on Mar 19, 2018

    @jonpryor
    Contributor

    Related: https://github.com/xamarin/xamarin-android/blob/8c210a0b5d4b3bd33c6bd2cfb010260c65fed1b3/src/monodroid/jni/monodroid-glue.c#L3684-L3700

    From the original commit introducing the register_packages() function:

    During app startup, check assemblies to see if they contain the
    Java.Interop.__TypeRegistrations type. If present, invoke
    __TypeRegistrations.RegisterPackages().

    This allows better type mappings to be done from
    Java.Interop.TypeManager.CreateInstance(): instead of always returning
    a wrapper type from Mono.Android.dll, we can use a "best fit" wrapper
    type from any assembly containing Java.Interop.__TypeRegistrations,
    including Mono.Android.GoogleMaps.dll.

    For example, previously activity.FindViewById(id) would return a
    ViewGroupInvoker for a MapView. With this change, it will return a
    MapView instance. See also: f0dad0a3.

    This isn't ideal, as we check every assembly during startup to see if
    it contains a Java.Interop.__TypeRegistrations type.

    The mentioned f0dad0a3 commit has this to say:

    [Mono.Android] Fix View.FindViewById<T>() for 3rd party libraries.

    View.FindViewById<T>() needs to use JavaCast<T>(), just as
    Activity.FindViewById<T>() does (see commit ec965bd0).

    Why? So that it actually works properly with 3rd party libs:

        http://lists.ximian.com/pipermail/monodroid/2012-February/009030.html
    

    The odd thing is that the user is reporting that null is being
    returned instead of throwing an InvalidCastException, but the problem
    is the same: we shouldn't be using a runtime cast, we need to use
    JavaCast<T>().

  3. jonpryor commented on Mar 19, 2018

    @jonpryor
    Contributor

    Summarizing the above, here's the scenario:

    1. You have a Java library which exports a com.example.MyView type which inherits android.view.View.

    2. The type from (1) is bound into an Example.MyView type in Example.dll

    3. The type from (1) is used in Resources\layout\Main.axml within the App, a'la

      <LinearLayout ...>
        <com.example.MyView android:id="@+id/myView" .../>
    4. The App has code which calls:

      var boundView = FindViewById (Resource.Id.myView);

      Note: non-generic Activity.FindViewById() invocation.

    The expectation is that Activity.FindViewById() will return the Example.MyView binding type. In order for that to happen, Xamarin.Android needs to know that the Example.MyView type exists, possibly before it is instantiated from C# code (which may never happen; all use could be via Activity.FindViewById()!).

    The chosen implementation strategy was to load all assemblies during process startup, look for the Java.Interop.__TypeRegistrations type within the assembly, and -- if found -- invoke Java.Interop. __TypeRegistrations.RegisterPackages(). This allows Mono.Android.dll to know that Example.MyView exists and use it for type mappings.

  4. jonpryor commented on Jul 2, 2018

    @jonpryor
    Contributor

    I believe that the previous description is now -- and has been! -- moot, since the introduction of "type maps" in 2015-Apr-22 and subsequent fixes.

    The typemap.jm and typemap.mj files should address the scenario described, without requiring any startup assembly loading.

    Additionally, allow assemblies to be loaded on-demand -- instead of all at once, during process startup -- should help with our startup times.

  5. jonpryor commented on Jul 2, 2018

    @jonpryor
    Contributor

    Related: #1829

  6. jonpryor commented on Jul 2, 2018

    @jonpryor
    Contributor

    The fix for "all assemblies are loaded on startup" is to remove register_packages(), and make sure that nothing breaks. :-)

  7. kzu commented on Jul 11, 2018

    @kzu
    ContributorAuthor

    Awesome news @jonpryor 💯 . I think this could have quite an impact in terms of app startup!

  8. claudioredi commented on Nov 5, 2019

    @claudioredi

    @jonpryor Was this already released on stable channel?

  9. jonpryor commented on Mar 9, 2022

    @jonpryor
    Contributor

    @claudioredi: yes, this was released as part of Visual Studio 16.1 / Xamarin.Android 9.2. See also:

    Note that $(AndroidEnablePreloadAssemblies) is True by default in "Classic" Xamarin.Android, and False by default in .NET 6.

  10. ghost locked as resolved and limited conversation to collaborators on Jun 8, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions