Xamarin.Forms Performance on Android

link all assemblies = Sdk and User Assemblies

一些参考文章

已经有相当多关于如何提高 Xamarin.Forms 性能的文章:

  • 阅读关于Xamarin.Forms performance 的官方文档
  • Jason Smith's Xamarin Forms Performance Tips
  • 五个关于降低 Xamarin.Forms App 启动时间的建议

建议先这些文章,对 Xamarin.Forms 的性能有一些了解之后,我们就可以更方便进行深入的了解......

测量当前 App 性能

在进行任何与性能相关的工作之前,我们需要确保对应用程序中的当前性能有一个正确的了解。不幸的是,我没有在 Xamarin.Forms 应用程序上找到很多指导,但我会分享我一直在使用的方法。

如果只是对普通 C# 代码进行 基准测试,那我们可以使用一些现有的基准测试库:

  • BenchmarkDotNet
  • NBench

这两个库都可以在您的桌面计算机上运行,​​以便在“单元测试”级别上按照您的想法计算 C# 代码。如果您想在共享的 C# 代码中计算性能,那么这就是要走的路。

不幸的是,Xamarin.Forms 应用程序(甚至是经典的 Xamarin 应用程序)中的计时性能并不那么容易。这将使您的应用程序更多地处于“集成测试”级别,并且我还没有找到一个可以执行此操作的库。为了使事情变得更加复杂,计算页面出现在 Xamarin.Forms 所需的时间需要在不同的类之间进行更改:您的 activity/controllerXF 页面以及可能的 custom renderers.

所以我的方法是使用如下的静态类,允许您在应用程序中命名不同的时间间隔:

using System;
using System.Collections.Concurrent;
using System.Diagnostics;

namespace xfperf
{
    public static class Profiler
    {
        static readonly ConcurrentDictionary watches = new ConcurrentDictionary();

        public static void Start(object view)
        {
            Start(view.GetType().Name);
        }

        public static void Start(string tag)
        {
            Console.WriteLine("Starting Stopwatch {0}", tag);

            var watch =
                watches[tag] = new Stopwatch();
            watch.Start();
        }

        public static void Stop(string tag)
        {
            Stopwatch watch;
            if (watches.TryGetValue(tag, out watch))
            {
                Console.WriteLine("Stopwatch {0} took {1}", tag, watch.Elapsed);
            }
        }
    }
}

然后,为了计时 Xamarin.Forms Android 应用程序,我在各个地方调用 Start/Stop 方法:

  • Application.OnCreate - Start "OnResume" interval
  • MainActivity.OnResume - Stop "OnResume" interval
  • Put a Start/Stop around the Xamarin.Forms.Forms.Init() call

记录此类时间时,请确保在 真机 上以 Release 模式测试您的应用。请务必多次记录,因为您的时间会有所不同。对于如何设置,可以参考(请注意这是仅适用于 Android) 这里.

与此同时,最好尝试使用 Xamarin Profiler。开发人员一般都非常擅长发现问题,但是开发人员往往不会意识到你写的代码到底会让程序变慢。Xamarin Profiler 应该很好地发现以下内容:内存泄漏,产生大量垃圾的代码,hot paths ...

Linker 和 Java Binding Projects

Xamarin.Android Binding Projects 生成 C# 代码,使我们能够从 C# 调用 Java API。与任何库一样,您肯定不会使用大多数 API - 通常是特定于您的应用程序的一小部分。

默认的 Linker 选项 SDK Only 不会剥离依赖程序集中的代码,因此您的应用程序将包含许多您不需要的已编译 C# 代码。

现在想想 Android support libraries:有几千个你肯定不会使用的 API。所有这些 C# 代码都位于与您的应用程序捆绑在一起的程序集中,永远不会被调用...

我的初步实验表明,几乎每个 Linker 选项设置为 SDK Only 并使用 support library 的应用程序都有大约4 MB 大小的 .NET 程序集是用不到的!这肯定会影响启动时间!实际上每个 Xamarin.Android 应用程序都使用 Android support libraries,因此我开始寻找改进方法。

我发现在 binding projects 中使用 [assembly:LinkerSafe] 属性是完全缓解此问题的有效方法。我向 support libraries 发送了一个 PR,可以在每个 Xamarin.Android 应用程序上有效地节省 3.8 MBAPK 大小!这些改进应该在下一版本的 Android support libraries(27.x)中提供,到时候 Linker 选项设置为 link all assemblies 就行了。

