link all assemblies = Sdk and User Assemblies
一些參考文章
已經(jīng)有相當多關于如何提高 Xamarin.Forms 性能的文章:
- 閱讀關于Xamarin.Forms performance 的官方文檔
- Jason Smith's Xamarin Forms Performance Tips
- 五個關于降低 Xamarin.Forms App 啟動時間的建議
建議先這些文章,對 Xamarin.Forms 的性能有一些了解之后,我們就可以更方便進行深入的了解......
測量當前 App 性能
在進行任何與性能相關的工作之前,我們需要確保對應用程序中的當前性能有一個正確的了解。不幸的是,我沒有在 Xamarin.Forms 應用程序上找到很多指導,但我會分享我一直在使用的方法。
如果只是對普通 C# 代碼進行 基準測試,那我們可以使用一些現(xiàn)有的基準測試庫:
這兩個庫都可以在您的桌面計算機上運行,??以便在“單元測試”級別上按照您的想法計算 C# 代碼。如果您想在共享的 C# 代碼中計算性能,那么這就是要走的路。
不幸的是,Xamarin.Forms 應用程序(甚至是經(jīng)典的 Xamarin 應用程序)中的計時性能并不那么容易。這將使您的應用程序更多地處于“集成測試”級別,并且我還沒有找到一個可以執(zhí)行此操作的庫。為了使事情變得更加復雜,計算頁面出現(xiàn)在 Xamarin.Forms 所需的時間需要在不同的類之間進行更改:您的 activity/controller,XF 頁面以及可能的 custom renderers.
所以我的方法是使用如下的靜態(tài)類,允許您在應用程序中命名不同的時間間隔:
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 應用程序,我在各個地方調(diào)用 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。開發(fā)人員一般都非常擅長發(fā)現(xiàn)問題,但是開發(fā)人員往往不會意識到你寫的代碼到底會讓程序變慢。Xamarin Profiler 應該很好地發(fā)現(xiàn)以下內(nèi)容:內(nèi)存泄漏,產(chǎn)生大量垃圾的代碼,hot paths ...
Linker 和 Java Binding Projects
Xamarin.Android Binding Projects 生成 C# 代碼,使我們能夠從 C# 調(diào)用 Java API。與任何庫一樣,您肯定不會使用大多數(shù) API - 通常是特定于您的應用程序的一小部分。
默認的 Linker 選項 SDK Only 不會剝離依賴程序集中的代碼,因此您的應用程序?qū)S多您不需要的已編譯 C# 代碼。
現(xiàn)在想想 Android support libraries:有幾千個你肯定不會使用的 API。所有這些 C# 代碼都位于與您的應用程序捆綁在一起的程序集中,永遠不會被調(diào)用...
我的初步實驗表明,幾乎每個 Linker 選項設置為 SDK Only 并使用 support library 的應用程序都有大約4 MB 大小的 .NET 程序集是用不到的!這肯定會影響啟動時間!實際上每個 Xamarin.Android 應用程序都使用 Android support libraries,因此我開始尋找改進方法。
我發(fā)現(xiàn)在 binding projects 中使用 [assembly:LinkerSafe] 屬性是完全緩解此問題的有效方法。我向 support libraries 發(fā)送了一個 PR,可以在每個 Xamarin.Android 應用程序上有效地節(jié)省 3.8 MB 的 APK 大小!這些改進應該在下一版本的 Android support libraries(27.x)中提供,到時候 Linker 選項設置為 link all assemblies 就行了。
注意:遺憾的是,現(xiàn)有的應用程序在切換到
Sdk and User Assemblies可能需要一些工作。如果您的應用使用反射等,則可能需要添加[Preserve]屬性或執(zhí)行類似操作。
So for the the future, my guidance on linking is:
- 在新項目中使用
Sdk and User Assemblies選項,或在現(xiàn)有應用程序啟用它(可能會報很多錯,需要你耐心的debug)。始終在Release模 式下對應用程序進行手動測試(勾選相應的Linker選項)。 - 在您自己的綁定項目中啟用
[assembly:LinkerSafe],并將其建議給其他開發(fā)人員。 - 獲取
27.x支持庫(和API 27),可在Xamarin.Android8.2中獲得
Proguard
如果您要為您的應用設置 linking,下一個顯而易見的步驟是 proguard。這不會直接有益于 Android 上的Xamarin.Forms 應用程序的性能,但它具有無數(shù)的其他好處.
較小的 dex 文件(編譯后的 Java 代碼)意味著:
- 較小的APK大小
- 啟動時間的改進
- 幫助您保持在
dex限制之下,以避免multi-dex
就像啟用 Sdk and User Assemblies 一樣,proguard 可能會導致您項目編譯失敗,必須解決相應一些問題。有關設置 proguard 的完整詳細信息,請在此處 深入了解 Jon Douglas 的關于 proguard 的詳細解釋。
Images & Bitmaps
該 Android.Graphics.Bitmap 類是每個 Xamarin.Android 開發(fā)者開發(fā) app 時潛在的禍根。Due to the nature of the relationship between the C# and Java worlds, 如果您沒有正確清理 Bitmaps 時,GC 可以達到您的應用程序?qū)氐妆罎⒌某潭?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# 端非常小。這可能會導致您的應用程序在Java 或 C# 端快速出現(xiàn)內(nèi)存異常。
通常在 Xamarin.Android 應用程序中,我采用以下方法:
- 不要使用
Bitmap,使用AndroidResource和resource system. 原生的API非常有效。下載的圖像可能是唯一需要Bitmap的情況。 - 如果 必須 使用
Bitmap,請將它們緩存在內(nèi)存中并重用它們。谷歌甚至建議Java開發(fā)人員使用 LRUCache 類。 - 完成
Bitmap后,顯式調(diào)用Recycle(), 然后調(diào)用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.Forms 的 API 設計在某種程度上讓我們陷入了困境。他們選擇的設計完全有意義:圖像可以來自 文件、URI、.NET嵌入式資源 和 Android資源。Xamarin.Forms 應該完全使用Android.Graphics.Bitmap,因為它涵蓋了所有情況。
有點不幸的是 Android上的圖像密集型Xamarin.Forms應用程序會發(fā)生什么:它可以達到它落空的程度。
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 這樣的小圖像,向下滾動也會很快達到圖像無法加載的極限。在此處 找到的特定于圖片的示例中,請注意圖片導致應用程序崩潰的速度:

