首页 > 解决方案 > 在 iOS 中从 HTML 创建 PDF 时出现异常

问题描述

我有一个处理 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个页面后仍然总是失败

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

根据评论中的建议,这是在调试器中运行 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 中的一个错误。我也最终向苹果报告了它。幸运的是,这个错误看起来已经在 13.1 中修复,所以即使 iOS 12 存在问题,未来也有前进的道路。

标签: iosswift

解决方案


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

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

是引发异常的行(请注意,这是来自 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)

推荐阅读