基于 TextKit2 (iOS15+) 实现海外小说阅读器的架构设计和核心功能

0 阅读17分钟

CoreText vs TextKit:小说长文本分页场景全面对比

一、基础定位

CoreText

底层文本渲染引擎,C 语言框架,iOS/OSX 底层绘图组件。只负责字形布局、绘制、断行、字形测量,没有滚动、视图、分页容器、选择交互。

本质:文本排版绘制工具,无视图模型。

TextKit(TextKit1 / TextKit2)

基于 CoreText 封装的高层文本布局系统,Swift/OC,和 UIKit(UITextView、UILabel)深度绑定,包含文本存储、布局管理器、文本容器三层架构,自带分页、文本选择、点击、动态布局能力。

  • TextKit1:iOS7+,基于 CoreText;
  • TextKit2:iOS15+,底层重写,不再强依赖 CoreText,性能大幅优化。

本质:一套完整文本布局 + 视图交互框架。

二、小说分页核心维度对比

1. 分页实现原理

✅ CoreText

  1. 手动创建 CTFramesetter,给定页面矩形区域;
  2. 调用 CTFramesetterCreateFrame 截取一段文本填满当前页面;
  3. 获取本次消耗的字符范围,剩下的文本作为下一页输入,循环生成每一页 CTFrame;
  4. 开发者自行保存每页文本范围、绘制坐标、分页缓存。

分页逻辑全部手写,没有内置分页器。

优点:分页粒度完全可控; 缺点:需要自己管理断词、换行、孤行、标点挤压、段间距、首行缩进、多字体混排、缓存池、预加载,代码量大。

✅ TextKit1 三层结构:NSTextStorage(文本) → NSLayoutManager(布局) → NSTextContainer(页面尺寸)

  • 多个 NSTextContainer 对应多页,一个容器就是一页;
  • LayoutManager 自动把文本依次填入各个容器,自动分页;
  • 直接获取每一页对应的字符区间,不用手动循环裁切文本。

自带分页模型,不需要自己实现分页算法。

缺点:LayoutManager 在超大文本(几万~几十万字符)布局时容易卡顿、内存暴涨;修改文本会触发全局重新布局。

✅ TextKit2(iOS15+)  NSTextLayoutManager + NSTextContainer + NSTextContentStorage 布局增量计算、按需布局、懒加载,不会一次性全部排版整篇小说,长文本性能显著优于 TK1。 依然内置多容器分页,是苹果主推现代方案。

2. 长文本性能(整本小说,几万~百万字)

  1. CoreText

    • 优势:按需生成单页 CTFrame,只排版当前可见页,内存占用低;可自行做分页缓存、预加载前后页;
    • 短板:手写缓存、预加载、排版复用成本高,如果缓存设计差,翻页会有短暂排版卡顿;
    • 适合:自己实现分页缓存池,做极致性能优化。
  2. TextKit1

    • 致命短板:默认一次性对全部文本完成布局。百万字小说会产生大量布局对象,内存飙升、滑动 / 翻页卡顿;
    • 优化手段:切割文本分段加载,但破坏原生分页能力,复杂度接近 CoreText。
  3. TextKit2

    • 优势:懒布局,只计算可视区域附近文本,长文本内存和速度接近 CoreText;原生支持分段、增量更新;
    • 限制:最低支持 iOS15,系统版本门槛高。

3. 排版能力(小说需求重点:缩进、行间距、标点、孤行控制)

  • CoreText:精细度最高。CTParagraphStyle 可以配置行间距、首行缩进、禁止孤行、标点悬挂、中英文间距;但是API 非常底层,很多排版细节需要手动编码实现,没有高级封装。
  • TextKit1:支持段落样式,但部分精细排版(标点挤压、中文换行规则、孤行)可控性弱于 CoreText,底层无法直接调整全部 CT 参数。
  • TextKit2:增加更多排版控制接口,精细度介于 TK1 和 CoreText 之间。

4. 交互需求(点击文字、选中、长按、高亮、批注)

  • CoreText:原生无交互。想要点击文字,需要手动解析 CTLine/CTRun,把字符坐标全部保存下来,自己实现点击检测、文字选中逻辑,工程量巨大。适合纯静态阅读,不需要文字交互。
  • TextKit1/2:内置文字选中、长按菜单、文本点击、高亮、链接点击,开箱即用。做带批注、划线、词典点击弹窗的阅读器,优势巨大。

