views:

200

answers:

1

My app launches with a view controller and a simple view consisting of a button and a subview. When the user touches the button, the subview is populated with scrollviews that display the column headers, row headers, and cells of a spreadsheet. To draw the cells, I use CGBitmapContext to draw the cells, generate an image, and then put the image into the imageview contained in the scrollview that displays the cells.

When I run the app on the iPad, it displays the cells just fine, and the scrollview lets the user scroll around in the spreadsheet without any problems. If the user touches the button a second time, the spreadsheet redraws and continues to work perfectly, If, however, the user touches the button a third time, the app crashes. There is no exception information display in the Application Output window.

My first thought was that the successive button pushes were using up all the available memory, so I overrode the DidReceiveMemoryWarning method in the view controller and used a breakpoint to confirm that this method was not getting called. My next thought was that the CGBitmapContext was not getting released and looked for a Monotouch equivalent of Objective C's CGContextRelease() function. The closest I could find was the CGBitmapContext instance method Dispose(), which I called, without solving the problem.

In order to free up as much memory as possible (in case I was somehow running out of memory without tripping a warning), I tried forcing garbage collection each time I finished using a CGBitmapContext. This made the problem worse. Now the program would crash moments after displaying the spreadsheet the first time. This caused me to wonder whether the Garbage Collector was somehow collecting something necessary to the continued display of graphics on the screen.

I would be grateful for any suggestions on further avenues to investigate for the cause of these crashes. I have included the source code for the SpreadsheetView class. The relevant method is DrawSpreadsheet(), which is called when the button is touched.

Thank you for your assistance on this matter.