注意:遗憾的是,现有的应用程序在切换到 Sdk and User Assemblies 可能需要一些工作。如果您的应用使用 反射 等,则可能需要添加 [Preserve] 属性或执行类似操作。

So for the the future, my guidance on linking is:

  • 在新项目中使用Sdk and User Assemblies选项,或在现有应用程序启用它(可能会报很多错,需要你耐心的 debug)。始终在 Release模 式下对应用程序进行手动测试(勾选相应的 Linker 选项)。
  • 在您自己的绑定项目中启用 [assembly:LinkerSafe],并将其建议给其他开发人员。
  • 获取 27.x支持库(和 API 27 ),可在 Xamarin.Android 8.2中获得

Proguard

如果您要为您的应用设置 linking,下一个显而易见的步骤是 proguard。这不会直接有益于 Android 上的Xamarin.Forms 应用程序的性能,但它具有无数的其他好处.

较小的 dex 文件(编译后的 Java 代码)意味着:

  • 较小的APK大小
  • 启动时间的改进
  • 帮助您保持在 dex 限制之下,以避免 multi-dex

就像启用 Sdk and User Assemblies 一样,proguard 可能会导致您项目编译失败,必须解决相应一些问题。有关设置 proguard 的完整详细信息,请在此处 深入了解 Jon Douglas 的关于 proguard 的详细解释。

Images & Bitmaps

Android.Graphics.Bitmap 类是每个 Xamarin.Android 开发者开发 app 时潜在的祸根。Due to the nature of the relationship between the C# and Java worlds, 如果您没有正确清理 Bitmaps 时,GC 可以达到您的应用程序将彻底崩溃的程度 OOM)。

如,如果我们考虑 Bitmap 对象的两个方面:

  • C# side - a few bytes, mainly a few fields holding IntPtrs to the Java world
  • Java side - potentially huge, contains the Byte[] that could be megabytes in size

当然,Mono GC 不跟踪 Bitmap 的全部大小,因为它的 C# 端非常小。这可能会导致您的应用程序在JavaC# 端快速出现内存异常。

通常在 Xamarin.Android 应用程序中,我采用以下方法:

  • 不要使用 Bitmap,使用 AndroidResourceresource system. 原生的 API 非常有效。下载的图像可能是唯一需要 Bitmap 的情况。
  • 如果 必须 使用 Bitmap,请将它们缓存在内存中并重用它们。谷歌甚至建议 Java 开发人员使用 LRUCache 类。
  • 完成 Bitmap 后,显式调用 Recycle(), 然后调用 Dispose().

Xamarin.Forms and Bitmap

因此,为了了解它在 Xamarin.Forms 中是如何工作的,让我们来看看 Android 的默认的 IImageSourceHandler

public async Task LoadImageAsync(ImageSource imagesource, Context context, CancellationToken cancelationToken = default(CancellationToken))
{
  string file = ((FileImageSource)imagesource).File;
  Bitmap bitmap;
  if (File.Exists (file))
    bitmap = !DecodeSynchronously ? (await BitmapFactory.DecodeFileAsync (file).ConfigureAwait (false)) : BitmapFactory.DecodeFile (file);
  else
    bitmap = !DecodeSynchronously ? (await context.Resources.GetBitmapAsync (file).ConfigureAwait (false)) : context.Resources.GetBitmap (file);


  if (bitmap == null)
  {
    Log.Warning(nameof(FileImageSourceHandler), "Could not find image or image file was invalid: {0}", imagesource);
  }

  return bitmap;
}

嗯,这会带来一些想法:

  • 这些在哪里被回收/处理?每个自定义渲染器都负责自己做...
  • 有什么东西缓存这些?不。
  • Android资源作为Bitmap加载!卧槽!

不幸的是,Xamarin.FormsAPI 设计在某种程度上让我们陷入了困境。他们选择的设计完全有意义:图像可以来自 文件URI.NET嵌入式资源Android资源Xamarin.Forms 应该完全使用Android.Graphics.Bitmap,因为它涵盖了所有情况。

有点不幸的是 Android上的图像密集型Xamarin.Forms应用程序会发生什么:它可以达到它落空的程度。

It is somewhat unfortunate what can happen to an image-heavy Xamarin.Forms app on Android: it can get to a point where it falls over.

我们假设您的应用中有一些这样的代码:

var grid = new Grid();
grid.ColumnDefinitions.Add(new ColumnDefinition());
grid.ColumnDefinitions.Add(new ColumnDefinition());
grid.ColumnDefinitions.Add(new ColumnDefinition());
grid.ColumnDefinitions.Add(new ColumnDefinition());