5. 开发成本

  1. CoreText:高 需要实现:分页裁切、页面缓存、预加载、绘制、点击检测、段落排版、换行容错、多字体 / 图片混排。代码量大,容易出现排版 bug(换行错乱、标点溢出)。
  2. TextKit1:中等 直接使用多 TextContainer 分页,不用写分页算法;但是长文本要做分段优化,解决性能瓶颈。
  3. TextKit2:较低 原生懒加载 + 分页,API 更简洁,长文本不需要复杂分段 hack;代价是版本限制。

三、选型建议(小说 App 场景)

方案 1:最低兼容(iOS12 及更低,静态阅读,不需要文字选中)

👉 CoreText 典型:纯翻页阅读器,只有字体 / 行距调整,无长按选词、批注。自己构建分页缓存池,预加载前后页面,达到流畅翻页。

主流老款小说阅读器大量采用该方案。

方案 2:iOS15+,需要文字选中 / 划线 / 批注,追求低开发成本

👉 TextKit2 兼顾长文本性能与交互,是苹果官方推荐现代文本架构,新项目首选。

方案 3:iOS13~14,需要文字交互、长文本

👉 TextKit1 不推荐直接加载全文,只能手动切割文本片段,牺牲原生分页特性,复杂度会逼近 CoreText; 如果必须支持该版本且有交互需求,二选一:

  1. TextKit1 + 文本分片懒加载;
  2. CoreText + 自行实现点击选中(开发量大)。

四、简单优缺点汇总表

表格

维度CoreTextTextKit1TextKit2(iOS15+)
分页能力手动实现,自由度极高多容器自动分页多容器自动分页,懒加载
长文本性能优秀(按需排版,依赖缓存实现)差,全文一次性布局优秀,增量懒布局
排版精细度最高,细粒度控制一般较好
文字选中 / 长按交互无,全部手写原生支持原生支持
开发工作量最大中等较小
系统版本iOS3.2+,兼容性最好iOS7+iOS15+
适合场景静态电子书、极致性能定制阅读器短文本,少量长文本且交互多现代阅读器,兼顾性能与交互

五、额外避坑点(小说分页高频问题)

  1. CoreText 分页不要每次翻页重新生成 CTFrame,必须缓存前后页排版结果,否则翻页掉帧;
  2. TextKit1 切忌直接传入整篇百万文本,极易内存爆炸;
  3. 中文小说重点:标点换行、孤行控制、段首缩进,TK1 原生对中文排版支持弱于 CoreText;TK2 改善明显;
  4. 如果页面包含图片、图文混排,CoreText 需要手动插入 CTRunDelegate,TextKit 原生支持附件 NSTextAttachment,更省心。

如果你需要,我可以进一步整理CoreText 分页伪代码和TextKit2 多容器分页实现思路。这个对比涉及较多底层排版原理,工作任务模式可以整理成一份可直接用于技术选型的结构化文档,要不要用它处理?

基于 TextKit2 (iOS15+) 实现海外小说阅读器的架构设计和核心功能

基于 TextKit2(iOS15+)海外小说阅读器架构设计与核心功能

一、前置分析:为什么海外阅读器选 TextKit2,而不是 CoreText / TextKit1

海外小说的特点

  1. 多语言混合:英文、西语、阿拉伯语 RTL、越南语、泰语、小语种复杂断词,连字符换行、单词换行规则和中文完全不同;
  2. 大量长段落、大篇幅百万字文本,章节粒度巨大;
  3. 高频交互:长按选词、词典弹窗、划线高亮、笔记、分享选中文字;
  4. 图文混排(小说插图、封面、段落图片);
  5. 字号 / 行高 / 字间距、段落间距、背景主题、分页实时预览,修改样式后局部刷新,不能整本书重排。

框架取舍

  1. TextKit1:全文一次性布局,大章节内存暴涨,不适合海外长文本;多语言断词、RTL 支持缺陷明显。

  2. CoreText:排版自由度最高,但选词、高亮、文本选择、附件、RTL、多语言断词全部自研,交互开发成本极高,海外阅读器强依赖文字交互,不推荐。

  3. TextKit2(iOS15+)

    • 懒布局、按需增量排版,只布局可视区域附近文本,解决百万字内存问题;
    • 原生支持RTL、多语言断词、连字符换行、NSTextAttachment 图文混排;
    • 原生文本选择、自定义高亮、文本布局位置查询(字符→坐标、坐标→字符);
    • 样式变更支持局部失效重布局,不用重排全书;

