【问题标题】:MonoTouch - WebRequest memory leak and crash?MonoTouch - WebRequest 内存泄漏和崩溃?
【发布时间】:2011-03-30 21:11:35
【问题描述】:

我有一个 MonoTouch 应用程序,它使用 3.5MB 文件执行 HTTP POST,它在我测试的主要平台上非常不稳定(带有 OS 3.1.2 的 iPhone 3G 和带有 OS 4.2.1 的 iPhone 4 )。我将描述我在这里所做的事情,如果我做错了什么,也许有人可以告诉我。

为了排除我的应用程序的其余部分,我将其缩减为一个很小的示例应用程序。该应用程序是一个 iPhone OpenGL 项目,它只做这个:

  1. 在启动时,以 30k 块分配 6MB 内存。这模拟了我的应用程序的内存使用情况。
  2. 将一个 3.5MB 的文件读入内存。
  3. 创建一个帖子来发布数据。 (创建一个 WebRequest 对象,使用 GetRequestStream(),并将 3.5MB 数据写入)。
  4. 当主线程检测到发布线程完成时,转到步骤 2 并重复。

另外,每一帧,我分配 0-100k 来模拟应用程序做某事。我没有保留对这些数据的任何引用,因此它应该被垃圾收集。

iPhone 3G 结果: 该应用程序通过 6 到 8 次上传,然后操作系统将其杀死。没有崩溃日志,但有一个 LowMemory 日志显示该应用程序已被抛弃。

iPhone 4 结果:在第 11 次上传时出现 Mprotect 错误。

几个数据点:

  • 仪器不显示内存随着应用程序继续上传而增加。
  • 仪器未显示任何重大泄漏(可能总共 1 KB)。
  • 无论我是以 64k 块的形式写入帖子数据,还是通过一次 Stream.Write() 调用一次全部写入帖子数据。
  • 在开始下一次上传之前是否等待响应 (HttpWebRequest.HaveResponse) 都没有关系。
  • POST 数据是否有效并不重要。我尝试使用有效的 POST 数据,并尝试发送 3MB 的零。
  • 如果应用程序没有在每帧分配任何数据,则内存不足需要更长的时间(但如前所述,我为每帧分配的内存在分配它的帧之后没有被引用,所以它应该被 GC 捡起)。

如果没有人有任何想法,我会向 Novell 提交一个错误,但我想先看看我这里是不是做错了什么。

如果有人想要完整的示例应用程序,我可以提供,但我已将我的 EAGLView.cs 的内容粘贴在下面。

using System;
using System.Net;
using System.Threading;
using System.Collections.Generic;
using System.IO;
using OpenTK.Platform.iPhoneOS;
using MonoTouch.CoreAnimation;
using OpenTK;
using OpenTK.Graphics.ES11;
using MonoTouch.Foundation;
using MonoTouch.ObjCRuntime;
using MonoTouch.OpenGLES;

namespace CrashTest
{
    public partial class EAGLView : iPhoneOSGameView
    {
        [Export("layerClass")]
        static Class LayerClass ()
        {
            return iPhoneOSGameView.GetLayerClass ();
        }

        [Export("initWithCoder:")]
        public EAGLView (NSCoder coder) : base(coder)
        {
            LayerRetainsBacking = false;
            LayerColorFormat = EAGLColorFormat.RGBA8;
            ContextRenderingApi = EAGLRenderingAPI.OpenGLES1;
        }

        protected override void ConfigureLayer (CAEAGLLayer eaglLayer)
        {
            eaglLayer.Opaque = true;
        }


        protected override void OnRenderFrame (FrameEventArgs e)
        {
            SimulateAppAllocations();
            UpdatePost();           

            base.OnRenderFrame (e);
            float[] squareVertices = { -0.5f, -0.5f, 0.5f, -0.5f, -0.5f, 0.5f, 0.5f, 0.5f };
            byte[] squareColors = { 255, 255, 0, 255, 0, 255, 255, 255, 0, 0,
            0, 0, 255, 0, 255, 255 };

            MakeCurrent ();
            GL.Viewport (0, 0, Size.Width, Size.Height);

            GL.MatrixMode (All.Projection);
            GL.LoadIdentity ();
            GL.Ortho (-1.0f, 1.0f, -1.5f, 1.5f, -1.0f, 1.0f);
            GL.MatrixMode (All.Modelview);
            GL.Rotate (3.0f, 0.0f, 0.0f, 1.0f);

            GL.ClearColor (0.5f, 0.5f, 0.5f, 1.0f);
            GL.Clear ((uint)All.ColorBufferBit);

            GL.VertexPointer (2, All.Float, 0, squareVertices);
            GL.EnableClientState (All.VertexArray);
            GL.ColorPointer (4, All.UnsignedByte, 0, squareColors);
            GL.EnableClientState (All.ColorArray);

            GL.DrawArrays (All.TriangleStrip, 0, 4);

            SwapBuffers ();
        }


        AsyncHttpPost m_Post;
        int m_nPosts = 1;

        byte[] LoadPostData()
        {
            // Just return 3MB of zeros. It doesn't matter whether this is valid POST data or not.
            return new byte[1024 * 1024 * 3];
        }

        void UpdatePost()
        {
            if ( m_Post == null || m_Post.PostStatus != AsyncHttpPostStatus.InProgress )
            {
                System.Console.WriteLine( string.Format( "Starting post {0}", m_nPosts++ ) );

                byte [] postData = LoadPostData();

                m_Post = new AsyncHttpPost( 
                    "https://api-video.facebook.com/restserver.php", 
                    "multipart/form-data; boundary=" + "8cdbcdf18ab6640",
                    postData );
            }
        }

        Random m_Random = new Random(0);
        List< byte [] > m_Allocations;

