OutOfMemoryException @ WriteableBitmap @ 后台代理

7
我有一个Windows Phone 8应用程序,使用后台代理来实现以下操作:
  1. 从互联网获取数据;
  2. 基于使用步骤1中的数据作为数据源的用户控件生成图像;
  3. 在用户控件中,我有Grid和StackPanel以及一些文本和图像控件;
  4. 当一些图像使用安装文件夹(/Assets/images/...)中的本地资源时,其中一个被用户从手机相册库中选择作为背景,因此我必须使用C#代码设置其源。
然而,当它在后台运行时,它会出现 OutOfMemoryException 错误,目前进行了一些故障排除。
  1. 当我在前台运行进程时,一切正常;
  2. 如果我注释掉更新进度,并直接创建图像,也能正常工作;
  3. 如果我不设置背景图片,也能正常工作;
  4. OutOfMemoryException是在 var bmp = new WriteableBitmap(480, 800);期间抛出的。
    我已经将图像大小从1280*768缩小到了800*480,我认为这是全屏背景图像的底线,不是吗?
  5. 经过一些研究,我发现这个问题是因为超过了周期任务的11MB限制而出现的。
  6. 我尝试使用DeviceStatus.ApplicationCurrentMemoryUsage来跟踪内存使用情况:

    -- 限制为11,534,336(位)

    -- 当后台代理启动时,即使没有任何任务,内存使用情况也会变为4,648,960

    -- 当从互联网获取更新时,它增加到5,079,040

    -- 完成后,它又降回到4,648,960

    -- 当调用开始(从用户控件生成图像)时,它增加到8,499,200

我猜那就是问题所在,内存太少了,无法通过WriteableBitmap渲染图像。

有什么办法可以解决这个问题吗?

有没有更好的方法从用户控件或其他任何地方生成图像?

实际上,原始图像可能只有100KB左右,但是当通过WriteableBitmap渲染时,文件大小(以及所需内存大小)可能增加到1-2MB。

或者我可以从哪里释放内存?

==============================================================

顺便提一下,当这篇Code Project article说我只能在定期任务中使用11MB内存时;

然而,这篇MSDN article说我可以使用高达20 MB或25MB(使用Windows Phone 8 Update 3)的内存; 哪个是正确的?为什么我处于第一种情况?

==============================================================

编辑:

说到调试器,MSDN文章中也提到了:

在调试器下运行时,内存和超时限制被暂停。

但为什么我仍然会遇到限制呢?

==============================================================

编辑:

好的,我找到了一些似乎有用的东西,现在我会检查它们,但仍然欢迎建议。

http://writeablebitmapex.codeplex.com/

http://suchan.cz/2012/07/pro-live-tiles-for-windows-phone/

http://notebookheavy.com/2011/12/06/microsoft-style-dynamic-tiles-for-windows-phone-mango/

==============================================================

