创建PDF时内存警告和崩溃

时间:2013-02-05 02:27:27

标签: ios objective-c cocoa-touch memory-management

在生成大型PDF时,我的应用程序会在内存中收到警告,然后在PDF的生成过程中崩溃。 PDF被绘制到Web视图中,当页面超过一定数量(取决于设备)时,我的内存不足

到目前为止,我对这个问题的研究让我明白我需要:

UIGraphicsBeginPDFContextToData更改为IGraphicsBeginPDFContextToFile

  1. 创建临时文件的合理路径

  2. 将其交给函数

  3. 将文件提交给webview加载。

  4. 完成后删除文件。

  5. 问题是,虽然我认为我(仅仅)在主流中抓住它,但我不知道如何实现这一点,或者完全理解它以便在我的代码中实现它。这个问题的建议非常适合

    我也对其他任何能阻止内存崩溃的想法持开放态度

    @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
    

4 个答案:

答案 0 :(得分:5)

很久以前,在一个完全不同的平台上,我正在生成PDF文件并遇到这种问题。解决方案是一次生成一个页面(关闭每个页面(不是文档),然后打开下一个页面进行成像)。这对我的许多页面文档产生了巨大的影响。查看您的代码,这与您在此处遇到的问题相同。实际上,您还有一个问题,就是首先以图形方式在内存中构建所有页面数据,然后在内存中构建所有pdf页面并使其崩溃。请阅读以下有关如何解决问题的详细信息。

阅读here

您想要的序列似乎是:

// 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();

所以这应该做的是一次将内存中的渲染数据量保持为一页(加上文档全局数据的一些开销)。

确定。我查看了你的代码,看起来你最终在内存中至少有2个渲染页面的副本,如果我正确理解它。

这是关键 - 一次只能绘制一页。你没有显示所有ICPDFEICRPage1等等,但看起来它们是将页面内容呈现给某种UIView的对象?这意味着他们拥有整个视图的像素数据(即副本1)。因此,您甚至在渲染任何pdf页面之前,将为每个页面使用内存。然后打开PDF页面上下文,将其呈现为NSMutableData(这是数据的第二个副本)。

另一个问题是:为什么要渲染某种内存构造(视图),而不是在打印该页面时只渲染到每页的pdf页面图形上下文?我猜测这是因为你有一个UIView层次结构,其中包含字段和xib文件中定义的所有内容,因此您不必手动完成所有字符串的布局和绘制。正确的吗?

如果是这样,那么要做的就是让你的pages数组成为一个数组,列出你希望它们按你想要的顺序排列的ICPDFEICR...个对象,而不是实际分配的对象他们自己。也许为每种可能类型的页面,观察,分发板等定义枚举。然后你的pages数组只是一个整数数组(那些枚举按你想要在文档中显示的顺序)。

然后只为当前页面分配实际对象,将其映像到页面,然后在执行下一页之前将其释放。

另外,我假设(希望!)你正在使用ARC,否则你在这段代码中会有很多内存泄漏(!!!)。

希望足以让你开始走上正轨。哦,使用文件技术,你需要给它一个文件路径,然后你只需将该路径作为file:// url传递给webview进行显示。

虽然现在我想到了,为什么你使用UIWebView来显示PDF?查看Zooming PDF Viewer示例,了解不会尝试将整个PDF文档加载到内存中以显示它的替代方法。

答案 1 :(得分:2)

可能这可以帮助您解决问题。 请注意,代码包含

  • 创建文件
  • 添加内容[在UIGraphicsGetCurrentContext();方法
  • 之后开始绘图
  • 在网页浏览中查看

从按钮操作开始

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

快乐编码:)

答案 2 :(得分: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];
     }
 }

答案 3 :(得分: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;
}