for (int i = 0; i < 100; i++)
{
    grid.RowDefinitions.Add(new RowDefinition());

    for (int j = 0; j < 4; j++)
    {
        var image = new Image
        {
            Source = ImageSource.FromFile("some_resource");
        };
        Grid.SetRow(image, i);
        Grid.SetColumn(image, j);
        grid.Children.Add(image);
    }
}
yourScrollView.Content = grid;

即使像 100x100 这样的小图像,向下滚动也会很快达到图像无法加载的极限。在此处 找到的特定于图片的示例中,请注意图片导致应用程序崩溃的速度:

Xamarin.Forms Performance on Android_第1张图片
AndroidXFImages.png

在运行应用程序时,您还会很快注意到它的缓慢和可笑的控制台输出量。 内存不足异常在应用程序加载后的相当一段时间内发生...

那么ListView呢?

In the above example, we are loading the images up front and pay for the performance cost of the entire ScrollView on load. ListView can virtualize items as you scroll (ListViewCachingStrategy), but in some ways it can be worse. Let's say you use ImageCell (or even just a ViewCell with a complex layout with Image). Only the visible cells will get loaded up front on the page, and subsequent Bitmaps will get created as you scroll. This means the page will load alot quicker, but you run into sluggishness while scrolling.

To understand what's happening let's explore what happens to a data-bound ListView Cell while scrolling:

  • The Cell is created, along with the native views, custom renderers, etc.
  • The BindableProperty of the ImageSource gets set via data-binding (BindingContext is set)
  • The IImageSourceHandler is invoked, creating an Android.Graphics.Bitmap
  • The Bitmap is passed to the native control, and the C# instance is Dispose()'d immediately. Note XF can't call Recycle(), since we don't know when the native side is done with the Bitmap.
  • The Cell gets scrolled off screen, where it can be recycled. The Cell's BindingContext is set to null.
  • The native control's image is cleared
  • Repeat...

注意这里创建了多少 Android.Graphics.Bitmaps ...如果 ListView 中的两行使用相同的图像,它们每个都使用完全相同的图像的副本。如果您将一个单元格从屏幕滚动并将其恢复,它会在将一个新的 Bitmap对象带回屏幕时加载它。

请记住,我并不批评 Xamarin.Forms 如何实现这一点。在开发 XF 时我可能会到达同一个地方,考虑到他们在构建他们的惊人框架时试图模仿的类似 WPFAPI

有修复吗?

幸运的是,经过一番挖掘后,我发现了一种在您自己的应用中解决此问题的极其简单的方法。

  • 第1步:仅为您的图像使用 AndroidResource。绝对没有别的!
  • 第2步:使用我的以下 图像处理程序
using System.Threading;
using System.Threading.Tasks;
using Android.Content;
using Android.Graphics;
using Xamarin.Forms;
using Xamarin.Forms.Platform.Android;

[assembly: ExportImageSourceHandler(typeof(FileImageSource), typeof(xfperf.FileImageSourceHandler))]

namespace xfperf
{
    public class FileImageSourceHandler : IImageSourceHandler
    {
        public Task LoadImageAsync(ImageSource imagesource, Context context, CancellationToken cancelationToken = default(CancellationToken))
        {
            return Task.FromResult(null);
        }
    }
}

WTF?!? 这是如何运作的?

在查看 Xamarin.Forms 源代码时,我注意到了这个回退逻辑的小块:

if (bitmap == null && source is FileImageSource)
    imageView.SetImageResource(ResourceManager.GetDrawableByName(((FileImageSource)source).File));

由于这似乎是为 Imagefast rendererlder one)和 ImageCell 设置的,因此我们可以利用这种回退逻辑来满足我们的需求。将此图像处理程序添加到我的示例中时,它会加载并快速滚动。没有 out of memory errors - 运行的很完美。

It has the exact same performance you would expect an image-heavy classic Xamarin.Android app to behave using AndroidResource.

有没有捕获?

显然有一些问题:

  • 任何使用 ImageSourcecustom renderers 都需要这个 AndroidResource 回退逻辑
  • 如果您还需要直接从磁盘加载图像文件,则需要将自定义逻辑添加到 ImageHandler

我在这里看到的唯一另一个问题是 XF 的ResourceManager类使用了很多 System.Reflection。也许可以在这里添加一些缓存代码以进一步加快速度?或者使用 Android API 来从其名称中获取资源整数 Id

原文链接: http://jonathanpeppers.com/Blog/xamarin-forms-performance-on-android

你可能感兴趣的:(Xamarin.Forms Performance on Android)