MonoTouch - WebRequest内存泄漏和崩溃?

5
我有一个使用MonoTouch开发的应用程序,它通过HTTP POST上传一个3.5MB的文件,在我测试的主要平台上(iPhone 3G和OS 3.1.2的iPhone 4和OS 4.2.1)非常不稳定。我会在这里描述我的做法,希望有人能告诉我是否有做错的地方。
为了排除我的应用程序中的其他因素,我将其简化为一个小型样例应用程序。该应用程序是一个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. Instruments不显示应用程序继续上传时内存的增加。 2. Instruments没有显示任何显着的泄漏(可能总共有1千字节)。 3. 无论是以64k块还是一次使用一个Stream.Write()调用写入POST数据都没有关系。 4. 在开始下一次上传之前等待响应(HttpWebRequest.HaveResponse)或不等待响应都没有关系。 5. POST数据是否有效都没有关系。我尝试过使用有效的POST数据,也尝试过发送3MB的零。 6. 如果应用程序在每个帧中没有分配任何数据,则需要更长时间才能耗尽内存(但如前所述,我每帧分配的内存在分配它的帧之后不再引用,因此应该被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;
    }
}

这听起来与我遇到的问题非常相似 - 不幸的是,我在解决它方面没有取得太多进展,但我很想看看是否有其他人能够为这种情况提供任何线索。 - Luke
我在https://dev59.com/Fuo6XIcBkEYKwwoYTSsu发布了一个问题,可能与此有关。我们也在进行http工作,并以类似的方式失败。 - mj2008
你是否已经在 http://bugzilla.xamarin.com/ 上报告了一个 bug? 这个问题听起来和我遇到的问题很相似,我已经报告了一个关于下载的 bug。这个问题在 MT 4.0.4 中仍然存在。 - Krumelur
1个回答

1

我认为你应该每次读取 1KB(或任意大小)的文件,并将其写入网页请求。

类似于以下代码:

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相当不错)。


我注意到另外几件事情:我认为你不需要在新线程上使用NSAutoReleasePool。此外,我建议使用HttpWebRequest的异步方法而不是自己创建线程。当你自己创建线程时,实际上会创建两个线程,因为HttpWebRequest中的同步方法在内部创建了它们自己的线程--这就是超时如何工作的原理。在我的上面的代码中,我将一个新缓冲区作为局部变量示例。将其设置为类的静态或实例变量并重复使用。 - jonathanpeppers
第一次编写此上传代码时,我使用了所有的异步方法。同样的问题发生了。我使用同步方法发布了错误,因为复现代码更短、更简单。 - Mike
我已经向Novell提交了一个错误报告(https://bugzilla.novell.com/show_bug.cgi?id=684316)。希望这个问题能够尽快得到解决。它严重限制了我们应用程序的一个主要功能。 - Mike
2
好消息和坏消息都有。好消息是,TcpClient+SslStream方法成功地避免了我的应用程序中的内存不足问题。坏消息是,有一个单独的问题,其中一个线程进入GC_remap(),然后调用abort(),进程就会死掉。我在所有设备上都看到了这个问题,在HTTP上传期间,不仅仅是iPhone3G。我将努力在我的应用程序之外获得一个漂亮的重现案例。 - Mike
@Mike,你解决了吗?我们正在进行HTTPS GETS,并且也遇到了GC_remap()问题。目前无法确定原因。 - mj2008
显示剩余2条评论

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