在運行應用程序時,您還會很快注意到它的緩慢和可笑的控制臺輸出量。 內(nèi)存不足異常在應用程序加載后的相當一段時間內(nèi)發(fā)生...
那么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...
注意這里創(chuàng)建了多少 Android.Graphics.Bitmaps ...如果 ListView 中的兩行使用相同的圖像,它們每個都使用完全相同的圖像的副本。如果您將一個單元格從屏幕滾動并將其恢復,它會在將一個新的 Bitmap對象帶回屏幕時加載它。
請記住,我并不批評 Xamarin.Forms 如何實現(xiàn)這一點。在開發(fā) XF 時我可能會到達同一個地方,考慮到他們在構建他們的驚人框架時試圖模仿的類似 WPF 的 API。
有修復嗎?
幸運的是,經(jīng)過一番挖掘后,我發(fā)現(xiàn)了一種在您自己的應用中解決此問題的極其簡單的方法。
- 第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));
由于這似乎是為 Image(fast renderer 和 lder 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.
有沒有捕獲?
顯然有一些問題:
- 任何使用
ImageSource的custom renderers都需要這個AndroidResource回退邏輯 - 如果您還需要直接從磁盤加載圖像文件,則需要將自定義邏輯添加到
ImageHandler
我在這里看到的唯一另一個問題是 XF 的ResourceManager類使用了很多 System.Reflection。也許可以在這里添加一些緩存代碼以進一步加快速度?或者使用 Android API 來從其名稱中獲取資源整數(shù) Id。
原文鏈接: http://jonathanpeppers.com/Blog/xamarin-forms-performance-on-android