Android 上的 Xamarin.Forms ListView OutOfMemoryError 异常

Posted

技术标签:

【中文标题】Android 上的 Xamarin.Forms ListView OutOfMemoryError 异常【英文标题】:Xamarin.Forms ListView OutOfMemoryError exception on Android 【发布时间】:2014-11-06 13:38:27 【问题描述】:

有人尝试过带有包含图像视图的 ItemTemplate 的 Xamarin.Forms Listview 吗?现在,当 ListView 包含 ca 20 或更多行时会发生什么?

就我而言,我在图像视图中加载了一个大小约为 4K 的 .png 文件。在应用程序因 OutOfMemoryError 崩溃之前最多显示 9 - 12 行。在 android Manifest 中请求大堆后,应用程序在 60 - 70 行后崩溃。

我知道 Xamarin 正在推广使用 BitmapFactory 类来缩小位图,但这不适用于 Xamarin Forms Image View(开箱即用)。

我正在尝试摆弄 ImageRenderer 的子类,看看是否可以添加 BitmapFactory.Options 属性以及这是否可以解决问题。

另外,我可能需要检查 Xamarin.Forms 是否在 ViewCell 滚动屏幕后处理(回收)包含的位图。

在开始这段旅程之前,我非常渴望获得任何可以使这更容易的 cmets 或认为不需要这个过程的更简单的解决方案。

期待……

【问题讨论】:

4K PNG 的位图大小是多少? PNG 未经压缩就存储在内存中。当转换为位图时,可以创建超过 1GB 数据的 4K PNG。另外,是的,您确实需要检查位图是否已处理。答案可能是,不,他们不是。 我当前使用的 PNG 被定义为 512 x 512。 因此 4kB PNG 需要 512 x 512 x 32 位 = 1MB 的 RAM 来存储/显示。所以很有可能你确实没有处理它们。 对不起,有点少。它是 24 位深度... 我可以肯定地确认加载到 ViewCell 中的图像视图“永远不会”被处理。与放置在表单上的图像视图相反。久经考验。不错的工作! Xamarin 伙计们! 【参考方案1】:

是的,我找到了解决方案。要遵循的代码。但在此之前,让我解释一下我做了什么。

因此,绝对需要我们自己来处理图像及其底层资源(位图或可绘制,无论您想如何称呼它)。基本上,它归结为处置本机“ImageRenderer”对象。

现在,没有办法从任何地方获取对该 ImageRenderer 的引用,因为要做到这一点,需要能够调用 Platform.GetRenderer(...)。无法访问“平台”类,因为它的范围被声明为“内部”。

所以,我别无选择,只能对 Image 类及其 (Android) 渲染器进行子类化并从内部销毁此渲染器本身(将“true”作为参数传递。不要尝试使用“false”) )。在渲染器内部,我连接到页面消失(在 TabbedPage 的情况下)。在大多数情况下,Page Disappear 事件不能很好地发挥作用,例如当页面仍在屏幕堆栈中但由于另一个页面正在其 Top 上绘制而消失。如果您处理图像,那么当页面再次被覆盖(显示)时,它将不会显示图像。在这种情况下,我们必须挂钩主导航页面的 'Popped' 事件。

我已尽力解释。剩下的——我希望——你能从代码中得到:

这是 PCL 项目中的图像子类。

using System;

using Xamarin.Forms;

namespace ApplicationClient.CustomControls

    public class LSImage : Image
    
    

以下代码在 Droid 项目中。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;

using Android.App;
using Android.Content;
using Android.OS;
using Android.Runtime;
using Android.Views;
using Android.Views.InputMethods;
using Android.Widget;
using Android.Util;
using Application.Droid.CustomControls;
using ApplicationClient.CustomControls;
using Xamarin.Forms;
using Xamarin.Forms.Platform.Android;

    [assembly: ExportRenderer(typeof(ApplicationClient.CustomControls.LSImage), typeof(LSImageRenderer))]

    namespace Application.Droid.CustomControls
    
        public class LSImageRenderer : ImageRenderer
        
            Page page;
            NavigationPage navigPage;

            protected override void OnElementChanged(ElementChangedEventArgs<Image> e)
            
                base.OnElementChanged(e);
                if (e.OldElement == null)
                
                    if (GetContainingViewCell(e.NewElement) != null)
                    
                        page = GetContainingPage(e.NewElement);
                        if (page.Parent is TabbedPage)
                        
                            page.Disappearing += PageContainedInTabbedPageDisapearing;
                            return;
                        

                        navigPage = GetContainingNavigationPage(page);
                        if (navigPage != null)
                            navigPage.Popped += OnPagePopped;
                    
                    else if ((page = GetContainingTabbedPage(e.NewElement)) != null)
                    
                        page.Disappearing += PageContainedInTabbedPageDisapearing;
                    
                
            

            void PageContainedInTabbedPageDisapearing (object sender, EventArgs e)
            
                this.Dispose(true);
                page.Disappearing -= PageContainedInTabbedPageDisapearing;
            

            protected override void Dispose(bool disposing)
            
                Log.Info("**** LSImageRenderer *****", "Image got disposed");
                base.Dispose(disposing);
            

            private void OnPagePopped(object s, NavigationEventArgs e)
            
                if (e.Page == page)
                
                    this.Dispose(true);
                    navigPage.Popped -= OnPagePopped;
                
            

            private Page GetContainingPage(Xamarin.Forms.Element element)
            
                Element parentElement = element.ParentView;

                if (typeof(Page).IsAssignableFrom(parentElement.GetType()))
                    return (Page)parentElement;
                else
                    return GetContainingPage(parentElement);
            

            private ViewCell GetContainingViewCell(Xamarin.Forms.Element element)
            
                Element parentElement = element.Parent;

                if (parentElement == null)
                    return null;

                if (typeof(ViewCell).IsAssignableFrom(parentElement.GetType()))
                    return (ViewCell)parentElement;
                else
                    return GetContainingViewCell(parentElement);
            

            private TabbedPage GetContainingTabbedPage(Element element)
            
                Element parentElement = element.Parent;

                if (parentElement == null)
                    return null;

                if (typeof(TabbedPage).IsAssignableFrom(parentElement.GetType()))
                    return (TabbedPage)parentElement;
                else
                    return GetContainingTabbedPage(parentElement);
            

            private NavigationPage GetContainingNavigationPage(Element element)
            
                Element parentElement = element.Parent;

                if (parentElement == null)
                    return null;

                if (typeof(NavigationPage).IsAssignableFrom(parentElement.GetType()))
                    return (NavigationPage)parentElement;
                else
                    return GetContainingNavigationPage(parentElement);
            
        
    

