Xamarin

Xamarin.Forms Performance on And

2018-10-14  本文已影响15人  Xamarin信仰中心

link all assemblies = Sdk and User Assemblies

一些参考文章

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

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

测量当前 App 性能

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

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

这两个库都可以在您的桌面计算机上运行,​​以便在“单元测试”级别上按照您的想法计算 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<string, Stopwatch> watches = new ConcurrentDictionary<string, Stopwatch>();

        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 方法:

记录此类时间时,请确保在 真机 上以 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:

Proguard

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

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

就像启用 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 对象的两个方面:

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

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

Xamarin.Forms and Bitmap

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

public async Task<Bitmap> 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;
}

嗯,这会带来一些想法:

不幸的是,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 这样的小图像,向下滚动也会很快达到图像无法加载的极限。在此处 找到的特定于图片的示例中,请注意图片导致应用程序崩溃的速度:

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:

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

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

有修复吗?

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

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<Bitmap> LoadImageAsync(ImageSource imagesource, Context context, CancellationToken cancelationToken = default(CancellationToken))
        {
            return Task.FromResult<Bitmap>(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.

有没有捕获?

显然有一些问题:

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

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

上一篇下一篇

猜你喜欢

热点阅读