Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Memory warning and crash when creating PDF

When genrating a large PDF, my app recives a memory a warning, then crashes during the generation process of the PDF. The PDF is drawn into a web view, when the pages get above a certain amount (depending on device) I run out of memory

My research into this matter so far leads me to understand I need to:

change UIGraphicsBeginPDFContextToData to IGraphicsBeginPDFContextToFile

  1. Create a reasonable path to a temporary file,

  2. Give that to the function,

  3. Give the file to the webview to load.

  4. Delete the file when done.

Problem is, while I think I grasp it (just) in pricipal, I dont know how to go about acheiving this, or fully understand it so as to impliment it within my code. Advice in this matter very much appricated

Im also open to any other ideas to stop the memory crash

@interface ICPDFPreviewController ()
@property (nonatomic, strong) Certificate *certificate;
@property (nonatomic, strong) NSData *pdfData;
@property (nonatomic) BOOL viewHasUnloaded;
- (void)generatePdf;
- (void)pdfDone:(NSData *)data;
- (NSData *)createPdfWithPages:(NSArray *)pages;
@end

@implementation ICPDFPreviewController
@synthesize certificate=_certificate;
@synthesize scrollView=_scrollView;
@synthesize webView=_webView;
@synthesize pdfData=_pdfData;
@synthesize viewHasUnloaded=_viewHasUnloaded;



- (void)generatePdf
 {
 NSMutableArray *pagesArray = [NSMutableArray array];

 if ([self.certificate.certificateType.title isEqualToString:@"Minor Works"]) {
[pagesArray addObject:[[ICPDFMinorWorksPage1 alloc] initWithCertificate:self.certificate]];
 [pagesArray addObject:[[ICPDFMinorWorksPage2 alloc] initWithCertificate:self.certificate]];

 } else if ([self.certificate.certificateType.title isEqualToString:@"EIC"]) {
[pagesArray addObject:[[ICPDFEICPage1 alloc] initWithCertificate:self.certificate]];
[pagesArray addObject:[[ICPDFEICPage2 alloc] initWithCertificate:self.certificate]];
[pagesArray addObject:[[ICPDFEICPage3 alloc] initWithCertificate:self.certificate]];
[pagesArray addObject:[[ICPDFEICPage4 alloc] initWithCertificate:self.certificate]];
[pagesArray addObject:[[ICPDFEICPage5 alloc] initWithCertificate:self.certificate]];
[self addDistributionBoardsToPagesArray:pagesArray];
ICPDFEICPageFinal *pageFinal = [[ICPDFEICPageFinal alloc]        initWithCertificate:self.certificate];
 pageFinal.pageNumber.text = [NSString stringWithFormat:@"%d", pagesArray.count+1];
[pagesArray addObject:pageFinal];

} else if ([self.certificate.certificateType.title isEqualToString:@"Domestic EIC"]) {
[pagesArray addObject:[[ICPDFDomesticEICPage1 alloc] initWithCertificate:self.certificate]];
[pagesArray addObject:[[ICPDFDomesticEICPage2 alloc] initWithCertificate:self.certificate]];
[pagesArray addObject:[[ICPDFDomesticEICPage3 alloc] initWithCertificate:self.certificate]];
[pagesArray addObject:[[ICPDFDomesticEICPage4 alloc] initWithCertificate:self.certificate]];
[self addDistributionBoardsToPagesArray:pagesArray];
[pagesArray addObject:[[ICPDFDomesticEICPageFinal alloc] initWithCertificate:self.certificate]];

} else if ([self.certificate.certificateType.title isEqualToString:@"EICR"]) {
[pagesArray addObject:[[ICPDFEICRPage1 alloc] initWithCertificate:self.certificate]];
[pagesArray addObject:[[ICPDFEICRPage2 alloc] initWithCertificate:self.certificate]];
[self addObservationsToPagesArray:pagesArray];
[self addDistributionBoardsToPagesArray:pagesArray];
[pagesArray addObject:[[ICPDFEICRInspection alloc] initWithCertificate:self.certificate]];
[pagesArray addObject:[[ICPDFEICRInspectionPage1 alloc] initWithCertificate:self.certificate]];
[pagesArray addObject:[[ICPDFEICRInspectionPage2 alloc] initWithCertificate:self.certificate]];
[pagesArray addObject:[[ICPDFEICRInspectionPage3 alloc] initWithCertificate:self.certificate]];
[pagesArray addObject:[[ICPDFEICRPageFinal alloc] initWithCertificate:self.certificate]];
 }

// Set page count on all pages
int pageNumber = 0;
for (ICCertificateComponent *page in pagesArray) {
page.pageNumber.text = [NSString stringWithFormat:@"%d", ++pageNumber];
page.pageCount.text = [NSString stringWithFormat:@"%d", pagesArray.count];
}

 NSData *pdfData = [self createPdfWithPages:pagesArray];
[self performSelectorOnMainThread:@selector(pdfDone:) withObject:pdfData waitUntilDone:YES];

 }

