创建PDF时出现内存警告和崩溃问题

5
当生成大型PDF文件时,我的应用程序会收到内存警告,然后在PDF生成过程中崩溃。PDF被绘制到Web视图中,当页面数量超过一定量(取决于设备)时,我会耗尽内存。
我对这个问题的研究迄今为止导致我理解需要做以下几点:
UIGraphicsBeginPDFContextToData更改为IGraphicsBeginPDFContextToFile 创建一个合理的路径到一个临时文件,
将其提供给函数,
将该文件提供给WebView进行加载。
完成后删除该文件。
问题是,虽然我认为我在原则上掌握了它,但我不知道如何实现它,或者如何完全理解它以便在我的代码中实现。非常感谢您在这件事上的建议。
我也乐于接受任何其他想法来防止内存崩溃。
@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];
  }
 }
}

调试控制台警告是许多这些,然后崩溃。

 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

包括崩溃日志。 - Lithu T.V
抱歉,我觉得我可能漏掉了一些东西。你想从什么生成PDF文件?你的源是什么?一些图形上下文吗?你在哪里遇到了内存问题?在生成文件时还是在webview中显示时? - Fábio Oliveira
4个回答

6
很久以前,在完全不同的平台上,我正在生成PDF文件并遇到了这种问题。解决方案是逐页生成(在成像下一页之前关闭每个页面(而不是文档))。对于我的多页文档来说,这产生了巨大的影响。看着你的代码,你在这里面临的是相同的问题。实际上,你还有一个额外的问题,那就是你首先在内存中构建所有页面数据,然后在内存中构建所有pdf页面,最终导致崩溃。阅读下面的详细信息以了解如何解决该问题。
阅读此处
似乎你想要的顺序是:
// 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();

所以这个操作应该将渲染数据的数量保持在内存中一次只有一页(加上文档全局数据的一些开销)。
好的。我看了你的代码,如果我理解正确的话,它似乎会在内存中产生至少两份已渲染页面的副本。
这里的关键是 - 一次只绘制一页。你没有展示所有的ICPDFEICRPage1之类的内容,但它们似乎是将页面内容渲染到某种UIView对象上的对象?这意味着它们拥有整个视图的像素数据(这是第一次复制)。因此,在您渲染任何pdf页面之前,您正在使用为每个要一次性绘制的页面而使用的内存。然后打开一个渲染它到NSMutableData的PDF页面上下文(这是数据的第二个复制)。
另一个问题是:为什么你要渲染到某种内存构造(视图)而不是在打印每一页时直接渲染到pdf页面图形上下文中呢?我猜这是因为你有一个UIView层次结构,其中包含在xib文件中定义的字段和其他内容,这样你就不必手动进行布局和字符串绘制。对吗?
如果是这样的话,那么需要做的就是让你的页面数组成为一些列表,其中包含了你想要的ICPDFEICR...对象的哪些以及以什么顺序显示它们,但不包括实际分配的对象本身。也许可以为每种可能的页面类型定义一个枚举,如观察、分发板等。然后你的页面数组只是一个整数数组(按照你想在文档中显示它们的顺序排列的那些枚举)。
然后只为当前页分配实际对象,在将其映射到页面上之前释放它,然后再处理下一页。
此外,我假设(希望!)你正在使用ARC,因为否则这段代码会有很多内存泄漏问题(!!!)。
希望这些能让你正确开始。哦,使用文件技术时,你需要给它一个文件路径,然后只需将该路径作为 file:// URL 传递给 webview 以显示。
虽然现在我想起来了,为什么要使用 UIWebView 来显示 PDF 呢?看一下 Zooming PDF Viewer 示例,这是一种不尝试将整个 PDF 文档加载到内存中以显示的替代方法。

我在Google上搜索了这个问题,不确定如何在我的代码中处理它,你有什么建议吗? - JSA986
我在回答中增加了很多更具体的信息,因此如果您再读一遍,我相信您会发现它回答了您的问题。 - Dad
只想说感谢您提供如此详细的答案,我仍在努力解决这个问题,尽管我已经多次重读了您的答案,但我仍然无法理解。是的,我正在使用ARC。再次感谢。 - JSA986
抱歉我的回答不够清晰。如何澄清呢?嗯,这样怎么样:逐个构建视图并将其映射到单个pdf页面,而不是一次性构建所有页面。因此,在上述循环中,同时构建页面和映像,并释放它们以进入下一页的循环。 - Dad
你的回答非常清晰,是我自己没有理解!好的,我要再试一次,如果我们解决了这个问题,我会请你喝很多啤酒的!我大致明白了,但实际上,逐页构建页面,然后发布它是我做错的地方。此外,如果写PDFContextToData有问题吗? - JSA986
最好将其写入文件,因为整个文档可能无法放入内存中。在执行PDF循环之前,请查看您的代码。看看您将所有数据放入ICPDFEICRPage1、...Page2等位置?那就是您在内存中构建所有页面(我认为它们实际上是视图)的地方。只在pdf页面写入循环中处理当前页面,然后释放它。 - Dad