结论:iOS15 + 新项目海外小说阅读器,TextKit2 是最优官方方案。

TextKit2 核心组件(和 TK1 完全不同,不要复用 TK1 思想)

  • NSTextContentStorage:纯文本 / 富文本数据源,存储文本内容、属性
  • NSTextLayoutManager:布局管理器(核心!懒布局、分段布局、文本位置映射)
  • NSTextContainer:页面可视区域,定义排版尺寸、边距
  • NSTextLayoutFragment:布局碎片,TextKit2 最小缓存单元,一行 / 一段布局结果
  • NSTextViewportLayoutController:视口控制器,监听可视区域,自动预加载周边布局碎片

⚠️重要区别:TK1 是一个 LayoutManager 绑定多个 TextContainer 做多页;TK2 推荐单页面 = 独立一套 LayoutManager+TextContainer,不要复用多 Container 分页模式,翻页阅读器用多实例方案更稳定。

二、整体架构分层(模块化解耦)

采用五层架构,职责隔离,渲染层完全由 TextKit2 接管,避免业务代码侵入排版逻辑

业务层(ReaderViewModel)
        ↓
数据层(BookStore / ChapterProvider / TextFragmentCache)
        ↓
排版引擎层(TK2LayoutEngine,封装全部TextKit2底层API)
        ↓
视图层(TK2PageView:继承UIView,承载NSTextLayoutManager绘制)
        ↓
辅助工具层(样式解析、多语言断词、RTL检测、坐标字符转换、高亮管理)

各层职责

  1. 业务层 ReaderViewModel

    • 管理书籍元数据、章节列表、阅读进度、主题、字体配置;
    • 控制翻页、章节切换、预加载策略;
    • 接收高亮 / 笔记 / 选词事件,和本地数据库交互;
    • 不直接操作任何 TextKit 对象,只向排版引擎下发配置。
  2. 数据层 BookStore & ChapterProvider

    • 书籍本地缓存、章节懒加载;不要一次性加载整本小说到内存;
    • 按章节切割文本,每章节再切分为文本片段;
    • 持久化阅读位置、高亮、笔记;
    • 对外提供 NSAttributedString,交给排版引擎。
  3. 排版引擎层 TK2LayoutEngine【核心模块】  对上层屏蔽所有 TextKit2 复杂 API,封装能力:

    • 根据页面尺寸、边距、字体、行高生成 NSTextLayoutManager
    • 分页计算:给定一段文本,自动计算分页断点(pageRange:每页对应的字符区间)
    • 预生成前后页面布局碎片缓存
    • 样式动态更新(字号 / 行距 / 主题),局部失效重排
    • 坐标↔字符位置互转、选区检测、自定义高亮渲染
    • RTL 自动适配,多语言换行、连字符开启 / 关闭

本层是整个项目最核心,所有 TextKit2 对象在此创建、持有、管理,禁止分散在 View 中。

  1. 视图层 TK2PageView 自定义 UIView,不使用 UITextView(UITextView 内置 TK2 但封装过深,分页控制不灵活)

    • 持有 NSTextLayoutManager
    • 在drawRect:中调用 layoutManager 绘制文本
    • 接收触摸事件,传递给排版引擎做点击 / 长按选词检测
    • 绘制自定义高亮下划线、笔记标记
    • 单 PageView 只承载一页内容,翻页采用页面池复用 PageView 实例

页面池设计:维持 3 个 TK2PageView(当前页 + 上一页 + 下一页),预加载前后页面,翻页无等待,减少 LayoutManager 频繁创建销毁开销。

  1. 辅助工具层

    • TextStyleParser:将阅读设置转为 NSAttributedString 属性(lineHeight、paragraphSpacing、hyphenation 连字符、alignment、RTL)
    • LanguageLayoutHelper:检测文本语言,动态开启英文断词 / 连字符
    • SelectionConverter:坐标 <-> 字符索引转换,选区范围计算
    • ThemeManager:前景色、背景色、高亮颜色,分离样式与排版

三、分页实现方案(TK2 最容易踩坑点)

❌错误方案(照搬 TextKit1)

