长章节内存优化 P0:每页子串改按需构造并新增内存打点
- RDEPUBTextPage.content 由存储属性改为基于 chapterContent+contentRange 的计算属性,三处构造点不再生成每页子串,章节缓存窗口内每章少一份 整章文本常驻副本 - 新增 RDEPUBMemoryProbe(--demo-memory-probe 开启),在章节插入、 每 20 次翻页、设置失效、转屏完成时输出 phys_footprint - shouldAvoidReaderPageCaching 补注释,锁定放开前置条件为 P1-1 - 新增实施清单文档 LONG_CHAPTER_MEMORY_OPTIMIZATION_PLAN.md 并勾选 P0 验证:Demo 编译通过;UI 回归 9/9 通过(PageNavigation/ ReaderOpenClose/LargeBookOnDemand) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
5a41066b66
commit
e4e629a06c
@@ -291,7 +291,6 @@ public final class RDEPUBTextBookBuilder {
|
||||
pageIndexInChapter: localPageIndex,
|
||||
totalPagesInChapter: effectiveFrames.count,
|
||||
chapterContent: chapterAttributedContent,
|
||||
content: content.attributedSubstring(from: range),
|
||||
contentRange: range,
|
||||
pageStartOffset: range.location,
|
||||
pageEndOffset: range.location + max(range.length - 1, 0),
|
||||
|
||||
@@ -54,7 +54,15 @@ public struct RDEPUBTextPage: Equatable {
|
||||
|
||||
public var chapterContent: NSAttributedString
|
||||
|
||||
public var content: NSAttributedString
|
||||
/// Page substring derived on demand from `chapterContent` + `contentRange`.
|
||||
/// Not stored: keeping a per-page substring alive roughly doubles the
|
||||
/// chapter's resident text memory across the chapter cache window.
|
||||
public var content: NSAttributedString {
|
||||
let bounds = NSRange(location: 0, length: chapterContent.length)
|
||||
let clamped = NSIntersectionRange(contentRange, bounds)
|
||||
guard clamped.length > 0 else { return NSAttributedString() }
|
||||
return chapterContent.attributedSubstring(from: clamped)
|
||||
}
|
||||
|
||||
public var contentRange: NSRange
|
||||
|
||||
|
||||
@@ -75,7 +75,6 @@ public final class RDPlainTextBookBuilder {
|
||||
pageIndexInChapter: localPageIndex,
|
||||
totalPagesInChapter: effectiveFrames.count,
|
||||
chapterContent: chapterAttributedContent,
|
||||
content: content.attributedSubstring(from: range),
|
||||
contentRange: range,
|
||||
pageStartOffset: range.location,
|
||||
pageEndOffset: range.location + max(range.length - 1, 0),
|
||||
|
||||
@@ -345,6 +345,7 @@ extension RDEPUBReaderController: RDReaderDataSource, RDReaderPageProvider, RDRe
|
||||
}
|
||||
|
||||
public func pageNum(readerView: RDReaderView, pageNum: Int) {
|
||||
RDEPUBMemoryProbe.logPageTurn()
|
||||
readerContext.markUserNavigationActivity()
|
||||
updateCurrentSelection(nil)
|
||||
reconcileTextPaginationSizeIfNeeded(for: pageNum)
|
||||
|
||||
@@ -297,6 +297,9 @@ public final class RDEPUBReaderController: UIViewController {
|
||||
public override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
|
||||
super.viewWillTransition(to: size, with: coordinator)
|
||||
runtime.viewportMonitor.viewWillTransition(with: coordinator)
|
||||
coordinator.animate(alongsideTransition: nil) { _ in
|
||||
RDEPUBMemoryProbe.log("orientationTransition size=\(Int(size.width))x\(Int(size.height))")
|
||||
}
|
||||
}
|
||||
|
||||
func showSearchBar() {
|
||||
|
||||
@@ -491,7 +491,6 @@ final class RDEPUBChapterLoader {
|
||||
) -> [RDEPUBTextPage] {
|
||||
let totalPageCount = pageRanges.count
|
||||
return pageRanges.enumerated().map { (pageIndex, range) in
|
||||
let pageContent = typesetString.attributedSubstring(from: range)
|
||||
let metadata: RDEPUBTextPageMetadata
|
||||
if let metaList = metadataSource, pageIndex < metaList.count {
|
||||
|
||||
@@ -513,7 +512,6 @@ final class RDEPUBChapterLoader {
|
||||
pageIndexInChapter: pageIndex,
|
||||
totalPagesInChapter: totalPageCount,
|
||||
chapterContent: typesetString,
|
||||
content: pageContent,
|
||||
contentRange: range,
|
||||
pageStartOffset: range.location,
|
||||
pageEndOffset: range.location + range.length - 1,
|
||||
|
||||
+2
@@ -62,6 +62,7 @@ final class RDEPUBChapterRuntimeStore {
|
||||
|
||||
func insertChapter(_ chapter: RDEPUBRuntimeChapter) {
|
||||
chapterDataCache[chapter.spineIndex] = chapter
|
||||
RDEPUBMemoryProbe.log("chapterLoaded spine=\(chapter.spineIndex) pages=\(chapter.pages.count)")
|
||||
}
|
||||
|
||||
func insertPageCount(_ pc: RDEPUBRuntimePageCount, for key: RDEPUBChapterCacheKey) {
|
||||
@@ -188,6 +189,7 @@ final class RDEPUBChapterRuntimeStore {
|
||||
}
|
||||
|
||||
func invalidateAllForSettingsChange() {
|
||||
RDEPUBMemoryProbe.log("settingsChangeInvalidateAll")
|
||||
chapterDataCache.removeAll()
|
||||
pageCountCache.removeAll()
|
||||
imageCache.removeAllObjects()
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import Foundation
|
||||
|
||||
/// Memory footprint probe for the long-chapter optimization work
|
||||
/// (Doc/LONG_CHAPTER_MEMORY_OPTIMIZATION_PLAN.md, P0-2). Enabled with the
|
||||
/// `--demo-memory-probe` launch argument; logs phys_footprint at chapter
|
||||
/// load, every 20 page turns, settings invalidation, and rotation.
|
||||
enum RDEPUBMemoryProbe {
|
||||
|
||||
static let isEnabled = ProcessInfo.processInfo.arguments.contains("--demo-memory-probe")
|
||||
|
||||
private static let pageTurnLogStride = 20
|
||||
|
||||
/// Main-thread only (page turns are delivered on main).
|
||||
private static var pageTurnCount = 0
|
||||
|
||||
static func logPageTurn() {
|
||||
guard isEnabled else { return }
|
||||
pageTurnCount += 1
|
||||
guard pageTurnCount % pageTurnLogStride == 0 else { return }
|
||||
log("pageTurn count=\(pageTurnCount)")
|
||||
}
|
||||
|
||||
static func log(_ event: String) {
|
||||
guard isEnabled else { return }
|
||||
let megabytes = Double(currentFootprint()) / 1_048_576
|
||||
print(String(format: "[EPUB][MemoryProbe] %@ footprint=%.1fMB", event, megabytes))
|
||||
}
|
||||
|
||||
/// phys_footprint matches the value Xcode's memory gauge and Jetsam use.
|
||||
private static func currentFootprint() -> UInt64 {
|
||||
var info = task_vm_info_data_t()
|
||||
var count = mach_msg_type_number_t(
|
||||
MemoryLayout<task_vm_info_data_t>.size / MemoryLayout<integer_t>.size
|
||||
)
|
||||
let result = withUnsafeMutablePointer(to: &info) { pointer in
|
||||
pointer.withMemoryRebound(to: integer_t.self, capacity: Int(count)) {
|
||||
task_info(mach_task_self_, task_flavor_t(TASK_VM_INFO), $0, &count)
|
||||
}
|
||||
}
|
||||
guard result == KERN_SUCCESS else { return 0 }
|
||||
return info.phys_footprint
|
||||
}
|
||||
}
|
||||
@@ -322,6 +322,9 @@ final class RDEPUBTextContentView: UIView, UIGestureRecognizerDelegate, RDReader
|
||||
// layouter alive for the visible page. Avoid reader-level page
|
||||
// caching so preloading does not retain several full-chapter
|
||||
// display copies at once.
|
||||
// Do NOT relax this until chapter-level shared display content
|
||||
// lands (LONG_CHAPTER_MEMORY_OPTIMIZATION_PLAN.md P1-1): cached
|
||||
// pages would each pin their own full-chapter copy again.
|
||||
return currentPage == nil
|
||||
|| loadingSpinner.isAnimating
|
||||
|| coreTextDisplayContent != nil
|
||||
|
||||
Reference in New Issue
Block a user