Stephen Ashley

  public class SpreadsheetView : UIView

 {
  public ISpreadsheetMessenger spreadsheetMessenger = null;
  public UIScrollView cellsScrollView = null;
  public UIImageView cellsImageView = null;

  public SpreadsheetView(RectangleF frame) : base()
  {
   Frame = frame;
   BackgroundColor = Constants.backgroundBlack;
   AutosizesSubviews = true;
  }

  public void DrawSpreadsheet()
  {
   UInt16 RowHeaderWidth = spreadsheetMessenger.RowHeaderWidth;
   UInt16 RowHeaderHeight = spreadsheetMessenger.RowHeaderHeight;
   UInt16 RowCount = spreadsheetMessenger.RowCount;
   UInt16 ColumnHeaderWidth = spreadsheetMessenger.ColumnHeaderWidth;
   UInt16 ColumnHeaderHeight = spreadsheetMessenger.ColumnHeaderHeight;
   UInt16 ColumnCount = spreadsheetMessenger.ColumnCount;

   // Add the corner
   UIImageView cornerView = new UIImageView(new RectangleF(0f, 0f,
      RowHeaderWidth, ColumnHeaderHeight));
   cornerView.BackgroundColor = Constants.headingColor;

   CGColorSpace cornerColorSpace = null;
   CGBitmapContext cornerContext = null;
   IntPtr buffer = Marshal.AllocHGlobal(RowHeaderWidth * ColumnHeaderHeight * 4);
   if (buffer == IntPtr.Zero)
    throw new OutOfMemoryException("Out of memory.");
   try
   {
    cornerColorSpace = CGColorSpace.CreateDeviceRGB();
    cornerContext = new CGBitmapContext
     (buffer, RowHeaderWidth, ColumnHeaderHeight, 8, 4 * RowHeaderWidth,
      cornerColorSpace, CGImageAlphaInfo.PremultipliedFirst);
    cornerContext.SetFillColorWithColor(Constants.headingColor.CGColor);
    cornerContext.FillRect(new RectangleF(0f, 0f, RowHeaderWidth, ColumnHeaderHeight));
    cornerView.Image = UIImage.FromImage(cornerContext.ToImage());
   }
   finally
   {
    Marshal.FreeHGlobal(buffer);
    if (cornerContext != null)
    {
     cornerContext.Dispose();
     cornerContext = null;
    }
    if (cornerColorSpace != null)
    {
     cornerColorSpace.Dispose();
     cornerColorSpace = null;
    }
   }
   cornerView.Image = DrawBottomRightCorner(cornerView.Image);
   AddSubview(cornerView);

   // Add the cellsScrollView
   cellsScrollView = new UIScrollView
    (new RectangleF(RowHeaderWidth, ColumnHeaderHeight,
     Frame.Width - RowHeaderWidth,
     Frame.Height - ColumnHeaderHeight));
   cellsScrollView.ContentSize = new SizeF
    (ColumnCount * ColumnHeaderWidth,
     RowCount * RowHeaderHeight);
   Size iContentSize = new Size((int)cellsScrollView.ContentSize.Width, 
                        (int)cellsScrollView.ContentSize.Height);
   cellsScrollView.BackgroundColor = UIColor.Black;
   AddSubview(cellsScrollView);

   CGColorSpace colorSpace = null;
   CGBitmapContext context = null;
   CGGradient gradient = null;
   UIImage image = null;
   int bytesPerRow = 4 * iContentSize.Width;
   int byteCount = bytesPerRow * iContentSize.Height;
   buffer = Marshal.AllocHGlobal(byteCount);
   if (buffer == IntPtr.Zero)
    throw new OutOfMemoryException("Out of memory.");

   try
   {
    colorSpace = CGColorSpace.CreateDeviceRGB();
    context = new CGBitmapContext
     (buffer, iContentSize.Width, 
      iContentSize.Height, 8, 4 * iContentSize.Width, 
      colorSpace, CGImageAlphaInfo.PremultipliedFirst);
    float[] components = new float[]
    {.75f, .75f, .75f, 1f,
     .25f, .25f, .25f, 1f};
    float[] locations = new float[]{0f, 1f};
    gradient = new CGGradient(colorSpace, components, locations);
    PointF startPoint = new PointF(0f, (float)iContentSize.Height);
    PointF endPoint = new PointF((float)iContentSize.Width, 0f);
    context.DrawLinearGradient(gradient, startPoint, endPoint, 0);
    context.SetLineWidth(Constants.lineWidth);
    context.BeginPath();
    for (UInt16 i = 1; i <= RowCount; i++)
    {
     context.MoveTo
      (0f, iContentSize.Height - i * RowHeaderHeight + (Constants.lineWidth/2));
     context.AddLineToPoint((float)iContentSize.Width, 
        iContentSize.Height - i * RowHeaderHeight + (Constants.lineWidth/2));
    }
    for (UInt16 j = 1; j <= ColumnCount; j++)
    {
     context.MoveTo((float)j * ColumnHeaderWidth - Constants.lineWidth/2, 
                    (float)iContentSize.Height);
     context.AddLineToPoint((float)j * ColumnHeaderWidth - Constants.lineWidth/2, 0f);
    }
    context.StrokePath();
    image = UIImage.FromImage(context.ToImage());
   }
   finally
   {
    Marshal.FreeHGlobal(buffer);
    if (gradient != null)
    {
     gradient.Dispose();
     gradient = null;
    }
    if (context != null)
    {
     context.Dispose();
     context = null;
    }
    if (colorSpace != null)
    {
     colorSpace.Dispose();
     colorSpace = null;
    }
    // GC.Collect();
    //GC.WaitForPendingFinalizers();
   }

   UIImage finalImage = ActivateCell(1, 1, image);
   finalImage = ActivateCell(0, 0, finalImage);
   cellsImageView = new UIImageView(finalImage);
   cellsImageView.Frame = new RectangleF(0f, 0f,
      iContentSize.Width, iContentSize.Height);
   cellsScrollView.AddSubview(cellsImageView);

  }

  private UIImage ActivateCell(UInt16 column, UInt16 row, UIImage backgroundImage)
  {
   UInt16 ColumnHeaderWidth = (UInt16)spreadsheetMessenger.ColumnHeaderWidth;
   UInt16 RowHeaderHeight = (UInt16)spreadsheetMessenger.RowHeaderHeight;

   CGColorSpace cellColorSpace = null;
   CGBitmapContext cellContext = null;
   UIImage cellImage = null;
   IntPtr buffer = Marshal.AllocHGlobal(4 * ColumnHeaderWidth * RowHeaderHeight);
   if (buffer == IntPtr.Zero)
    throw new OutOfMemoryException("Out of memory: ActivateCell()");
   try
   {
    cellColorSpace = CGColorSpace.CreateDeviceRGB();
    // Create a bitmap the size of a cell
    cellContext = new CGBitmapContext
     (buffer, ColumnHeaderWidth, RowHeaderHeight, 8,
      4 * ColumnHeaderWidth, cellColorSpace, CGImageAlphaInfo.PremultipliedFirst);
    // Paint it white
    cellContext.SetFillColorWithColor(UIColor.White.CGColor);
    cellContext.FillRect(new RectangleF(0f, 0f, ColumnHeaderWidth, RowHeaderHeight));
    // Convert it to an image
    cellImage = UIImage.FromImage(cellContext.ToImage());
   }
   finally
   {
    Marshal.FreeHGlobal(buffer);
    if (cellContext != null) 
    {
     cellContext.Dispose();
     cellContext = null;
    }
    if (cellColorSpace != null)
    {
     cellColorSpace.Dispose();
     cellColorSpace = null;
    }
    // GC.Collect();
    //GC.WaitForPendingFinalizers();
   }
   // Draw the border on the cell image
   cellImage = DrawBottomRightCorner(cellImage);

   CGColorSpace colorSpace = null;
   CGBitmapContext context = null;
   Size iContentSize = new Size((int)backgroundImage.Size.Width,
                                (int)backgroundImage.Size.Height);
   buffer = Marshal.AllocHGlobal(4 * iContentSize.Width * iContentSize.Height);
   if (buffer == IntPtr.Zero)
    throw new OutOfMemoryException("Out of memory: ActivateCell().");
   try
   {
    colorSpace = CGColorSpace.CreateDeviceRGB();
    // Set up a bitmap context the size of the whole grid
    context = new CGBitmapContext
     (buffer, iContentSize.Width, 
      iContentSize.Height, 8, 4 * iContentSize.Width, 
      colorSpace, CGImageAlphaInfo.PremultipliedFirst);
    // Draw the original grid into the bitmap
    context.DrawImage(new RectangleF(0f, 0f, iContentSize.Width, iContentSize.Height),
                      backgroundImage.CGImage);
    // Draw the cell image into the bitmap
    context.DrawImage(new RectangleF(column * ColumnHeaderWidth, 
                                     iContentSize.Height - (row + 1) * RowHeaderHeight,
                                     ColumnHeaderWidth, RowHeaderHeight),
                      cellImage.CGImage);
    // Convert the bitmap back to an image
    backgroundImage = UIImage.FromImage(context.ToImage());
   }
   finally
   {
    Marshal.FreeHGlobal(buffer);
    if (context != null)
    {
     context.Dispose();
     context = null;
    }
    if (colorSpace != null)
    {
     colorSpace.Dispose();
     colorSpace = null;
    }
    // GC.Collect();
    //GC.WaitForPendingFinalizers();
   }
   return backgroundImage;
  }

  private UIImage DrawBottomRightCorner(UIImage image)
  {
   int width = (int)image.Size.Width;
   int height = (int)image.Size.Height;
   float lineWidth = Constants.lineWidth;

   CGColorSpace colorSpace = null;
   CGBitmapContext context = null;
   UIImage returnImage = null;
   IntPtr buffer = Marshal.AllocHGlobal(4 * width * height);
   if (buffer == IntPtr.Zero)
    throw new OutOfMemoryException("Out of memory: DrawBottomRightCorner().");
   try
   {
    colorSpace = CGColorSpace.CreateDeviceRGB();
    context = new CGBitmapContext
     (buffer, width, height, 8, 4 * width, colorSpace, 
      CGImageAlphaInfo.PremultipliedFirst);
    context.DrawImage(new RectangleF(0f, 0f, width, height),
                      image.CGImage);
    context.BeginPath();
    context.MoveTo(0f, (int)(lineWidth/2f));

    context.AddLineToPoint(width - (int)(lineWidth/2f), (int)(lineWidth/2f));

    context.AddLineToPoint(width - (int)(lineWidth/2f), height);
    context.SetLineWidth(Constants.lineWidth);
    context.SetStrokeColorWithColor(UIColor.Black.CGColor);
    context.StrokePath();
    returnImage = UIImage.FromImage(context.ToImage());
   }
   finally
   {
    Marshal.FreeHGlobal(buffer);
    if (context != null){
     context.Dispose();
     context = null;}
    if (colorSpace != null){
     colorSpace.Dispose();
     colorSpace = null;}
    // GC.Collect();
    //GC.WaitForPendingFinalizers();
   }
   return returnImage;
  }
 }
A: 

Not sure if this will solve your problem (I'm even newer to this than you are), but it seems from this answer that MonoTouch prefers a different paradigm for creating/releasing graphics contexts, along the lines of:

UIGraphics.BeginImageContext(rect.Size)
var context = UIContext.GetCurrentContext();
// ... do stuff ...
UIImage image = UIGraphics.GetImageFromCurrentImageContext();
UIGraphics.EndImageContext();
// ... do something with image ...

I don't know if it's releasing everything properly, but otherwise it seems to work.

David Moles