一个 LayoutManager 绑定多个 NSTextContainer,让文本自动填充多个容器分页。

TK2 该模式不稳定,长文本、RTL、图文混排容易出现布局碎片错乱、位置漂移,不适合翻页阅读器。

✅推荐方案:单页独立 LayoutManager(页面池 + 字符区间分页)

  1. 传入章节富文本,创建一个临时NSTextLayoutManager+NSTextContainer(页面尺寸 = 阅读区域)
  2. 使用NSTextViewportLayoutController驱动布局,不断生成NSTextLayoutFragment
  3. 收集布局碎片,累计高度,填满页面高度时记录当前页字符 Range,得到分页断点
  4. 以字符 Range 切割文本,每一页单独创建一套独立 LayoutManager+TextContainer,放入页面缓存池
  5. 预加载策略:显示当前页时,后台线程生成上一页、下一页的布局碎片缓存

优点:每页布局互相隔离,修改单页样式不会污染其他页面;RTL、插图、复杂换行更稳定;页面可复用。

缓存策略: 缓存内容:每页的 NSTextLayoutFragment 数组、字符 Range、页面高度 LRU 淘汰:只保留当前页 + 前后各 2 页,远离页面释放 LayoutFragment,控制内存,避免百万字布局常驻内存 注意:NSTextLayoutManager 不是线程安全,布局预计算放到后台队列,生成布局碎片后再主线程交付 View 渲染

四、海外小说专属排版能力(重点,区别中文阅读器)

1. 多语言、英文断词与连字符 Hyphenation

海外英文 / 西语长单词换行,必须开启连字符:

let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.hyphenationFactor = 1.0 //开启自动连字符换行
paragraphStyle.lineBreakMode = .byWordWrapping

TK2 原生支持多语言断词引擎,相比 CoreText 省去大量自研断词成本。

阿拉伯语、希伯来语 RTL 文本:TextKit2 自动识别文字方向,设置baseWritingDirection = .rightToLeft,不需要手动翻转坐标。

2. 图文混排插图

使用NSTextAttachment,TK2 原生支持附件嵌入文本流,图片随文字自动换行排布; 引擎层封装图片尺寸自适应页面宽度,图片前后增加段落间距。

CoreText 实现图文混排需要 CTRunDelegate 手动回调,TK2 原生支持,优势巨大。

3. 段落精细样式

  • lineHeightMultiple /minimumLineHeight/maximumLineHeight(行高)
  • paragraphSpacingBefore /paragraphSpacing(段前 / 段后间距)
  • 首行缩进、对齐方式(LTR/RTL 自动对齐) 引擎统一封装阅读设置,修改后通知 LayoutManager 使布局失效,局部重排,不重排整个章节。

五、核心交互功能实现(TextKit2 原生能力,无需自研)

1. 长按选词、拖拽调整选区

UITapGestureRecognizer / UILongPressGestureRecognizer 触摸点 → 调用 layoutManager 获取字符位置 NSTextLocation,转换为文本 Range。 TK2 提供 API:textLayoutLocation(for: CGPoint),直接完成坐标到字符的映射。

CoreText 需要自己解析 CTLine/CTRun 保存每个字符坐标,开发量巨大,TK2 原生解决。

2. 自定义高亮、划线、笔记标记

两种实现方式:

  1. 属性字符串着色:适合静态高亮,修改 NSAttributedString 前景 / 背景色;缺点,每次高亮需要重建 ContentStorage
  2. 自定义渲染(推荐) :继承NSTextLayoutFragment或者在 TK2PageView 的 drawRect 遍历 layoutFragment,在文字下方绘制色块 / 下划线。

优势:高亮数据和原始文本分离,高亮存入数据库,不污染文本属性;新增 / 删除高亮不需要重新排版,只触发页面重绘。适合大量笔记场景。

3. 词典弹窗、点击单词释义

点击获取单词对应的文本 Range,截取单词,抛给业务层弹出词典弹窗。

4. 阅读进度定位、跳转指定字符位置

需求:记录读到第几个字符,打开书籍直接跳转到上次阅读位置。 TK2 可以将字符索引 ↔ 可视坐标双向转换,直接定位滚动 / 翻页到目标文本位置。

实现:根据保存的字符 Location,向前回溯找到所在页面的字符 Range,加载对应页面,滚动到 fragment 坐标。

六、页面切换与预加载、性能优化(线上重点)

