iOS Swift中使用PDFKit对比大页数PDF时应用因内存崩溃
PDF对比NativeModule内存崩溃问题优化方案
问题背景
我开发了一个NativeModule用于对比两个PDF文件,汇总差异后返回给React Native桥接层。当前代码在PDF页数少于100时能正常运行,但页数增加后会因内存问题崩溃。我已经尝试过autoreleasepool等方法,相关代码如下:
Native Module (PDF Manager)
import Foundation import PDFKit import SwiftDiff @objc(PDFManager) class PDFManager: NSObject { private var currentPageIndex: Int = 0 private var lastProcessedIndex: Int = 0 @objc func compareAndMerge(_ pdfPath1: String, pdfPath2: String, documentName: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) { do { guard let firstPdfURL = URL(string: pdfPath1), let secondPdfURL = URL(string: pdfPath2) else { reject("pdfInvalidPath", "Invalid PDF path", nil) return } guard let pdf1 = PDFDocument(url: firstPdfURL), let pdf2 = PDFDocument(url: secondPdfURL) else { reject("PDFLoadError", "Failed to load PDFs", nil) return } let summary = try compareAndSummarize(doc1: pdf1, doc2: pdf2) let json = summarizeToJSON(summary: summary) resolve(json) } catch { print("Error merging PDFs:", error) reject("pdfMergeError", "Error merging PDFs", nil) } } @objc static func requiresMainQueueSetup() -> Bool { return true } private func compareAndSummarize(doc1: PDFDocument, doc2: PDFDocument) throws -> (String, Int) { var changesSummary = "" var pagesChanged = 0 // Iterate over pages of doc1 and doc2 simultaneously let pageCount = min(doc1.pageCount, doc2.pageCount) for pageIndex in 0..<pageCount { autoreleasepool { guard let page1 = doc1.page(at: pageIndex), let page2 = doc2.page(at: pageIndex) else { return } let page1Text = page1.string ?? "" let page2Text = page2.string ?? "" changesSummary += "Page \(pageIndex + 1):\n" // Calculate differences between the text of the two pages let diff = diff(text1: page1Text, text2: page2Text) var pageChanged = false // Process each change in the differences for change in diff { switch change { case .delete(let value): pageChanged = true changesSummary += "- Deleted: \(value)\n" case .insert(let value): pageChanged = true changesSummary += "+ Inserted: \(value)\n" case .equal( _): break } } if pageChanged { pagesChanged += 1 } } } return (changesSummary, pagesChanged) } func summarizeToJSON(summary: (String, Int)) -> String { let dictionary: [String: Any] = ["changes": summary.0, "pagesChanged": summary.0] let data = try? JSONSerialization.data(withJSONObject: dictionary, options: [.prettyPrinted]) return String(data: data!, encoding: .utf8) ?? "" } }
React Native 代码
import React, {useState} from 'react'; import {ActivityIndicator, NativeModules, View, useWindowDimensions} from 'react-native'; import Text from '../../Components/Text'; import s from '../../assets/styles'; import {Button} from '../../Components/Buttons'; import DocumentPicker from 'react-native-document-picker'; import {useSafeAreaInsets} from 'react-native-safe-area-context'; function HomeScreen() { const [pdf1, setPDF1] = useState(null); const [pdf2, setPDF2] = useState(null); const [merging, setMerging] = useState(false); const {PDFManager} = NativeModules; const insets = useSafeAreaInsets(); const {width} = useWindowDimensions(); const buttonWidth = width / 2 - 25; const selectFirstPDF = async () => { try { const result = await DocumentPicker.pickSingle({ type: DocumentPicker.types.pdf, copyTo: 'cachesDirectory', }); if (!result?.fileCopyUri) { return; } setPDF1(result); } catch (error) {} }; const selectSecondPDF = async () => { try { const result = await DocumentPicker.pickSingle({ type: DocumentPicker.types.pdf, copyTo: 'cachesDirectory', }); if (!result?.fileCopyUri) { return; } setPDF2(result); } catch (error) {} }; const updateManuals = async () => { setMerging(true); try { const result = await PDFManager.compareAndMerge( pdf1?.fileCopyUri, pdf2?.fileCopyUri, `${pdf2?.name}_PDFCompared.pdf`, ); setMerging(false); if (result) { const json = JSON.parse(result); console.log('json -> ', json); } } catch (error) { console.log('Error -> ', error); } finally { setMerging(false); } }; return ( <View style={[s.flex1, {paddingTop: insets.top}, s.jcac]}> <View style={[s.mHor10]}> <Text style={[s.f30, s.ls2, s.fontBlack, s.textCenter, s.as]}> Keep Highlights </Text> <Text style={[s.fontSemiBold, s.mVer10]}> Effortlessly compare your PDF manuals and keep your highlights updated. Files are compared locally and your data always remain confidential. </Text> <View style={[s.flexRow, s.aic, s.jsb, s.mVer10]}> <Button title={pdf1?.name ? pdf1?.name : 'Highlighted PDF'} style={[s.bgPurple, s.br30, {width: buttonWidth}, s.me10]} textStyle={[s.fontMedium, s.f14, s.textWhite]} onPress={selectFirstPDF} /> <Button title={pdf2?.name ? pdf2?.name : 'Updated manual'} onPress={selectSecondPDF} textStyle={[s.fontMedium, s.f14, s.textWhite]} style={[s.br30, {width: buttonWidth}]} /> </View> <Button title={'Get updated manual'} isDisabled={!pdf1?.fileCopyUri || !pdf2?.fileCopyUri} onPress={updateManuals} /> {merging && <ActivityIndicator size={'large'} color={'white'}/>} </View> </View> ); } export default HomeScreen;
优化方案
1. 移至后台线程处理
当前代码在主线程执行大PDF对比,既阻塞UI又加剧内存压力。修改requiresMainQueueSetup并将处理逻辑移到后台队列:
@objc static func requiresMainQueueSetup() -> Bool { return false } @objc func compareAndMerge(_ pdfPath1: String, pdfPath2: String, documentName: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) { DispatchQueue.global(qos: .userInitiated).async { do { guard let firstPdfURL = URL(string: pdfPath1), let secondPdfURL = URL(string: pdfPath2) else { DispatchQueue.main.async { reject("pdfInvalidPath", "无效的PDF路径", nil) } return } guard let pdf1 = PDFDocument(url: firstPdfURL), let pdf2 = PDFDocument(url: secondPdfURL) else { DispatchQueue.main.async { reject("PDFLoadError", "加载PDF失败", nil) } return } let summary = try self.compareAndSummarize(doc1: pdf1, doc2: pdf2) let json = self.summarizeToJSON(summary: summary) DispatchQueue.main.async { resolve(json) } } catch { DispatchQueue.main.async { print("对比PDF错误:", error) reject("pdfMergeError", "对比PDF时出错", error) } } } }
2. 优化字符串拼接性能
直接用String拼接会频繁生成新对象,改用NSMutableString减少内存开销:
private func compareAndSummarize(doc1: PDFDocument, doc2: PDFDocument) throws -> (String, Int) { let changesSummary = NSMutableString() var pagesChanged = 0 let pageCount = min(doc1.pageCount, doc2.pageCount) for pageIndex in 0..<pageCount { autoreleasepool { guard let page1 = doc1.page(at: pageIndex), let page2 = doc2.page(at: pageIndex) else { return } let page1Text = page1.string ?? "" let page2Text = page2.string ?? "" changesSummary.append("第\(pageIndex + 1)页:\n") let diff = diff(text1: page1Text, text2: page2Text) var pageChanged = false for change in diff { switch change { case .delete(let value): pageChanged = true changesSummary.append("- 删除: \(value)\n") case .insert(let value): pageChanged = true changesSummary.append("+ 新增: \(value)\n") case .equal: break } } if pageChanged { pagesChanged += 1 } } } return (changesSummary as String, pagesChanged) }
3. 修复JSON序列化错误
原代码中pagesChanged错误地赋值为字符串,修正为整数:
func summarizeToJSON(summary: (String, Int)) -> String { let dictionary: [String: Any] = ["changes": summary.0, "pagesChanged": summary.1] guard let data = try? JSONSerialization.data(withJSONObject: dictionary, options: []) else { return "" } return String(data: data, encoding: .utf8) ?? "" }
(注:去掉.prettyPrinted可进一步减少JSON数据体积)
4. 加强内存释放
- 确保
autoreleasepool包裹每一页的所有处理逻辑,让每页的临时对象及时释放 - 若PDF文档过大,可考虑分批加载处理,处理完部分页面后主动释放对应资源(例如重置PDFDocument引用)
5. 调试与监控
- 使用Xcode的Memory Graph Debugger排查内存泄漏点
- 用Instruments的Memory工具跟踪内存增长,定位内存占用过高的对象
内容的提问来源于stack exchange,提问作者Anwer Solangi
相关产品推荐
相关产品推荐

