Xamarin.Forms之跨平台性能
1、使用 Profiler
开发应用程序时,请务必先分析代码,再尝试对代码进行优化。Profiling是一种用于确定代码优化在减少性能问题方面将发挥最大作用的技术。 探查器跟踪应用程序的内存使用情况,并记录应用程序中方法的运行时间。 这些数据有助于在应用程序的执行路径和代码的执行成本之间导航,从而可以发现最佳的优化机会。
Xamarin Profiler 将测量、评估和帮助查找应用程序中的性能相关问题。 可用于从 Visual Studio for Mac 或 Visual Studio 内部分析 Xamarin.iOS 和 Xamarin.Android 应用程序。 有关 Xamarin Profiler 的详细信息,请参阅 Xamarin Profiler 简介。
分析应用时建议采用以下最佳做法:
- 使用真机测试,而不是用模拟器,因为模拟器可能误报应用程序性能。
- 使用多种不同型号 不同配置的真机测试。
- 关闭所有其他应用程序,减少不必要的干扰。
从历史上看,Mono具有功能强大的命令行分析器,用于收集有关在Mono运行时中运行的程序的信息,称为Mono日志分析器。 Xamarin Profiler是Mono日志探查器的图形界面,并支持对Mac上的Android,iOS,tvOS和Mac应用程序以及Windows上的Android,iOS和tvOS应用程序进行性能分析。
Xamarin Profiler有许多可用于性能分析的工具:分配、周期和时间分析器。 下面探讨了这些工具的测量内容以及它们如何分析您的应用程序,并阐明了每个屏幕上显示的数据的含义。
1.1、下载和安装
Xamarin Profiler是一个独立的应用程序,并且与Visual Studio for Mac和Visual Studio集成在一起,从而可以在IDE中进行性能分析。
下载适合您平台的安装软件包:
下载完成后,启动安装程序以将Xamarin Profiler添加到您的系统。
选择Android或者ios程序,在分析菜单中:
1.2、Profilers and Profiling
Profiling是应用程序开发中的一个重要且通常被忽略的步骤,Profiling是动态程序分析的一种形式-它在程序运行和使用时对其进行分析。
Profiler是一种数据挖掘工具,它收集有关时间复杂性,特定方法的使用以及分配的内存的信息,Profiler使您能够深入研究和分析这些指标,以查明代码中的问题区域。
在设计和开发应用程序时,不要过早优化很重要; 也就是说,将时间花在很少访问的区域上来开发代码,这就是分析的力量。 探查器可洞悉代码库中最常用的部分,并帮助您确定应花时间进行改进的区域,开发人员应注意了解大部分时间在应用程序中所花的时间,以及应用程序如何使用内存。
概要分析对所有类型的开发都有帮助,但在移动开发中尤其重要。 在移动平台上,未优化的代码比在台式机上更加引人注目,而您的应用能否成功取决于能否有效运行的美观且经过优化的代码。
分析方法:
使用Xamarin Profiler剖析应用程序的方法有多种,包括内存剖析和统计采样,这些分别通过“分配”和“时间分析器”工具执行。
探查器是与VS是分开的进程,因此,除了从Visual Studio中启动之外,它还可以用作独立的应用程序,以检查由mono log profiler生成的.exe和.mlpd文件。
注: you can only profile Debug configurations
1.3、Profiler Basics
在成功对应用程序进行配置文件之前,您需要在应用程序的“项目选项”中允许“Profiling”
安装Profiler后,Android程序默认可以打开Xamarin.Profiler了,而IOS程序需要在项目属性中设置:
探查器可用于测量内存和性能。 它通过分配和时间探查器检测来实现此目标,我们将在下一节将对此进行详细探讨。
保存和加载探查器会话
若要随时保存分析会话,请从探查器 菜单栏中选择 " > 文件" "另存为 ... "。 这会将文件保存为 .mlpd_格式,这是一种特殊的高度压缩格式,用于分析数据。
安装后,可以在应用程序目录中找到 Xamarin Profiler 应用程序:
可以通过打开独立的应用程序,选择 "选择目标" 并加载该文件,将 .mlpd文件加载到探查器中。
1.4、探查器功能
Xamarin Profiler 由五个部分组成,
- 工具栏-位于探查器顶部,这提供了启动/停止分析、选择目标进程、查看应用程序运行时间以及组成探查器应用程序的拆分视图的选项。
- 检测列表–列出了为分析会话加载的所有工具。
- 绘图图–这些图表水平与检测列表中的相关乐器相关。 可以使用滑块(在时间探查器下显示)来更改刻度。
- 检测详细信息区域-包含当前乐器的选定视图显示的数据。 在下面的部分中,我们将更详细地介绍这些视图。
- 检查器视图–其中包含可由分段控件选择的部分。 这些部分依赖于所选乐器,并包括:配置设置、统计信息、堆栈跟踪信息和根的路径。
1.4.1、分配
分配工具在创建和收集垃圾时提供有关应用程序中对象的详细信息。
探查器的顶部是分配图,该图显示了性能分析期间按固定间隔分配的内存量。 当前,分配图是分配的总数,而不是该时间点的堆大小。 从某种意义上说,它将永远不会下降,只会不断增加,这包括分配在堆栈上的对象。 根据使用的运行时版本,图表可能看起来有所不同-即使对于同一应用程序也是如此。
分配工具中有不同的数据视图,使开发人员可以分析其应用程序如何使用和释放内存。 这些视图如下所述:
- 分配:显示所有分配的列表,并按类名称对其进行分组。 这为使用的类和方法提供了很好的概览、使用的频率以及使用的类的集合大小。 双击类将显示分配的内存:
检查器的“分配”视图提供用于对对象进行过滤和分组的选项,提供有关已分配内存和最高分配的统计信息,以及“堆栈跟踪”和“根路径”的视图。
- 调用树:显示应用程序中所有线程的整个调用树,并包括有关在每个节点上分配的内存的信息。 在列表中选择一个元素时,所有同级节点将显示为灰色。 您可以展开树或双击该元素以对其进行深入研究。显示此数据视图时,可以使用显示设置检查器视图来更改其显示方式。 当前有两种选择:
反向调用树–这从上到下考虑堆栈跟踪。 这是一个方便的视图选项,因为它指示CPU花费时间的最深层方法。
按线程分隔–此选项按线程组织调用树。
- 快照:此窗格显示有关内存快照的信息。 若要在分析实时应用程序时生成这些内容,请在工具栏上单击要查看保留和释放内存的每个点的_相机_按钮。 然后,你可以单击每个快照以了解在后台发生的情况。 请注意,仅当实时分析应用程序时,才可以执行快照。
1.4.2、时间分析器
Time Profiler仪器精确地测量在应用程序中每个方法花费了多少时间。 应用程序会定期暂停,并在每个活动线程上运行堆栈跟踪。 检测详细信息区域中的每一行都显示了已遵循的执行路径。
如下图的屏幕截图所示,该绘图图显示了该应用在运行时收到的样本数量:
"调用树"-显示每个方法所用的时间量。
1.4.3、循环
通过使用C#和F#托管代码,它可能很常见,并且很容易创建对对象的引用,这将永远不会被释放。 此检测允许你查找这些对象,并显示应用程序中引用的循环。
2、释放 IDisposable 资源
IDisposable接口提供了一种释放资源的机制,它提供了Dispose方法,应实现该方法以显式释放资源,IDisposable不是析构函数,仅应在以下情况下实现:
- 当类拥有非托管资源时,需要释放的典型非托管资源包括文件,流和网络连接。
- 当类拥有托管IDisposable资源时。
然后,类型使用者可以调用IDisposable.Dispose实现以在不再需要该实例时释放资源。 有两种方法可以实现此目的:
- 通过将IDisposable对象包装在using语句中。
- 通过将对IDisposable的调用包装起来,放在try / finally块中。
在 using 语句中包装 IDisposable 对象
public void ReadText (string filename) { ... string text; using (StreamReader reader = new StreamReader (filename)) { text = reader.ReadToEnd (); } ... }
StreamReader类实现IDisposable,并且using语句提供了一种方便的语法,该语法在超出范围之前在StreamReader对象上调用StreamReader.Dispose方法。 在using块中,StreamReader对象是只读的,无法重新分配。 using语句还确保即使发生异常也可以调用Dispose方法,因为编译器为try / finally块实现了中间语言(IL)。
在 Try/Finally 块中包装对 IDisposable.Dispose 的调用
public void ReadText (string filename) { ... string text; StreamReader reader = null; try { reader = new StreamReader (filename); text = reader.ReadToEnd (); } finally { if (reader != null) { reader.Dispose (); } } ... }
3、取消订阅事件
为了防止内存泄漏,应该在释放订阅者对象之前取消订阅事件。 在取消订阅事件之前,发布对象中事件的委托人具有对该委托的引用,该委托封装了订阅者的事件处理程序。 只要发布对象拥有此引用,垃圾回收就不会回收订阅对象的内存。
public class Publisher { public event EventHandler MyEvent; public void OnMyEventFires () { if (MyEvent != null) { MyEvent (this, EventArgs.Empty); } } } public class Subscriber : IDisposable { readonly Publisher publisher; EventHandler handler; public Subscriber (Publisher publish) { publisher = publish; handler = (sender, e) => { Debug.WriteLine ("The publisher notified the subscriber of an event"); }; publisher.MyEvent += handler; } public void Dispose () { publisher.MyEvent -= handler; } }
handler字段维护对匿名方法的引用,并用于事件订阅和取消订阅。
4、使用弱引用来阻止不变对象
iOS 开发者应查看有关在 iOS 中避免循环引用的文档,确保其应用高效使用内存。
5、延迟创建对象的开销
可使用延迟初始化来延迟创建对象,直至首次使用该对象。 此方法主要用于提升性能、避免计算和降低内存需求。
出现以下 2 种情况时,建议使用延迟初始化来创建开销很大的对象:
- 应用程序可能不会使用该对象。
- 创建对象之前,必须先完成其他大开销操作。
void ProcessData(bool dataRequired = false) { Lazy<double> data = new Lazy<double>(() => { return ParallelEnumerable.Range(0, 1000) .Select(d => Compute(d)) .Aggregate((x, y) => x + y); }); if (dataRequired) { if (data.Value > 90) { ... } } } double Compute(double x) { ... }
延迟初始化在第一次访问Lazy<T>.Value属性时发生。 包装的类型将在首次访问时创建并返回,并存储以供将来访问。
有关延迟初始化的详细信息,请参阅延迟初始化。
6、实施异步操作
.NET提供了许多API的异步版本。与同步API不同,异步API确保活动执行线程永远不会在相当长的时间内阻塞调用线程。因此,从UI线程调用API时,请使用异步API(如果有)。这将使UI线程保持畅通,这将有助于改善用户对应用程序的体验。
另外,应在后台线程上执行长时间运行的操作,以避免阻塞UI线程。 .NET提供了async和await关键字,这些关键字允许编写异步代码,该代码在后台线程上执行长时间运行的操作,并在完成时访问结果。但是,尽管可以使用await关键字异步执行长时间运行的操作,但这不能保证该操作将在后台线程上运行。相反,这可以通过将长时间运行的操作传递给Task.Run来实现,如以下代码示例所示:
public class FaceDetection { ... async void RecognizeFaceButtonClick(object sender, EventArgs e) { await Task.Run(() => RecognizeFace ()); ... } async Task RecognizeFace() { ... } }
RecognizeFace方法在后台线程上执行,而RecognizeFaceButtonClick方法要等到RecognizeFace方法完成后再继续。
长时间运行的操作也应支持取消操作。 例如,如果用户在应用程序内导航,则继续长时间运行的操作可能变得不必要。 实现取消的模式如下:
- 创建一个CancellationTokenSource实例。 该实例将管理并发送取消通知。
- 将CancellationTokenSource.Token属性值传递给应该可取消的每个任务。
- 为每个任务提供一种机制来响应取消。
- 调用CancellationTokenSource.Cancel方法以提供取消通知。
有关详细信息,请参阅异步支持概述。
7、缩减应用程序大小
务必了解每个平台上的编译流程,才能了解应用程序可执行文件大小的来源:
- iOS 应用程序预先 (AOT) 编译为 ARM 汇编语言。 其中包括 .NET Framework,并且仅在启用了适当的链接器选项时才去除未使用的类。
- Android 应用程序编译为中间语言 (IL),并打包了 MonoVM 和实时 (JIT) 编译。 仅在启用了适当的链接器选项时才去除未使用的框架类。
- Windows Phone 应用程序编译为 IL,并由内置运行时执行。
此外,如果应用程序广泛使用泛型,则最终的可执行文件大小将进一步增加,因为它包含无限可能性的本机编译版本。【尽量不用泛型】
为帮助缩减应用程序的大小,Xamarin 平台将链接器包含在了生成工具中。 链接器默认处于禁用状态,必须在应用程序的项目选项中启用。 在生成时,链接器会对应用程序执行静态分析,确定应用程序实际使用的类型和成员。 然后,从应用程序中删除所有未使用的类型和方法。
链接器提供 3 个可用于控制其行为的设置:
- 不链接 - 链接器不会删除任何未使用的类型和方法。 出于性能方面的考虑,默认对调试版本使用此设置。
- 仅链接 Framework SDK/SDK 程序集 - 此设置只会缩减 Xamarin 提供的程序集大小。 用户代码不受影响。
- 链接所有程序集 - 这是一种针对 SDK 程序集和用户代码的更高性能优化。 对于绑定,这将删除未使用的支持字段并淡化每个实例(或绑定对象),以占用较少内存。
应慎用“链接所有程序集” ,因为它可能以意外方式中断应用程序。 链接器执行的静态分析可能无法正确识别所有必需代码,从而导致从编译应用程序中删除过多代码。 应用程序崩溃时,只会在运行时表现出这种情况。 因此,务必在更改链接器行为后全面测试应用程序。
如果测试结果表明链接器错误删除了某个类或方法,则可使用以下属性之一来标记未静态引用但应用程序需要的类型或方法:
Xamarin.iOS.Foundation.PreserveAttribute
- 此属性适用于 Xamarin.iOS 项目。Android.Runtime.PreserveAttribute
- 此属性适用于 Xamarin.Android 项目。
例如,可能需要保留经过动态实例化的类型的默认构造函数。 此外,使用 XML 序列化可能需要保留类型的属性。
有关详细信息,请参阅用于 iOS 的链接器和用于 Android 的链接器。
8、优化图像资源
图像是应用程序使用的一些最昂贵的资源,通常以高分辨率捕获。 尽管这可创建包含完整详细信息的生动图像,但显示此类图像的应用程序往往需要占用更多 CPU 才能解码图像和更多内存来存储已解码图像。 如果需要缩小图像才能显示,那么在内存中解码高分辨率图像就是一种资源浪费。 相反,可通过创建已存储图像的多个分辨率版本(接近预计显示大小)来减少 CPU 使用率和内存占用量。 例如,在列表视图中显示的图像分辨率多数时候都比全屏显示的图像分辨率更低。 此外,还可以加载缩小版本的高分辨率图像,在最大程度降低对内存影响的同时有效地显示这些图像。 有关详细信息,请参阅高效加载大位图。
无论图像分辨率高低,显示图像资源都可能大大提高应用的内存占用量。 因此,仅应在必要时创建图像,应用程序不再需要图像后应立即将其释放。
9、缩短应用程序激活期限
所有应用程序都有激活期限 ,这是指应用程序启动和应用程序可供使用之间的时间段。 此激活期限是用户对应用程序的第一印象,因此缩短激活期限和用户等待时间对于获得用户对应用程序的良好第一印象非常重要。
应用程序显示其初始 UI 之前,应提供指示用户应用程序正在启动的初始屏幕。 如果应用程序无法快速显示其初始 UI,初始屏幕应通过激活期限告知用户进度,让用户确信应用程序没有挂起,可通过进度栏或类似控件提供此确信通知。
激活期间,应用程序会执行激活逻辑,通常包括加载和处理资源。 通过确保所需资源已打包在应用内,而不用远程检索,可缩短激活期限。 例如,某些情况可能适合在激活期间加载存储在本地的占位符数据。 然后,显示初始 UI 后,用户便可与应用交互,占位符数据可逐渐替换为远程源。 此外,应用的激活逻辑仅应执行让用户开始使用应用程序所需的工作。 这在激活逻辑延迟加载其他程序集时可起到帮助,因为程序集在首次使用时需完成加载。
10、减少Web服务通信
从应用程序连接到Web服务可能影响应用程序性能。 例如,网络带宽使用量增加将导致设备电池使用量增加。 此外,用户可能在带宽受限的环境中使用应用程序。 因此,限制应用程序和 Web 服务之间的带宽利用率是明智之选。
减少应用程序带宽使用率的一种方法是:通过网络传输数据之前,先压缩数据。 但是,压缩过程中的其他 CPU 使用率也可能导致电池用量增加。 因此,决定是否通过网络移动压缩数据之前,应仔细对二者进行权衡。
要考虑的另一个问题是在应用程序和 Web 服务之间移动的数据格式。 两种主要格式为:可扩展标记语言 (XML) 和 JavaScript 对象表示法 (JSON)。 XML 是一种基于文本的数据交换格式,可生成相对较大的数据负载,因为它包含大量格式化字符。 JSON 是一种基于文本的数据交换格式,可生成压缩数据负载,这可在发送和接收数据时降低带宽需求。 因此,对于移动应用程序,通常首选 JSON 格式。
在应用程序和 Web 服务之间传输数据时,建议使用数据传输对象 (DTO)。 DTO 包含一组用于跨网络传输的数据。 利用DTO,通过一次远程调用即可传输更多数据,从而帮助减少应用程序进行远程调用的次数。 通常,传输较大数据负载的远程调用所需时间与仅传输小型数据负载的调用所需时间相差无几。
检索自Web服务的数据应在本地缓存,同时利用已缓存数据而不是反复从Web服务检索。 但是,采用此方法时,还应实现合适的缓存策略以便更新本地缓存中的数据(如果数据在 Web 服务中发生更改)。