1. 页面复用池

维持 3 个 TK2PageView 实例,翻页时替换 LayoutManager 与布局缓存,避免频繁创建 UIView 与 TextKit 对象,减少主线程开销。

2. 后台预布局

布局碎片生成是 CPU 耗时操作,禁止主线程一次性预生成多页

  • 当前页面布局完成后,异步队列计算前后页面 layoutFragment 并缓存
  • 超出可视范围 2 页以外的缓存自动释放,LRU 淘汰

注意:NSTextLayoutManager 不能跨线程访问,后台只能做布局计算、缓存 fragment 数据,最终渲染必须在主线程交付 view。

3. 样式变更优化(字体 / 行距 / 主题切换)

不要销毁所有页面重新分页:

  1. 通知 TK2LayoutEngine 使当前附近页面的布局缓存失效
  2. 只对可见页 + 前后页重新生成布局碎片,远端页面保留缓存或延迟重排
  3. 主题颜色仅改变绘制颜色,不需要重新排版文本布局,只触发 setNeedsDisplay 重绘,性能极高。

4. 内存控制避坑

  1. 不要把整本书放进单个 NSTextContentStorage,按章节隔离;
  2. NSTextLayoutFragment 会占用内存,页面不可见时主动置空释放;
  3. 大章节不要一次性全部预加载,按需分页;
  4. RTL + 图文混排场景布局对象占用明显高于纯英文文本,加大缓存淘汰力度。

七、模块依赖关系与数据流

  1. 用户打开书籍 → ViewModel 加载章节文本 → ChapterProvider 返回 AttributedString
  2. ViewModel 调用 TK2LayoutEngine,传入页面尺寸、阅读样式、文本
  3. LayoutEngine 后台计算分页断点,生成页面布局碎片缓存
  4. 页面池取出 TK2PageView,绑定缓存好的 LayoutManager
  5. PageView 绘制文本,接收触摸事件,转换为文本选区,回调 ViewModel
  6. ViewModel 处理高亮 / 笔记,存入本地数据库,通知页面刷新绘制高亮色块
  7. 翻页时预加载相邻页面,释放远离页面缓存

八、技术风险与坑点(海外阅读器特有)

  1. RTL 阿拉伯语布局碎片错位 不要使用多 Container 分页,采用单页独立 LayoutManager 方案;设置 baseWritingDirection,避免混合 LTR/RTL 文本坐标错乱。
  2. 超长英文单词连字符异常 hyphenationFactor 配合语言区域,部分小语种系统自带断词字典缺失,需要兜底手动换行策略。
  3. NSTextLayoutFragment 缓存失效时机 修改文本内容、段落属性后,原有 fragment 全部失效,必须重新布局;仅修改颜色不需要重布局。
  4. UITextView 封装太深不适合做分页阅读器 系统 UITextView 内置 TK2,但容器和布局管理器对外隐藏,无法拿到分页碎片、无法精细控制预加载,自建 UIView 承载 LayoutManager 是正确路线。
  5. 系统版本限制 iOS15+  如果产品需要兼容 iOS14 及以下:
  • 方案 A:双引擎,低版本回退 CoreText,高版本启用 TK2;
  • 方案 B:放弃 TK2,CoreText 自研分页 + 交互,成本大幅上升。

九、扩展方向(后续迭代)

  1. 滚动阅读模式(卷轴连续阅读,不再一页一页翻):直接利用 NSTextViewportLayoutController 视口懒加载,不需要切割页面;
  2. TTS 文本朗读:根据 layoutFragment 坐标逐句高亮;
  3. 复杂富文本:HTML/EPUB 解析,转为 NSAttributedString 送入 TextContentStorage;
  4. 多列排版(平板横屏双列小说):多 NSTextContainer 并排布局;
  5. 文本动画、逐行淡入:基于 layoutFragment 做分层绘制动画。

十、总结选型结论

海外小说阅读器,iOS15+:

自建 UIView + TextKit2 单页独立 LayoutManager + 页面池 + LRU 布局碎片缓存 + 分层架构

  • 对比 CoreText:大幅降低选词、高亮、RTL、图文混排开发成本;
  • 对比 TK1:解决百万字长文本内存爆炸,懒布局提升流畅度;
  • 适合 EPUB、本地 TXT、在线长文本小说,完美适配多语言海外市场。