
简介这份资源面向鸿蒙开发学习者与阅读类应用开发者聚焦鸿蒙版阅读仓库的完整工程实现可用于研究鸿蒙OS下阅读APP的页面结构、组件拆分与资源组织方式。压缩包共886个文件约5.58MB以ets组件代码为主配合svg、png、jpg等图形资源以及js、ts、vue、json、json5等配置与逻辑文件另有css样式、ttf与woff字体、html页面及少量md、txt说明文档基本覆盖一个阅读类应用从界面到配置的完整素材。资源描述中提及阅读3.0提供Web与Content Provider两种API调用方式并支持通过legado://import/{path}?src{url}格式的URL一键导入书源、订阅源、替换规则、本地txt目录规则、在线朗读引擎、主题、阅读排版及添加到书架等内容便于开发者按需对接与扩展。目前已有230人学习下载适合希望借鉴鸿蒙端阅读应用目录结构与组件组织、并了解导入机制的中高级开发者参考。1. 人工智能-鸿蒙开发-阅读鸿蒙版仓库从零跑通一个端侧AI阅读应用你可能已经在鸿蒙设备上装过不少阅读类应用但有没有想过自己动手做一个带端侧AI能力的鸿蒙版阅读器这个标题拆开来看核心是三件事鸿蒙开发ArkTS/ArkUI 技术栈、阅读器场景电子书解析与渲染、人工智能端侧推理做摘要、翻译或问答。三者叠在一起就是一个能在鸿蒙手机或平板上离线运行的智能阅读工具。我最初接触这个方向是因为一个很朴素的需求在鸿蒙平板上看技术文档时想直接划词让本地模型解释一段话而不是把内容传到云端。端侧推理的好处是隐私可控、无网络也能用代价是模型要小、内存要省、推理要快。这篇文章就围绕这个目标把仓库结构、环境搭建、核心模块、参数调优和踩坑记录一次讲清楚。适合有 ArkTS 基础、想往端侧 AI 方向走的鸿蒙开发者也适合做阅读类应用想加 AI 能力的同学。2. 鸿蒙版阅读仓库的工程结构与技术选型2.1 为什么选 ArkTS 端侧推理框架而不是纯 Web 方案鸿蒙应用开发目前主流是 ArkTS 语言配合 ArkUI 声明式框架。相比把阅读器做成一个 WebView 套壳原生方案在长列表滚动、手势响应、文件访问权限上更可控。阅读器最核心的体验是翻页流畅和排版准确WebView 在复杂 EPUB 排版下容易出现滚动卡顿和字体渲染不一致而 ArkUI 的List组件配合LazyForEach可以做到按需渲染几千章的网文也不会一次性吃掉内存。端侧推理这块常见做法是使用鸿蒙的 AI 推理能力把量化后的小模型比如 1B 到 3B 参数级别的文本模型或者更小的专用摘要模型放到设备上跑。选型时要权衡三件事模型体积、推理延迟、内存占用。我的经验是阅读场景对延迟的容忍度比对话场景高用户划词后等一两秒出结果是可以接受的所以可以优先保模型质量而不是一味追求极致速度。2.2 仓库目录拆解与各模块职责一个典型的鸿蒙阅读仓库目录结构大致如下。这里用模拟项目X 的代称来描述不指向任何真实仓库。entry/src/main/ets/ ├── pages/ │ ├── Index.ets // 书架首页 │ ├── Reader.ets // 阅读页核心 │ └── Settings.ets // 设置页 ├── components/ │ ├── BookShelf.ets // 书架网格组件 │ ├── PageView.ets // 翻页容器 │ └── AiPanel.ets // AI 划词面板 ├── model/ │ ├── BookParser.ets // EPUB/TXT 解析 │ ├── AiEngine.ets // 推理引擎封装 │ └── BookRepository.ets // 数据持久化 └── utils/ ├── FileUtil.ets // 文件读写 └── TextUtil.ets // 文本分章、分句BookParser负责把 EPUB 的 zip 结构解开提取 OPF 清单和 XHTML 内容再转成应用内部的章节模型。AiEngine是端侧推理的封装层对外暴露summarize(text)、translate(text)、ask(question, context)这类方法内部管理模型加载、输入分词、推理调用和结果解码。BookRepository用鸿蒙的关系型数据库或首选项做阅读进度、书签、笔记的持久化。2.3 环境搭建与最小可运行工程先把开发环境跑起来。需要 DevEco Studio 和对应版本的 SDK具体版本以你本地 IDE 的提示为准不要盲目照搬别人的版本号。# 检查 hvigor 版本确认构建工具链可用 hvigorw --version # 清理并重新构建 entry 模块 hvigorw clean --mode module -p moduleentrydefault hvigorw assembleHap --mode module -p moduleentrydefault构建产物在entry/build/default/outputs/default/下是一个.hap文件。用hdc工具安装到设备# 查看已连接设备 hdc list targets # 安装 hap 包 hdc install entry/build/default/outputs/default/entry-default-signed.hap参数说明--mode module表示按模块构建-p moduleentrydefault指定目标模块。如果构建报错找不到 SDK检查local.properties里的 SDK 路径是否指向你实际安装的位置。第一次构建会比较慢因为要下载依赖和编译原生库。3. 阅读器核心文本解析、分页与渲染3.1 EPUB 解析的四个关键步骤EPUB 本质是一个 zip 包解析流程分四步。第一步解压用鸿蒙的zlib能力读取META-INF/container.xml找到 OPF 文件路径。第二步解析 OPF拿到 manifest所有资源清单和 spine阅读顺序。第三步按 spine 顺序读取 XHTML用正则或轻量解析器提取纯文本和基础样式。第四步按章节切分生成应用内部的章节数组。// BookParser.ets 核心解析逻辑简化示意 import zlib from ohos.zlib; async function parseEpub(filePath: string): PromiseChapter[] { // 1. 解压到临时目录 const tempDir getContext().cacheDir /epub_temp; await zlib.decompressFile(filePath, tempDir); // 2. 读取 container.xml 定位 OPF const containerXml await readTextFile(tempDir /META-INF/container.xml); const opfPath extractOpfPath(containerXml); // 3. 解析 OPF 获取 spine 顺序 const opfContent await readTextFile(tempDir / opfPath); const spineItems parseSpine(opfContent); // 4. 逐章提取文本 const chapters: Chapter[] []; for (const item of spineItems) { const html await readTextFile(tempDir / item.href); chapters.push({ title: extractTitle(html), content: stripHtmlTags(html) }); } return chapters; }逻辑说明decompressFile是异步的大文件要加进度回调。extractOpfPath用正则匹配full-path属性。stripHtmlTags要保留段落换行否则排版会糊成一团。参数上临时目录建议放在cacheDir而不是filesDir系统在空间紧张时可以自动清理缓存。3.2 分页算法与 LazyForEach 渲染优化分页是阅读器最容易翻车的地方。核心思路是给定屏幕宽高、字体大小、行高、边距计算每页能容纳多少字符。中文和英文的字符宽度不同简单按字符数切分会导致英文单词被截断。我的做法是按段落先切再在段落内按测量宽度切。// 按测量宽度分页 function paginate(text: string, pageWidth: number, paint: drawing.TextBlob): Page[] { const pages: Page[] []; let currentPage ; const paragraphs text.split(\n); for (const para of paragraphs) { const paraWidth paint.measureText(para); if (paraWidth pageWidth) { if (paint.measureText(currentPage para) pageWidth) { pages.push({ content: currentPage }); currentPage para \n; } else { currentPage para \n; } } else { // 长段落按字符逐个累加测量 for (const char of para) { if (paint.measureText(currentPage char) pageWidth) { pages.push({ content: currentPage }); currentPage char; } else { currentPage char; } } currentPage \n; } } if (currentPage) pages.push({ content: currentPage }); return pages; }参数说明pageWidth是屏幕宽度减去左右边距paint是 ArkUI 的文本测量对象需要提前设置好字体和字号。这个算法的时间复杂度是 O(n)对单章几万字的文本可以在几十毫秒内完成。渲染时用LazyForEach只创建可见页的组件配合cachedCount预加载前后各一页翻页就不会有白屏。3.3 阅读进度持久化与恢复进度保存要记录三个值书籍 ID、章节索引、章节内偏移量。用鸿蒙的relationalStore建一张阅读进度表。CREATE TABLE IF NOT EXISTS reading_progress ( book_id TEXT PRIMARY KEY, chapter_index INTEGER NOT NULL, char_offset INTEGER NOT NULL, updated_at INTEGER NOT NULL );每次翻页时更新但不要每翻一页就写库那样 IO 太频繁。我的做法是节流翻页后延迟 2 秒写入如果 2 秒内又翻页就重置计时器。应用切到后台时强制写一次。恢复时按book_id查出记录定位到对应章节和偏移量重新分页后跳到目标页。4. 端侧 AI 能力接入划词摘要与问答4.1 模型选型与量化格式的取舍端侧跑文本模型绕不开量化和格式转换。常见做法是把训练好的模型转成 ONNX 或特定推理框架支持的格式再做 INT8 或 INT4 量化。INT8 量化后模型体积约为 FP32 的四分之一精度损失通常在可接受范围内INT4 体积更小但生成质量下降明显摘要任务可能出现语句不通顺。选型时看三个指标模型参数量、量化后体积、首 token 延迟。阅读场景建议控制在 2B 参数以内量化后 1GB 到 2GB首 token 延迟在 500ms 以内。如果设备内存紧张可以考虑更小的专用模型比如只做抽取式摘要的模型体积可以压到几百 MB。4.2 AiEngine 封装加载、推理、释放推理引擎的封装要处理好生命周期。模型加载是重操作不能每次划词都加载一遍。我的做法是在阅读页aboutToAppear时异步加载加载完成前 AI 按钮置灰。// AiEngine.ets 推理封装简化示意 class AiEngine { private session: InferenceSession | null null; private isLoading false; async loadModel(modelPath: string): Promisevoid { if (this.session || this.isLoading) return; this.isLoading true; try { // 从沙箱路径加载模型文件 this.session await InferenceSession.create(modelPath, { intraOpNumThreads: 2, // 控制线程数避免抢占 UI 线程 optimizationLevel: BASIC }); } finally { this.isLoading false; } } async summarize(text: string): Promisestring { if (!this.session) throw new Error(model not loaded); // 截断过长输入防止超出模型上下文 const input text.slice(0, 1024); const tokens this.tokenize(input); const output await this.session.run({ input_ids: tokens }); return this.detokenize(output); } release(): void { this.session?.release(); this.session null; } }参数说明intraOpNumThreads设为 2 是为了在推理速度和功耗之间取平衡设太高会导致设备发热降频。optimizationLevel用BASIC即可EXTENDED会增加加载时间。summarize里截断输入是必要的大多数小模型的上下文窗口在 512 到 2048 token 之间超长输入会直接报错。4.3 划词交互与结果面板划词交互用 ArkUI 的Text组件配合onTextSelectionChange回调。用户长按选中文本后弹出一个自定义菜单包含“摘要”“翻译”“解释”三个选项。点击后把选中文本传给AiEngine结果展示在底部面板里。// AiPanel.ets 划词面板核心逻辑 State result: string ; State loading: boolean false; async function onSummarizeClick(selectedText: string) { this.loading true; try { this.result await aiEngine.summarize(selectedText); } catch (e) { this.result 推理失败请重试; } finally { this.loading false; } }面板用bindSheet从底部弹出高度设为屏幕的 40%。加载中显示骨架屏或转圈不要留白。结果文本要支持滚动和复制方便用户把摘要粘贴到笔记里。5. 避坑与常见问题排查5.1 模型加载失败路径与权限问题现象调用loadModel时抛出文件不存在或权限拒绝。原因通常是模型文件放在了resources/rawfile下但推理框架需要的是沙箱内的绝对路径。解决首次启动时把 rawfile 里的模型拷贝到filesDir下再用拷贝后的路径加载。拷贝操作要放在TaskPool里避免阻塞主线程。5.2 推理时 UI 卡顿线程调度没做对现象划词后界面冻结一两秒动画掉帧。原因是推理调用跑在了主线程。解决把session.run放到TaskPool的并发任务里通过Concurrent装饰器标记并发函数。注意并发函数不能直接访问 UI 状态结果要通过消息或 Promise 回传。5.3 分页错乱字体测量与实际渲染不一致现象分页时算好的每页内容渲染出来最后一行被截断或溢出。原因是测量用的paint对象和实际渲染的Text组件字体参数不一致。解决把字体大小、行高、字重抽成统一的常量配置测量和渲染都引用同一份配置。另外注意系统字体缩放设置用户调大字体后要重新分页。5.4 内存溢出长列表和模型同时占内存现象阅读长篇小说时应用闪退日志显示 OOM。原因是章节列表全量加载加上模型常驻内存。解决章节内容按需加载只保留当前章和前后各一章模型在退出阅读页时调用release释放图片资源用PixelMap按需解码不要一次性全部加载。5.5 进度丢失节流写入没做兜底现象用户反馈阅读进度偶尔回到很久之前。原因是节流写入在应用被系统杀掉时没来得及执行。解决除了定时写入还要在onBackground和onPageHide生命周期里强制写一次。另外写入前先读一次数据库避免并发写覆盖。6. 进阶技巧让端侧 AI 阅读体验更稳的几个习惯第一个习惯是给推理加超时。端侧设备性能参差不齐某些低端机上推理可能超过 5 秒。我会在AiEngine里包一层Promise.race超过 8 秒就返回“当前设备推理较慢建议缩短选中文本”。这样用户不会一直等体验上有个兜底。function withTimeoutT(promise: PromiseT, ms: number): PromiseT { return Promise.race([ promise, new PromiseT((_, reject) setTimeout(() reject(new Error(timeout)), ms) ) ]); }第二个习惯是缓存推理结果。同一段文本的摘要用户可能反复查看。用Map做内存缓存key 用文本的哈希值value 存结果。缓存上限设 50 条超出后按 LRU 淘汰。这样重复划词几乎零延迟。第三个习惯是模型预热。应用启动后在后台跑一次空推理让推理框架完成图优化和内存分配。这样用户第一次划词时不会因为初始化而多等一两秒。预热放在TaskPool里不干扰启动速度。第四个习惯是降级策略。如果模型加载失败或推理连续报错三次自动切换到“轻量模式”只提供基于规则的摘要比如取每段首句并提示用户“AI 能力暂不可用”。这比直接报错让用户一脸茫然要好。最后一个习惯是日志分级。端侧调试不方便我会把推理耗时、内存占用、失败原因打到hilog里用不同的 tag 区分。出问题时用hdc hilog抓日志比盲猜快得多。这些习惯都是踩坑踩出来的希望帮到你。本文还有配套的精品资源点击获取