- (void)pdfDone:(NSData *)data
 {
 self.pdfData = data;
[self.webView loadData:self.pdfData MIMEType:@"application/pdf" textEncodingName:@"utf-8"      baseURL:nil];
 [ICUtils removeProgressView];
 }

 - (NSData *)createPdfWithPages:(NSArray *)pages
  {
 // Creates a mutable data object for updating with binary data, like a byte array
 NSMutableData *pdfData = [NSMutableData data];

 ICCertificateComponent *firstPage = [pages objectAtIndex:0];

UIGraphicsBeginPDFContextToData(pdfData, firstPage.contentView.bounds, nil);

 for (int i = 0; i < pages.count; i++) {
 ICCertificateComponent *thisPage = [pages objectAtIndex:i];
 UIGraphicsBeginPDFPageWithInfo(thisPage.contentView.bounds, nil);


 CGContextRef pdfContext = UIGraphicsGetCurrentContext();
 [thisPage.contentView.layer renderInContext:pdfContext];
 }

 UIGraphicsEndPDFContext();

 return pdfData;
}

- (void)addDistributionBoardsToPagesArray:(NSMutableArray *)pagesArray
{
int pageCount = pagesArray.count;
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"createdAt"       ascending:YES];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptor, nil]; 
NSArray *boards = [self.certificate.distributionBoards      sortedArrayUsingDescriptors:sortDescriptors];
 for (DistributionBoard *thisBoard in boards) {
DebugLog(@"Creating a board page");
ICPDFDistributionBoard *boardPage = [[ICPDFDistributionBoard alloc]        initWithDistributionBoard:thisBoard];
boardPage.pageNumber.text = [NSString stringWithFormat:@"%d", ++pageCount];
DebugLog(@"Page number is %d", pageCount);
[pagesArray addObject:boardPage];

NSSortDescriptor *circuitDescriptor = [[NSSortDescriptor alloc] initWithKey:@"createdAt"     ascending:YES];
NSArray *circuitDescriptors = [[NSArray alloc] initWithObjects:circuitDescriptor, nil]; 
NSArray *circuits = [thisBoard.circuits sortedArrayUsingDescriptors:circuitDescriptors];

 //int circuitCount = circuits.count;
 ICPDFCircuitDetails *circuitDetails = boardPage.circuitDetails;

int circuitCount = 0;
for (Circuit *thisCircuit in circuits) {
circuitCount++;
if (circuitCount > 16) {
    // Add an extension page
    DebugLog(@"Adding an extension sheet");
    circuitCount = 1;
    ICPDFDistributionBoardExtension *boardExtension = [[ICPDFDistributionBoardExtension  alloc]   initWithDistributionBoard:thisBoard];
    [pagesArray addObject:boardExtension];
    boardExtension.pageNumber.text = [NSString stringWithFormat:@"%d", ++pageCount];
    circuitDetails = boardExtension.circuitDetails;
   }
    NSString *key = [NSString stringWithFormat:@"circuitRow%d", circuitCount];
   ICCircuitRow *circuitRow = [circuitDetails valueForKey:key];
   [circuitRow populateFromCircuit:thisCircuit];
  }
 }
}

debug console warnings are a load of these, then crash

 2013-02-08 10:38:35.475 iCertifi[5772:907] Received memory warning.
 2013-02-08 10:38:35.477 iCertifi[5772:907] <ICPDFPreviewController: 0x1eb28930>   didReceiveMemoryWarning
like image 306
JSA986 Avatar asked Feb 05 '13 02:02

JSA986


2 Answers

A long time ago on a completely different platform I was generating PDF files and running into this kind of problem. The solution there was to generate a single page at a time (closing each page (not the document) before opening the next one for imaging). This made a huge difference for my many page documents. Looking at your code, this is the same issue you are facing here. Actually, you have an additional issue which is that you are building all the page data in memory graphically first and then building all the pdf pages also all in memory and it crashes. Read below for details on how to fix the problem.

Reading here

It appears that the sequence you want is:

// start pdf document using default page size (CGRectZero)
UIGraphicsBeginPDFContextToFile(pdfFileName, CGRectZero, nil); 

