【问题标题】:Exception when creating PDF from HTML in iOS在 iOS 中从 HTML 创建 PDF 时出现异常
【发布时间】:2019-09-04 22:05:23
【问题描述】:

我有一个过程,它采用 HTML 字符串数组并使用以下方法逐页构建 PDF:

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)

drawPDFUsing... 方法使用了我见过多次描述的标准方法:

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

整个过程完美运行,直到我获得大约 130 页长的 PDF 文档。在模拟器中,我可以毫无问题地打印超过 250 页的文档。但是,对于实际设备(iPhone x 或 iPad pro)上的相同数据,data.forEach 循环会一直运行,直到 pageIndex 达到大约 130,然后它会在此行显示以下信息失败:

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.

没有其他异常产生

内存使用量还可以,虽然它已经飙升,但考虑到我正在做的事情,它似乎并不过分。此外,在模拟器中运行时它会像这样增加,并且通常只有 300-400MB 左右的峰值。

我尝试了以下方法来解决这个问题,但都没有成功:

  1. 将进程的各个部分包装在 autoreleasepool 中
  2. 通过在处理数据时随机化数据来确保它不是我的 HTML 数据字符串。无论订单如何,它总是在 130 条记录后失败
  3. 切换到 iOS13
  4. 更改将每个页面作为单独的 PDF 写入磁盘的过程。不管我怎么做,创建130个页面后仍然总是失败

我不知所措,因为它正在发生什么,并且不知道如何解决这个问题。任何想法或帮助将不胜感激。

根据 cmets 中的建议,这是在调试器中运行 bt 的跟踪:

* 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&, 
.....

如果我将其剥离为最基本的要素,甚至不尝试创建 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
    }

在第 130 次通过循环后它仍然崩溃。用一些简单的 html 替换 htmlPage,例如

<html><body>Hi</body></html>

工作得很好。因此,我正在创建的表中一定存在导致问题的内容。

更新

我能够将其简化为一个非常简单的示例,向我证明这是 iOS 中的一个错误。我也最终向苹果报告了它。幸运的是,这个 bug 看起来在 13.1 中已经修复,所以即使 iOS 12 存在问题,未来也有前进的道路。

【问题讨论】:

  • 从 Xcode 连接到设备时,您能否重现该问题?如果是这样,您可以提供堆栈跟踪吗?如果发生异常时 Xcode 没有停止,请确保您启用了 Exception Breakpoints。见stackoverflow.com/a/17802723/196964
  • 以上信息来自Xcode所附的设备。我没有得到堆栈跟踪或任何有用的东西。只是自动中断调试器,突出显示该行和第一条错误消息。调试控制台有第二条消息。不幸的是,这真的没有太多信息。
  • 如果你打开 lldb 调试器并运行bt 来获取堆栈跟踪呢? stackoverflow.com/a/26770913/196964
  • 谢谢...不知道那个。我更新了我的答案。仍然不确定发生了什么,但也许你会

标签: ios swift


【解决方案1】:

要么内存不足,要么堆已损坏。我的猜测是,尽管你的内存使用截图,你的内存不足。

这个EXC_BREAKPOINT 出现在WebKit 的bmalloc::IsoAllocator 中。

* 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

Here is the line 引发异常(注意这是来自 WebKit 中继,可能与您手机上运行的版本不同):


template<typename Config>
BNO_INLINE void* IsoAllocator<Config>::allocateSlow(bool abortOnFailure)
{
    ...

    return m_freeList.allocate<Config>([] () { BCRASH(); return nullptr; });
}

m_freeList.allocate 如果无法从自己的列表中分配,则运行 lambda(括号中的部分)中的内容。

BCRASH 引发您看到的异常。来自BAssert.h

// Crash with a SIGTRAP i.e EXC_BREAKPOINT.
// We are not using __builtin_trap because it is only guaranteed to abort, but not necessarily
// trigger a SIGTRAP. Instead, we use inline asm to ensure that we trigger the SIGTRAP.
#define BCRASH() do { \
        BBreakpointTrap(); \
        __builtin_unreachable(); \
    } while (false)

此分配似乎是 WebKit 呈现 PDF 的一部分,但它可能会因您不断增长的NSMutableData 而受到压力。

尝试使用UIGraphicsBeginPDFContextToFile 而不是UIGraphicsBeginPDFContextToData,看看是否仍然发生崩溃。 UIGraphicsBeginPDFContextToFile 的实现可能会将页面写入磁盘,而不是像您正在做的那样一次将整个结果全部放入内存。

切换到UIGraphicsBeginPDFContextToFile后,您可以将PDF作为文件使用,或者,如果您需要加载到NSData,使用此方法使用内存映射文件:

let data = try Data(contentsOf: URL(fileURLWithPath: newPDFPath), options: .mappedIfSafe)

【讨论】:

  • 我绝对同意内存问题是有道理的,但是,我不认为这是我可以控制的。我可以将方法剥离为像这样简单的方法,但它仍然以完全相同的方式崩溃。似乎该特定方法中的内存没有得到很好的处理。 data.forEach { (htmlPage) in autoreleasepool { () -> Void in let _ = UIMarkupTextPrintFormatter(markupText: htmlPage) } }
  • 您是否尝试使用 UIGraphicsBeginPDFContextToFile 来查看崩溃是否仍然发生?
  • 抱歉,我可能没有完全理解您的建议。在我上面的评论中,我调用的唯一代码是我的评论中显示的代码。我什至不尝试写出 PDF 或调用和 UIGraphicsBeing 方法。我只遍历 html 数据并调用 UIMarkupTextPrintFormatter 方法。因此,如果内存出现问题,我认为这完全是由于该方法造成的。我不明白吗?
  • 您问题中的第二个代码块调用UIGraphicsBeginPDFContextToData。我建议您用UIGraphicsBeginPDFContextToFile 替换以查看崩溃是否仍然发生。一旦将其写入文件,您可以将其用作文件而不是 NSMutableData,或者,如果您确实需要在数据对象中使用它,则使用 mmap 选项将其从磁盘加载到 NSData 中。我会更新答案以澄清这一点。
  • 我明白了,但在我最近做的测试中,从未调用过该方法。我会更新我的问题
猜你喜欢
  • 2012-08-28
  • 1970-01-01
  • 1970-01-01
  • 2013-05-12
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-02-17
  • 1970-01-01
相关资源
最近更新 更多