Memory issue when using CGImage.ScreenImage in a l

2019-07-17 17:06发布

问题:

I'm trying to create an app to read QR codes using Monotouch and C# port of Zxing but I'm hitting memory issues. While the app processes captured screen frames the app receives memory warnings and is then shut down. I have removed the call to Zxing to track down where the memory issue stems from and can reproduce the issue with just capturing the screen image in a loop.

Here is the code:

using System;
using System.Drawing;
using System.Collections.Generic;
using System.Threading;
using MonoTouch.UIKit;
using MonoTouch.Foundation;
using MonoTouch.CoreGraphics;
using com.google.zxing;
using com.google.zxing.common;
using System.Collections;
using MonoTouch.AudioToolbox;
using iOS_Client.Utilities;

namespace iOS_Client.Controllers
{

    public class CameraOverLayView : UIView
    {

        private Thread _thread;
        private CameraViewController _parentViewController; 
        private Hashtable hints;
        private static com.google.zxing.MultiFormatReader _multiFormatReader = null;
        private static RectangleF picFrame = new RectangleF(0, 146, 320, 157);
        private static UIImage _theScreenImage = null;


        public CameraOverLayView(CameraViewController parentController) : base()
        {
            Initialize();
            _parentViewController = parentController;
        }

        private void Initialize()
        {              

        }

        private bool Worker()
        {


            Result resultb = null;

            if(DeviceHardware.Version == DeviceHardware.HardwareVersion.iPhone4
               || DeviceHardware.Version == DeviceHardware.HardwareVersion.iPhone4S)
            {
                picFrame = new RectangleF(0, 146*2, 320*2, 157*2);

            }

                if(hints==null)
                {
                    var list = new ArrayList();

                    list.Add (com.google.zxing.BarcodeFormat.QR_CODE);

                    hints = new Hashtable();
                    hints.Add(com.google.zxing.DecodeHintType.POSSIBLE_FORMATS, list);
                    hints.Add (com.google.zxing.DecodeHintType.TRY_HARDER, true);
                }

                if(_multiFormatReader == null)
                {
                    _multiFormatReader = new com.google.zxing.MultiFormatReader();
                }

                using (var screenImage = CGImage.ScreenImage.WithImageInRect(picFrame))
                {
                    using (_theScreenImage = UIImage.FromImage(screenImage))
                    {
                        Bitmap srcbitmap = new System.Drawing.Bitmap(_theScreenImage);
                        LuminanceSource source = null;
                        BinaryBitmap bitmap = null;
                        try {
                            source = new RGBLuminanceSource(srcbitmap, screenImage.Width, screenImage.Height);
                            bitmap = new BinaryBitmap(new HybridBinarizer(source));

                            try {
                                    _multiFormatReader.Hints = hints;
                                    resultb = null;

                                    //_multiFormatReader.decodeWithState(bitmap);

                                    if(resultb != null && resultb.Text!=null)
                                    {

                                        InvokeOnMainThread( () => _parentViewController.BarCodeScanned(resultb));

                                    }
                                }
                            catch (ReaderException re)
                            {
                                //continue;
                            }

                        } catch (Exception ex) {
                            Console.WriteLine(ex.Message);
                        }

                        finally {
                            if(bitmap!=null)
                                bitmap = null;

                             if(source!=null)
                                source = null;

                            if(srcbitmap!=null)
                            {
                                srcbitmap.Dispose();
                                    srcbitmap = null;
                            }

                        }   

                    }
                }

            return resultb != null;
        }

        public void StartWorker()
        {
            if(_thread==null)
            {
                _thread = new Thread(()=> { 

                        bool result = false;
                        while (result == false)
                        {
                            result = Worker();
                            Thread.Sleep (67);
                        }               

                }); 

            }

            _thread.Start();            

        }

        public void StopWorker()
        {

            if(_thread!=null)
            {

                _thread.Abort();
                _thread = null;

            }

            //Just in case
            _multiFormatReader = null;
            hints = null;
        }

        protected override void Dispose(bool disposing)
        {
            StopWorker();   
            base.Dispose(disposing);
        }

    }
}

Interestingly I took a look at http://blog.reinforce-lab.com/2010/02/monotouchvideocapturinghowto.html to try and see how others were capturing and processing video and this code suffers from the same as mine, quitting after about 40 seconds with memory warnings.

Hopefully the QR codes will be scanned in less than 40 seconds but I'm not sure if the memory ever gets released so the problem may crop up after many codes have been scanned. Either way it should be possible to capture a video feed continuously without memory issues right?

回答1:

The original System.Drawing.Bitmap from zxing.MonoTouch suffered from a lack of Dispose which made it never release the unmanaged memory it allocated.

The more recent one (from your link) does free the unmanaged memory when Dispose is called (it's better). However it creates a bitmap context (in it's constructor) and does not dispose it manually (e.g. with a using). So it relies on the garbage collector (GC) to do it later...

In many cases this is not a big issue since the GC will, eventually, free this context instance and will reclaim the associated memory. However if you're doing this in a loop it's possible you'll run out of (unmanaged) memory before the GC kicks in. That will get you memory warnings and iOS can decide to kill your application (or it could crash by itself).

but I'm not sure if the memory ever gets released

Yes, it should be - but maybe not as fast as you need the memory back. Implementing (and using) IDisposable correctly will solve this.

Either way it should be possible to capture a video feed continuously without memory issues right?

Yes. Make sure you're releasing your memory as soon as possible, e.g. with using (var ...) { }, and ensure the 3rd party code you use does the same.



回答2:

This is somewhat counter-intuitive, but the ScreenImage property will create a new CGImage instance every time you call it, so you must call Dispose on that object as well:

using (var img = CGImage.ScreenImage) {
    using (var screenImage = img.WithImageInRect(picFrame))
    {
    }
}


回答3:

I will just add the actual solution that worked for me which combined information from previous answers. The code inside the loop looks like:

using (var pool = new NSAutoreleasePool ())
{
    using (var img = CGImage.ScreenImage)
    {       
        using (var screenImage = img.WithImageInRect(picFrame))
        {
            using (_theScreenImage = UIImage.FromImage(screenImage))
            {
            }
        }
    }
}

GC.Collect();