Skip to content

Commit fe5f30a

Browse files
davidhooclaude
andcommitted
fix: 分栏预览 WebView 生命周期避免 use-after-free
分栏预览的 webMarkdownView 此前与渲染模式共享同一 @State exportedPage 绑定,切模式/切分栏过渡窗口两者争抢同一 WebPage 导致 over-release (EXC_BAD_ACCESS in objc_release during autorelease drain)。 - 预览 WebView 改用独立 @State previewExportedPage,解除共享 WebPage 握手 - 防抖 .task 加守卫:分栏关闭时不写 previewContent,避免追已拆毁视图 - Raw 模式 PDF 导出走独立 HTML 路径,不读预览绑定,行为不变 经 main 对照确认崩溃为 PR #14 引入;修复后 5 阶段密集自动化零崩溃, 195 tests 全过,调试/发布构建通过。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 2645d26 commit fe5f30a

1 file changed

Lines changed: 14 additions & 5 deletions

File tree

Sources/MarkdownReader/Views/DetailView.swift

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,12 @@ struct DetailView: View {
7878
/// 编辑模式右侧实时预览的内容(防抖更新,避免每次击键都全量渲染)
7979
@State private var previewContent: String = ""
8080

81+
/// 分栏预览 WebView 自身的 WebPage 引用。
82+
/// 不与渲染模式的 `exportedPage` 共享:预览 WebView 移除/重建时不应影响渲染模式
83+
/// 仍持有的 WebPage,避免两者在切模式/切分栏的过渡窗口争抢同一绑定导致 use-after-free。
84+
/// Raw 模式 PDF 导出走独立 HTML 路径,不读取此绑定。
85+
@State private var previewExportedPage: WebPage?
86+
8187

8288
var body: some View {
8389
VStack(spacing: 0) {
@@ -652,7 +658,7 @@ struct DetailView: View {
652658

653659
// 渲染模式视图 — 仅在渲染模式下显示(渲染模式布局保持不变)
654660
if documentViewModel.displayMode == .rendered {
655-
webMarkdownView(content: documentViewModel.content)
661+
webMarkdownView(content: documentViewModel.content, exportedPage: $exportedPage)
656662
}
657663
}
658664
.frame(maxWidth: .infinity, maxHeight: .infinity)
@@ -686,7 +692,7 @@ struct DetailView: View {
686692
if documentViewModel.displayMode == .raw && !documentViewModel.isPlainTextMode && appViewModel.isSplitPreviewEnabled {
687693
Rectangle().fill(themeColors.border).frame(width: 1)
688694

689-
webMarkdownView(content: previewContent)
695+
webMarkdownView(content: previewContent, exportedPage: $previewExportedPage)
690696
.frame(maxWidth: .infinity, maxHeight: .infinity)
691697
}
692698
}
@@ -703,9 +709,12 @@ struct DetailView: View {
703709
previewContent = documentViewModel.content
704710
}
705711
.task(id: documentViewModel.content) {
706-
// 击键防抖:停顿 200ms 无新输入后再刷新右侧预览
712+
// 击键防抖:停顿 200ms 无新输入后再刷新右侧预览。
713+
// 分栏已关闭时不写 previewContent:避免对已拆毁的预览 WebView 触发无谓重算,
714+
// 也避免与视图树拆毁竞态。
707715
try? await Task.sleep(for: .milliseconds(200))
708716
guard !Task.isCancelled else { return }
717+
guard appViewModel.isSplitPreviewEnabled else { return }
709718
previewContent = documentViewModel.content
710719
}
711720
.overlay(alignment: .topTrailing) {
@@ -739,7 +748,7 @@ struct DetailView: View {
739748

740749
/// 渲染视图(渲染模式全宽显示 / 编辑模式右栏实时预览共用)
741750
@ViewBuilder
742-
private func webMarkdownView(content: String) -> some View {
751+
private func webMarkdownView(content: String, exportedPage: Binding<WebPage?>) -> some View {
743752
WebViewMarkdownView(
744753
content: content,
745754
fileURL: documentViewModel.currentFileURL,
@@ -767,7 +776,7 @@ struct DetailView: View {
767776
onOpenLinkedMarkdownFile: { [weak session] url in
768777
session?.handleLinkedMarkdownFile(url.standardizedFileURL)
769778
},
770-
exportedPage: $exportedPage
779+
exportedPage: exportedPage
771780
)
772781
.onChange(of: documentViewModel.scrollToLineRequest) { _, newValue in
773782
if newValue != nil {

0 commit comments

Comments
 (0)