生成图像的代码:
Deployment.Current.Dispatcher.BeginInvoke(() =>
{
    var customBG = new ImageUserControl();
    customBG.Measure(new Size(480, 800));
    var bmp = new WriteableBitmap(480, 800); //Thrown the **OutOfMemoryException**
    bmp.Render(customBG, null);
    bmp.Invalidate();
    using (var isf = IsolatedStorageFile.GetUserStoreForApplication())
    {
        filename = "/Shared/NewBackGround.jpg";
        using (var stream = isf.OpenFile(filename, System.IO.FileMode.OpenOrCreate))
        {
            bmp.SaveJpeg(stream, 480, 800, 0, 100);
        }
    }
}

ImageUserControl的XAML代码:

<UserControl blabla... d:DesignHeight="800" d:DesignWidth="480">
    <Grid x:Name="LayoutRoot">
    <Image x:Name="nBackgroundSource" Stretch="UniformToFill"/>

    //blabla...
    </Grid>
</UserControl>

ImageUserControl的C#代码如下:

public ImageUserControl()
{
    InitializeComponent();
    LupdateUI();
}

public void LupdateUI()
{
    DataInfo _dataInfo = new DataInfo();
    LayoutRoot.DataContext = _dataInfo;
    try
    {
        using (var isoStore = IsolatedStorageFile.GetUserStoreForApplication())
        {
            using (var isoFileStream = isoStore.OpenFile("/Shared/BackgroundImage.jpg", FileMode.Open, FileAccess.Read))
            {
                BitmapImage bi = new BitmapImage();
                bi.SetSource(isoFileStream);
                nBackgroundSource.Source = bi;
            }
        }
    }
    catch (Exception) { }
}

DataInfo设置页面 中的另一个类,它保存从互联网获取的数据时:
public class DataInfo
{
    public string Wind1 { get { return GetValueOrDefault<string>("Wind1", "N/A"); } set { if (AddOrUpdateValue("Wind1", value)) { Save(); } } }
    public string Wind2 { get { return GetValueOrDefault<string>("Wind2", "N/A"); } set { if (AddOrUpdateValue("Wind2", value)) { Save(); } } }
    //blabla...
}

我非常确定诺基亚有一个API可以做到这一点,它根本不会将整个图像加载到内存中(他们为他们的4100万像素相机手机构建了它,并且据说效果很好)。无论如何,如果我是你,我不会在后台更新图像 - 为什么你需要在代理中这样做,而不是只为应用程序标记“在加载时执行”呢? - Benjamin Gruenbaum
41MP相机应该在前面运行,对吧?那可能是另一回事?我创建了这个图像作为锁屏图像,因此每次启动应用程序时“手动”更新不像是一个好主意,谢谢。 - Max Meng
1
不,他们拥有的是一个API,可以让你在不将所有内容放入内存的情况下处理图像 - 这听起来正是你所需要的。话虽如此 - 从用户体验的角度来看,如果我注意到你的应用程序使我的手机变慢,我会禁用后台代理,这是用户可能会采取的措施。我可以向诺基亚的朋友询问,但我认为诺基亚成像API是你最好的选择。 - Benjamin Gruenbaum
@BenjaminGruenbaum,感谢您提供这个有用的信息,听起来不错,我会抽时间去了解一下。顺便问一下,这是否意味着我们只能在诺基亚手机上使用它? - Max Meng
1
不,它适用于所有Windows Phone手机 - 他们只是为自己开发了它,但它也可以在其他设备上运行(并得到支持)。 - Benjamin Gruenbaum
太好了!再次感谢。 - Max Meng
2个回答

1
如果我注释掉更新进度,并直接创建图像,它也可以正常工作。我认为你应该关注这一部分。这似乎表明在更新后某些内存没有被释放。确保在呈现图片之前,更新过程中使用的所有引用都超出范围。强制进行垃圾收集也可能有帮助:
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect(); // Frees the memory that was used by the finalizers

另一个需要考虑的因素是调试器也会占用大量内存。通过在“发布”模式下编译项目并在手机上部署进行真实测试,以确保您的内存耗尽。
不过,我已经遇到过这种情况,所以我知道这可能还不够。关键是:.NET Framework中的某些库是懒加载的。例如,如果您的更新过程涉及下载一些数据,则后台代理将加载网络库。这些库无法卸载,并会浪费代理的一些内存。这就是为什么即使您释放了更新过程中使用的所有内存,也无法恢复启动后台代理时相同数量的可用内存。看到这一点,我在我的一个应用程序中所做的是将后台代理的工作负载跨越两个执行。基本上,当代理执行时:
检查隔离存储中是否有待处理的数据。如果没有,只需执行更新过程并将所有所需数据存储在隔离存储中
如果有挂起的数据(即在下一次执行中),则生成图片并清除数据
这意味着每小时只会生成一次图片,而不是每30分钟生成一次,因此只有在其他所有方法都失败时才使用此解决方法。

感谢您的回答和所有建议!“两次执行后台代理”听起来像是一个聪明的解决方法,如果我找不到更好的解决方案,我会把它作为最后的选择。 - Max Meng
反馈:发布模式没有帮助。 - Max Meng
好吧,我放弃了。无论我尝试什么,最终都以“OutOfMemoryException”结束。 - Max Meng

1
更大的内存限制是为后台音频代理而设定的,文档中有明确说明。你只能使用11MB,当你试图在后台处理图片时,这可能会非常困难。
480x800会增加1MB的内存,因为每个像素需要4个字节,所以最终大约是1.22MB。当以JPEG格式压缩时,它只有100KB左右,但无论何时使用WriteableBitmap,它都会被加载到内存中。
在强制使用GC.Collect之前,您可以尝试将事物置为空,即使在超出范围之前 - 无论是BitmapImage还是WriteableBitmap。除此之外,您可以在完成后以编程方式从Grid中删除Image对象,并将其源设置为null。
还有其他未向我们展示的WriteableBitmap、BitmapImage或Image对象吗?
另外,尝试不使用调试器。我曾经在某个地方读过,它会添加另外1-2MB的内存,当你只有11MB时这很多。虽然,如果它在调试器下崩溃得如此之快,即使没有调试器,我也不会冒险尝试。但仅供测试目的,您可以试一试。

您是否需要使用ImageUserControl?您可以尝试逐步创建图像和其他对象,而不使用XAML,以便可以测量每个步骤的内存使用情况,以确定何时达到极限。


谢谢您的回答!1. --周期代理和资源密集型代理在具有1 GB或更多内存的设备上任何时候都不能使用超过20 MB的内存。在低内存设备上,限制为11 MB。 这就是文档页面上的内容。看起来20MB不应该成为音频代理的限制? - Max Meng
2.--我必须承认用户控件有点复杂,我有很多的<Grid.RowDefinitions><Grid.ColumnDefinitions>,还有大约10个小图像,它们使用数据绑定如:<Image Source="{Binding Img1}"/>。因此,没有更多的WriteableBitmapBitmapImage对象了。 我猜最占用内存的进程是[为用户控件创建一个位图]+[渲染背景图像]+[从互联网获取数据]。实际上,我尝试注释掉用户控件中除了背景图像之外的所有元素,但这并没有帮助。 - Max Meng
3.-- 其实我希望能用另一种方式来做,我忘了提到我是C#和Win Phone Dev的新手。也许WriteablebitmapEX可以完成这项工作?在发布问题后,我会尝试一些新的方法。感谢您的关注! - Max Meng
反馈:发布模式没有帮助。 - Max Meng

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