I have a process that takes an array of HTML strings and builds up a PDF page by page using the following approach:
let printPageRenderer = ReportPrintPageRenderer(withTemplate: self.template)
var pageIndex = 0
data.forEach { (htmlPage) in
let printFormatter = UIMarkupTextPrintFormatter(markupText: htmlPage)
printPageRenderer.addPrintFormatter(printFormatter, startingAtPageAt: pageIndex)
pageIndex += 1
}
let pdfData = drawPDFUsingPrintPageRendererNoImages(printPageRenderer: printPageRenderer)
Where the drawPDFUsing... method used the standard approach I have seen described many times:
let data = NSMutableData()
UIGraphicsBeginPDFContextToData(data, CGRect.init(x: 0, y: 0, width: template.pageWidth, height: template.pageHeight), nil)
printPageRenderer.prepare(forDrawingPages: NSMakeRange(0, printPageRenderer.numberOfPages))
let bounds = UIGraphicsGetPDFContextBounds()
for i in 0...((printPageRenderer.numberOfPages - 1 < 0) ? 0 : printPageRenderer.numberOfPages - 1) {
UIGraphicsBeginPDFPage()
printPageRenderer.drawPage(at: i, in: bounds)
}
UIGraphicsEndPDFContext()
return data
The whole process works perfectly for until I reach PDF documents that are about 130 pages long. In the simulator, I can print documents that reach 250+ pages without issue. However, for the same data on an actual device (iPhone x or iPad pro), the data.forEach loop runs until the pageIndex hits about 130 and then it fails with the following information at this line:
let printFormatter = UIMarkupTextPrintFormatter(markupText: htmlPage)
Thread 1: EXC_BREAKPOINT (code=1, subcode=0x1aa2b0330)
warning: could not execute support code to read Objective-C class data in the process. This may reduce the quality of type information available.
No other exceptions are generated
Memory usage is still OK and while it has spiked, it does not seem too excessive given what I am doing. In addition, it increases like this when running in the simulator and generally only peaks around 300-400MB.
I have tried the following to get around the issue, all without success:
- Wrap various parts of the process in autoreleasepool
- Make sure it was not my HTML data strings by randomising the data as it is processed. No matter the order it always fails after 130ish records
- Switch to iOS13
- Change the process to write out each page as a separate PDF to disk. No matter how I do it, it still always fails after 130 pages are created
I am at a loss as it what is going on and am out of ideas as to how to fix this. Any ideas or help would be appreciated.
As per a suggestion in comments, here is the trace from running bt in the debugger:
* thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BREAKPOINT (code=1, subcode=0x1aa326a14)
frame #0: 0x00000001aa326a14 WebCore`bmalloc::IsoAllocator<bmalloc::IsoConfig<88u> >::allocateSlow(bool) + 252
frame #1: 0x00000001aaaf7f48 WebCore`WebCore::RenderText::createTextBox() + 28
frame #2: 0x00000001aab06e90 WebCore`WebCore::RenderTextLineBoxes::createAndAppendLineBox(WebCore::RenderText&) + 40
frame #3: 0x00000001aa9e2c50 WebCore`WebCore::RenderBlockFlow::constructLine(WebCore::BidiRunList<WebCore::BidiRun>&, WebCore::LineInfo const&) + 344
frame #4: 0x00000001aa9e5944 WebCore`WebCore::RenderBlockFlow::createLineBoxesFromBidiRuns(unsigned int, WebCore::BidiRunList<WebCore::BidiRun>&, WebCore::InlineIterator const&, WebCore::LineInfo&, WebCore::VerticalPositionCache&, WebCore::BidiRun*, WTF::Vector<WebCore::WordMeasurement, 64ul, WTF::CrashOnOverflow, 16ul>&) + 100
frame #5: 0x00000001aa9e825c WebCore`WebCore::RenderBlockFlow::layoutRunsAndFloatsInRange(WebCore::LineLayoutState&, WebCore::BidiResolverWithIsolate<WebCore::InlineIterator, WebCore::BidiRun, WebCore::BidiIsolatedRun>&, WebCore::InlineIterator const&, WebCore::BidiStatus const&, unsigned int) + 4576
frame #6: 0x00000001aa9e5e2c WebCore`WebCore::RenderBlockFlow::layoutRunsAndFloats(WebCore::LineLayoutState&, bool) + 920
frame #7: 0x00000001aa9ea03c WebCore`WebCore::RenderBlockFlow::layoutLineBoxes(bool, WebCore::LayoutUnit&, WebCore::LayoutUnit&) + 1800
frame #8: 0x00000001aa9cd5cc WebCore`WebCore::RenderBlockFlow::layoutBlock(bool, WebCore::LayoutUnit) + 1056
frame #9: 0x00000001aaadad84 WebCore`WebCore::RenderTableCell::layout() + 196
frame #10: 0x00000001aaae8794 WebCore`WebCore::RenderTableRow::layout() + 252
frame #11: 0x00000001aaaea914 WebCore`WebCore::RenderTableSection::layout() + 844
frame #12: 0x00000001aaacf744 WebCore`WebCore::RenderTable::layout() + 1916
frame #13: 0x00000001aa9cf578 WebCore`WebCore::RenderBlockFlow::layoutBlockChild(WebCore::RenderBox&, WebCore::RenderBlockFlow::MarginInfo&, WebCore::LayoutUnit&,
.....
If I strip it back to barest essentials and don't even try to create the PDF:
func exportHTMLContentToPDFNoImages(reportName name:String, fromHTMLData data: [String]) throws -> URL? {
data.shuffled().forEach { (htmlPage) in
autoreleasepool { () -> Void in
let _ = UIMarkupTextPrintFormatter(markupText: htmlPage)
}
}
return nil
}
It still crashes after the 130th pass through the loop. Replacing the htmlPage with some simple html like
<html><body>Hi</body></html>
works just fine. So there must be something in the table I am creating that is causing the issue.
UPDATE
I was able to reduce this to a very simple example that to me demonstrated it was an bug in iOS. I also ended up reporting it to apple. Luckily, it looks like this bug has been fixed in 13.1, so there is a path forward for the future, even if there are issues with iOS 12.