2

也许这能帮助您解决问题。

请注意,代码包含:

  • 创建文件
  • UIGraphicsGetCurrentContext(); 方法后开始绘图
  • 在 Webview 中查看

从按钮操作开始。

- (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];
}

0
如果其他方法都不起作用,您可以尝试我的解决方案。它会滚动webView并逐页将其添加到PDF中。我无法以其他方式解决这个问题。 这个解决方案的缺点是它使用了一个计时器,并且在生成PDF时您将在应用程序中看到滚动。
以下是代码:
...
    CGPoint savedContentOffset;
    CGRect savedFrame;
    NSMutableData * pdfData;
    int pdfOffset;
    NSTimer *timer;
...

- (void)preparePdf {
    CGFloat height = aWebView.scrollView.contentSize.height;
    CGRect screenRect = [[UIScreen mainScreen] bounds];
    int pageHeight = screenRect.size.height;

    savedContentOffset = aWebView.scrollView.contentOffset;
    savedFrame = aWebView.frame;
    pdfData=[NSMutableData data];

    aWebView.scrollView.contentOffset = CGPointZero;
    aWebView.frame = CGRectMake(aWebView.frame.origin.x,aWebView.frame.origin.y,aWebView.frame.size.width,pageHeight);

    UIGraphicsBeginPDFContextToData(pdfData, aWebView.frame, nil);

    NSLog(@"pageHeight = %d", pageHeight);
    pdfOffset = 0;

    timer = [NSTimer scheduledTimerWithTimeInterval: 0.4
                                             target: self
                                           selector: @selector(printPdfPage:)
                                           userInfo: nil
                                            repeats: YES];
}

- (void) printPdfPage: (NSTimer*) timer {
    CGFloat height = aWebView.scrollView.contentSize.height;
    CGRect screenRect = [[UIScreen mainScreen] bounds];
    int pageHeight = screenRect.size.height;

    NSLog(@"pdfOffset = %d", pdfOffset);
    UIGraphicsBeginPDFPage();
    [aWebView drawViewHierarchyInRect:CGRectMake(0, 0, screenRect.size.width, pageHeight) afterScreenUpdates:YES];

    pdfOffset += pageHeight;
    aWebView.scrollView.contentOffset = CGPointMake(0, pdfOffset);

    if (pdfOffset > height) {
        [timer invalidate];
        NSLog(@"pdf data size: %ld", pdfData.length);
        [self emailPdf: pdfData];
    }
}

- (void) emailPdf: (NSMutableData*) pdfData {

    // finally end the PDF context.
    UIGraphicsEndPDFContext();

    aWebView.scrollView.contentOffset = savedContentOffset;
    aWebView.frame = savedFrame;

    MFMailComposeViewController *mailer = [[MFMailComposeViewController alloc] init];
    mailer.mailComposeDelegate = self;
    [mailer setSubject:[@"iPad Screenshot " stringByAppendingString:[self getDate]]];
    [mailer addAttachmentData:pdfData mimeType:@"application/pdf" fileName:[[self getDate] stringByAppendingPathExtension:@"pdf"]];
    NSString *emailBody = @"";
    [mailer setMessageBody:emailBody isHTML:NO];
    [self presentViewController:mailer animated:YES completion:nil];

    if ([SVProgressHUD isVisible]) {
        [SVProgressHUD showSuccessWithStatus:@"Screenshot prepared."];
    }
    pdfData = nil;
    timer = nil;
}

0
尝试在您的for循环中使用@autoreleasepool。
for (int i = 0; i < pages.count; i++) {

// notice this line 
     @autureleasepool 
     {
       ICCertificateComponent *thisPage = [pages objectAtIndex:i];
       UIGraphicsBeginPDFPageWithInfo(thisPage.contentView.bounds, nil);


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

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