        List< byte[] > m_InitialAllocations;

        void SimulateAppAllocations()
        {
            // First time through, allocate a bunch of data that the app would allocate.
            if ( m_InitialAllocations == null )
            {
                m_InitialAllocations = new List<byte[]>();
                int nInitialBytes = 6 * 1024 * 1024;
                int nBlockSize = 30000;
                for ( int nCurBytes = 0; nCurBytes < nInitialBytes; nCurBytes += nBlockSize )
                {
                    m_InitialAllocations.Add( new byte[nBlockSize] );
                }
            }

            m_Allocations = new List<byte[]>();
            for ( int i=0; i < 10; i++ )
            {
                int nAllocationSize = m_Random.Next( 10000 ) + 10;
                m_Allocations.Add( new byte[nAllocationSize] );
            }
        }       
    }




    public enum AsyncHttpPostStatus
    {
        InProgress,
        Success,
        Fail
    }

    public class AsyncHttpPost
    {
        public AsyncHttpPost( string sURL, string sContentType, byte [] postData )
        {
            m_PostData = postData;
            m_PostStatus = AsyncHttpPostStatus.InProgress;
            m_sContentType = sContentType;
            m_sURL = sURL;

            //UploadThread();
            m_UploadThread = new Thread( new ThreadStart( UploadThread ) );
            m_UploadThread.Start();            
        }

        void UploadThread()
        {
            using ( MonoTouch.Foundation.NSAutoreleasePool pool = new MonoTouch.Foundation.NSAutoreleasePool() )
            {
                try
                {
                    HttpWebRequest request = WebRequest.Create( m_sURL ) as HttpWebRequest;
                    request.Method = "POST";
                    request.ContentType = m_sContentType;
                    request.ContentLength = m_PostData.Length;

                    // Write the post data.
                    using ( Stream stream = request.GetRequestStream() )
                    {
                        stream.Write( m_PostData, 0, m_PostData.Length );
                        stream.Close();
                    }

                    System.Console.WriteLine( "Finished!" );

                    // We're done with the data now. Let it be garbage collected.
                    m_PostData = null;

                    // Finished!
                    m_PostStatus = AsyncHttpPostStatus.Success;
                }
                catch ( System.Exception e )
                {
                    System.Console.WriteLine( "Error in AsyncHttpPost.UploadThread:\n" + e.Message );
                    m_PostStatus = AsyncHttpPostStatus.Fail;
                }
            }
        }

        public AsyncHttpPostStatus PostStatus
        {
            get
            {
                return m_PostStatus;
            }
        }


        Thread m_UploadThread;

        // Queued to be handled in the main thread.
        byte [] m_PostData;

        AsyncHttpPostStatus m_PostStatus;
        string m_sContentType;
        string m_sURL;
    }
}

【问题讨论】:

  • 这听起来与我遇到的问题非常相似 - 不幸的是,我在这方面没有取得太大进展,但我很想看看是否有其他人可以对这种情况有所了解
  • 我在stackoverflow.com/questions/5819700 发布了一个可能与此相关的问题。我们也在做 http 工作并且以类似的方式失败。
  • 您是否向bugzilla.xamarin.com 发布了错误?这个问题听起来与我面临的问题相似,我报告了一个关于下载的错误。 MT 4.0.4 中问题依然存在

标签: memory-leaks xamarin.ios webrequest


【解决方案1】:

我认为您应该一次读取 1 KB(或任意大小)的文件并将其写入 Web 请求。

类似这样的代码:

byte[] buffer = new buffer[1024];
int bytesRead = 0;
using (FileStream fileStream = File.OpenRead("YourFile.txt"))
{
    while ((bytesRead = fileStream.Read(buffer, 0, buffer.Length)) != 0)
    {
        httpPostStream.Write(buffer, 0, bytesRead);
    }
}

这不是我的想法,但我认为它是正确的。

这样,当您真的不需要时,您就不会在内存中浮动额外的 3MB。我认为这样的技巧在 iDevices(或其他设备)上比在桌面上更重要。

也测试缓冲区大小,更大的缓冲区可以让您在某一点上获得更好的速度(我记得 8KB 相当不错)。

【讨论】:

  • 感谢您的建议。不幸的是,我用于上传的块大小并不重要。我刚刚修改了我的示例,以 8k 块上传 3MB,但由于内存不足,该应用仍然被 iPhone 抛弃。
  • 我注意到的其他一些事情:我认为您不需要在新线程上使用 NSAutoReleasePool。另外,我会在 HttpWebRequest 上使用 Async 方法,而不是创建自己的线程。当您创建自己的线程时,实际上是在创建 2 个线程,因为 HttpWebRequest 中的同步方法在内部创建了自己的线程——这就是超时的工作方式。在上面的代码中,我将一个新缓冲区作为局部变量作为示例。将其设为类中的静态变量或实例变量并重用它。
  • 第一次写这个上传代码时,我使用了所有的异步方法。发生了同样的问题。我使用同步方法发布了错误,因为重现代码更短更简单。
  • 我继续前进,filed a bug with Novell。希望这很快得到解决。它严重限制了我们应用的一个主要功能。
  • 好的,一些好消息和一些坏消息。好处:TcpClient+SslStream 方法成功地避免了我的应用程序中的内存不足问题。坏处:有一个单独的问题是线程进入 GC_remap(),然后调用 abort() 并且进程终止。在 HTTP 上传期间,我在所有设备上都看到了这个问题,而不仅仅是 iPhone3G。我将努力在我的应用程序之外获得一个不错的复制案例。
猜你喜欢
  • 1970-01-01
  • 2011-05-21
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-10-20
  • 1970-01-01
  • 2020-09-07
  • 1970-01-01
相关资源
最近更新 更多