最后,我将命名空间中的应用程序名称更改为 PCL 项目中的“ApplicationClient”和 Droid 项目中的“Application.Droid”。您应该将其更改为您的应用名称。

另外,Renderer 类末尾的几个递归方法,我知道我可以将它组合成一个通用方法。问题是,我在需要时一次构建一个。所以,这就是我离开它的方式。

编码愉快,

Avrohom

【讨论】:

感谢您分享您的代码和解释。只是一件事,我可以在您的代码中使用 ImageCell 吗?我尝试了一个自定义 ViewCell 但无法让它工作。干杯 从未尝试过使用 ImageCell。是什么阻碍了您使用 ViewCell?如果您坚持使用 ImageCell,那么我想在您有“ViewCell”的地方更改“GetContainingViewCell”方法中的代码以将其替换为“ImageCell”是一个好主意。我看不出它不应该这样做的原因。 更重要的是,您可以将 'ViewCell' 更改为 'Cell',这样两者都可以使用。 我终于让我的自定义 ViewCell 工作了,但我一定是做错了什么,因为我会从我的列表中得到一个调用然后用完内存,但是上面的代码用完了在它显示列表之前。我不知道我做错了什么 能否给您展示自定义 ViewCell 的代码?【参考方案2】:

另一组可能有帮助的步骤如下:

Android 上似乎存在内存泄漏,涉及带有自定义单元格的列表视图。我做了一些调查,发现如果我在我的页面中执行以下操作:

    protected override void OnAppearing()
    
        BindingContext = controller.Model;
        base.OnAppearing();
    

    protected override void OnDisappearing()
    
        BindingContext = null;
        Content = null;
        base.OnDisappearing();
        GC.Collect();
    

在清单中设置 android 选项以使用大堆 (android:largeHeap="true"),最后,使用新的垃圾收集器,(在 environment.txt 中,MONO_GC_PARAMS=bridge-implementation=new) 这种组合,似乎解决了崩溃问题。我只能假设这只是因为将 things 设置为 null 有助于 GC 处理元素,大堆大小为 GC 购买时间,以及新的 GC 选项有助于加速收集本身。我真诚地希望这对其他人有所帮助...

【讨论】:

我认为你仍然有内存泄漏。用 largeHeap="true" 解决问题确实是个坏主意。它适用于具有更多内存的设备,但“小型”设备将没有额外的可用内存。你只是推迟崩溃。 你说得对,这就是为什么我建议它只是为垃圾收集器争取时间并释放空间。我有一个列表视图,每个单元格有多个图像和文本,一次在屏幕上显示 5 到 10 个单元格(取决于屏幕空间),整个列表的数百部分,在具有 256 MB RAM 的设备上,它处理它没有崩溃。在 Xamarin 人员修复内存泄漏之前,这只是一个创可贴。 environment.txt 在哪里?【参考方案3】:

在 Visual Studio 2015 中转到调试选项>.droid 属性>Android 选项>高级并将 Java 最大堆大小设置为 1G

【讨论】:

以上是关于Android 上的 Xamarin.Forms ListView OutOfMemoryError 异常的主要内容,如果未能解决你的问题,请参考以下文章

退出应用程序时在 Android 上的 Xamarin.Forms 中获取 NullReferenceException

CSS Id 选择器阻止所有内容在 Android 10 上的 Xamarin.Forms WebView 中显示

TeamCity 上的 Xamarin 表单:Xamarin.Forms 任务与目标不匹配

iOS 上的 Xamarin.Forms Master/Detail 边缘滑动

是否可以在 Xamarin Forms ContentView 中嵌入 Android.VideoView

Xamarin Forms - iOS 上的键盘覆盖条目(文本字段)