Xamarin.Forms Performance on Android

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

  • 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<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;
}

嗯,这会带来一些想法:

  • 这些在哪里被回收/处理?每个自定义渲染器都负责自己做...
  • 有什么东西缓存这些?不。
  • 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 这样的小图像,向下滚动也会很快达到图像无法加载的极限。在此处 找到的特定于图片的示例中,请注意图片导致应用程序崩溃的速度:

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<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.

有没有捕获?

显然有一些问题:

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

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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,098评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,213评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,960评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,519评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,512评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,533评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,914评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,804评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,563评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,644评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,350评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,933评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,908评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,146评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,847评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,361评论 2 342