Xamarin.Forms ListView在Android上出现OutOfMemoryError异常

11
有人尝试过在Xamarin.Forms Listview中使用包含图像视图的ItemTemplate吗?那么,当ListView包含大约20行或更多行时会发生什么?
就我而言,我加载了一个大约4K大小的.png文件到图像视图中。在应用程序崩溃并出现OutOfMemoryError之前,最多显示9-12行。在Android清单中请求了大堆之后,应用程序在60-70行后崩溃。
我知道Xamarin正在推广使用BitmapFactory类来缩小位图,但这对于Xamarin Forms Image View(开箱即用)不适用。
我正试图通过ImageRenderer的子类进行调整,看看是否可以添加一个BitmapFactory.Options属性以解决问题。
此外,我可能需要检查Xamarin.Forms是否在ViewCell被滚动屏幕后处置(回收)所包含的位图。
在开始这个过程之前,我非常渴望得到任何评论,可以使这个过程变得更容易或提供更简单的解决方案,以使这个过程变得不必要。
期待中...

4K PNG的位图大小是多少?PNG以无压缩方式存储在内存中。当转换为位图时,可能会创建一个超过1GB数据的4K PNG文件。此外,确实需要检查位图是否已被处理。而且很可能答案是否定的。 - Frank
我目前使用的PNG文件大小为512 x 512。 - Avrohom
抱歉,它稍微少了一点。它是24位深度... - Avrohom
据我所知,默认行为是将其解压为32位。即使源文件只是黑白(1位)PNG。无论如何,这并不是真正的问题。问题很可能是位图未被回收利用。 - Frank
1
我可以肯定地确认,加载在ViewCell中的ImageView永远不会被释放。这与放置在表单上的ImageView形成对比。经过尝试和测试,干得好!Xamarin团队! - Avrohom
显示剩余4条评论
3个回答

10

是的,我找到了解决方案。以下是代码。但在此之前,让我稍微解释一下我所做的。

因此,有必要自己处理图像及其底层资源(位图或可绘制对象,无论您想怎么称呼它)。基本上,这归结于处理原生的“ImageRenderer”对象。

现在,没有办法从任何地方获取对该ImageRenderer的引用,因为为了这样做,需要能够调用Platform.GetRenderer(...)。由于其作用域被声明为“internal”,因此无法访问“Platform”类。

因此,我别无选择,只能子类化Image类及其(Android) Renderer,并从内部销毁此Renderer本身(将'true'作为参数传递。不要尝试使用'false')。在Renderer中,我钩取页面消失事件(在TabbedPage的情况下)。在大多数情况下,页面消失事件不能很好地发挥作用,例如当页面仍在屏幕堆栈中但由于另一个页面正在其上方绘制而消失时。如果你销毁了图片,那么当页面再次被揭开(显示)时,它将不会显示图片。在这种情况下,我们必须钩住主导航页的'Popped'事件。

我已经尽力解释了。剩下的 - 希望 - 您将能够从代码中获得:

这是PCL项目中的Image子类。

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,但无法使其正常工作。谢谢。 - user1667474
从未尝试过使用ImageCell。为什么不使用ViewCell呢?如果您坚持使用ImageCell,那么我想将“GetContainingViewCell”方法中所有的“ViewCell”替换为“ImageCell”可能是个好主意。我认为这样做没有任何问题。 - Avrohom
我终于让我的自定义ViewCell工作了,但是我一定做错了其他事情,因为以前我会从列表中获取一个调用,然后内存就会耗尽,但是使用上述代码后,在显示列表之前就会耗尽。我不知道我做错了什么。 - user1667474
请问您能展示一下自定义ViewCell的代码吗? - Avrohom
不错的解决方案,但需要在 OnPagePopped 函数中进行微调以处理 MasterDetailPage... 对于我来说,在 Detail 页面上的 ListView 中的图像没有被释放。 - fredrik
显示剩余6条评论

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)。这些组合似乎可以解决崩溃问题。我只能假设这是因为将事物设置为null有助于GC处理元素,大堆大小购买GC时间来执行操作,并且新的GC选项有助于加速收集本身。我真诚地希望这能帮助其他人...

我认为你仍然有内存泄漏的问题。使用 largeHeap="true" 来解决问题真的是一个糟糕的主意。它可以在拥有更多内存的设备上运行,但是“小”设备将没有额外的内存可用。这只是推迟了崩溃的时间。 - Ton Snoei
你是正确的,这就是为什么我建议只是为了给垃圾回收器留出时间来释放空间。我有一个带有多个图像和文本的列表视图,每个单元格在屏幕上显示5到10个单元格(取决于屏幕空间),其中包含数百个整体列表的一部分,在256 MB RAM的设备上处理它而不会崩溃。这只是一个临时措施,直到Xamarin的开发人员能够修复内存泄漏问题。 - TChadwick
环境变量.txt在哪里? - Amir Hajiha

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接