// then loop through your content drawing it one page at a time.
for ( page p in pages )   // or whatever you are cycling over for page content
{
   UIGraphicsBeginPDFPage();   // or UIGraphicsBeginPDFPageWithInfo

  // do your drawing to the page here by drawing to the current graphics context.
  // if you are setting up transforms and such

}
// close out the last page and the document both
UIGraphicsEndPDFContext();

So what this should do is keep the amount of rendered data in memory to one page at a time (plus some overhead for the document global data).

Ok. I looked over your code and it looks like you are ending up with at least 2 copies of the rendered page in memory, if I'm understanding it correctly.

Here's the key - only draw one page at a time. You don't show what all the ICPDFEICRPage1 and such are, but it appears that they are objects that are rendering page content to a UIView of some kind? That means they have the pixel data for an entire view (that's copy 1). So you are using memory for every single page you will draw all at once before you even render any pdf pages. Then you open the PDF page context that renders it to an NSMutableData (that's the second copy of the data).

The other issue is: why are you rendering to some kind of in memory construct (a view) instead of rendering just onto the pdf page graphics context for each page as you print that page? I'm guessing that this is because you have a UIView hierarchy with fields and everything defined in an xib file so that you don't have to do all the layout and drawing of strings and such manually. Correct?

If so, then the thing to do is to have your pages array be an array be some kind of list of which of the ICPDFEICR... objects you want in what order you want them, but not the actual allocated objects themselves. Perhaps define an enum for each possible type of page, observations, distribution boards, and such. Then your pages array is just an array of integers (those enums in the order you want to display them in the document).

Then only allocate the actual object for the current page, image it to the page, and then release it before you do the next page.

Also, I assume (hope!) you are using ARC because otherwise you have a lot of memory leaks in this code (!!!).

That will hopefully be enough to get you started on the right track. Oh, with the file technique you'll need to give it a file path and then you just pass that path as a file:// url to the webview to display.

Though now that I think of it, why you are using a UIWebView to display PDF? Look at the Zooming PDF Viewer sample for an alternative that doesn't try to load the whole PDF document into memory to display it.

like image 87
Dad Avatar answered Nov 12 '22 21:11

Dad


May be this can help you solve the problems. Note that the code contain

  • creating a file
  • adding the content [start your drawing in after UIGraphicsGetCurrentContext(); method
  • viewing in a webview

Starting from tha button action

- (IBAction)buttonPress:(id)sender
{
    [self createPDFData:[self getPDFFileName]];
}
-(NSString *)getPDFFileName
{
    NSString * fileName = @"Info.PDF";
    
    NSArray *arrayPaths =
    NSSearchPathForDirectoriesInDomains(
                                        NSDocumentDirectory,
                                        NSUserDomainMask,
                                        YES);
    NSString * path = [arrayPaths objectAtIndex:0];
    NSString *pdfFileName = [path stringByAppendingPathComponent:fileName];
    NSLog(@"path : %@",path);
    NSLog(@"pdf file name : %@",pdfFileName);
    return pdfFileName;
    
}

-(void)showPDFFile
{
    
    
    UIWebView *  webView = [[UIWebView alloc] initWithFrame:CGRectMake(0,44,1024,748)];
    
    NSURL *url = [NSURL fileURLWithPath:[self getPDFFileName]];
    NSURLRequest *request = [NSURLRequest requestWithURL:url];
    [webView setScalesPageToFit:YES];
    [webView loadRequest:request];
    
    [self.view addSubview:webView];
    
}


-(void)createPDFData:(NSString *)fileName{
    
    CGRect pageFrame = CGRectMake(0,0,768,1024);
    UIGraphicsBeginPDFContextToFile(fileName, CGRectZero, nil);//This starts drawing a pdf to file named "filename"  UIGraphicsBeginPDFPageWithInfo(pageFrame, nil);//For each new page call this
    UIGraphicsGetCurrentContext();
    UIGraphicsBeginPDFPageWithInfo(pageFrame, nil);//Next or new Page
    NSString * timeLabel = [NSString stringWithFormat:@"Some text"];
    [timeLabel drawInRect:CGRectMake(200, 200, 428, 44) withFont:[UIFont boldSystemFontOfSize:10] lineBreakMode:NSLineBreakByWordWrapping alignment:NSTextAlignmentLeft];
    UIGraphicsEndPDFContext();
    [self showPDFFile];
}
like image 27
Lithu T.V Avatar answered Nov 12 '22 20:11

Lithu T.V