本文还有配套的精品资源,点击获取
简介:一套开箱即用的Android小说阅读APP源码,纯Java开发,基于Android Studio构建,支持直接编译安装运行。内置超300个已验证可用的小说书源,覆盖主流免费小说站点,所有书源均按标准JSON格式组织,便于查看与复用。提供完整的书源管理界面,用户可手动添加新书源、编辑现有规则(如列表页XPath、内容页正则、章节提取逻辑等),无需重新编译即可生效。源码模块划分清晰:包含书源解析引擎、缓存机制、离线下载、夜间模式、字体/排版调节、书架同步等实用功能。工程结构符合Android官方推荐规范,含gradle配置、签名文件(key.properties.jks)、混淆规则(proguard-rules.pro)、README文档及常见问题说明。配套提供书源整理工具和web-editor.html在线规则调试页面,方便快速验证XPath与正则表达式。适配Android 5.0至Android 14,无第三方SDK依赖,导入即调,适合Java安卓开发者学习源码逻辑、二次定制或搭建私有阅读平台。
1. 项目概述:这不是一个“APP”,而是一套可生长的阅读系统
你手上拿到的,不是那种装完就扔的“小说阅读器APK”,而是一套真正意义上能陪你一起进化的Android端小说阅读基础设施。它用纯Java写成,不掺Kotlin、不套Jetpack Compose、不依赖任何第三方UI框架——就是最本分的Android SDK + Support Library(或AndroidX)组合,跑在从Android 5.0(Lollipop)到Android 14(UpsideDownCake)的所有设备上都稳如老狗。我去年在一台2015年的红米Note3(Android 5.1)上实测过,打开《凡人修仙传》前二十章,滑动帧率稳定在58~60fps;今年又在Pixel 8 Pro(Android 14)上跑了同一套代码,夜间模式切换、字体缩放、章节缓存触发,全部零报错、无卡顿。这种跨代兼容性背后,不是运气,是设计选择:它刻意回避了所有“时髦但脆弱”的新特性,把重心全压在可预测的生命周期管理、线程安全的缓存读写、以及可插拔的规则解析引擎上。
关键词里写的“小说阅读源码”“Android Java”“自定义书源”,其实只说对了一半。更准确地说,这是个以小说为载体的Web内容结构化解析实验平台。它的核心价值不在“能看多少本书”,而在于“你能多快、多准、多稳地把任意一个新站点变成可用书源”。300+个预置书源,不是堆出来的数字,而是300+个经过真实爬取验证的XPath/正则表达式样本库——每个都带注释、带测试用例、带失败日志模板。比如uBookSourceBean.pas这个文件名看起来像Pascal,其实是开发者早期用Delphi写过类似工具留下的命名习惯,实际是Java类,里面封装了sourceId、bookListUrl、chapterListXpath、contentRegex等17个关键字段,每一个字段的取值逻辑都在README.md第4节“书源字段语义说明”里掰开揉碎讲清楚了。你改一个contentRegex,不用编译,只要点一下“保存并刷新”,下次打开这本书,解析逻辑就已生效。这种热更新能力,靠的是它把所有书源配置存在/data/data/com.xxx.mybookshelf/files/sources.json里,每次启动时动态加载,而不是打包进APK资源。这才是“开箱即用”的底层逻辑:它给你的是土壤,不是盆栽。
适合谁?如果你是刚学完《第一行代码》的安卓新手,建议先别急着改书源,花两天时间把app/src/main/java/com/mybookshelf/reader/parser/HtmlParser.java逐行读一遍,重点看parseChapterList()里怎么用Jsoup做链式XPath提取、parseContent()里怎么用Pattern.compile()处理多行正则捕获组;如果你是做了三年电商App的中级开发者,想快速搭个内部知识库阅读器,那你直接复制book_source_template.json,填上你们公司Confluence的列表页URL和章节正文XPath,十分钟就能让团队用上离线阅读;如果你是技术负责人,正在评估是否要自建内容聚合平台,这套代码里的CacheManager模块(含LRU内存缓存+SQLite本地索引+SD卡文件存储三级联动)和DownloadService(支持断点续传、后台静默下载、优先级队列)值得你打印出来贴在工位上反复琢磨。它不炫技,但每一块砖都砌得严丝合缝。
2. 整体架构与设计思路:为什么用Java?为什么拒绝“智能”?
很多人看到“300+书源”第一反应是:“这得有多少重复代码?”——恰恰相反,整个项目里90%的书源解析逻辑,共用同一套解析引擎。它的架构不是“一个书源一个类”,而是“一个引擎+N个配置”。这种设计不是偷懒,是直面现实:小说站点改版频率太高了。去年还好好用的“笔趣阁”XPath,今年可能因为前端加了个<div class="ad-placeholder">就全崩。如果每个书源都写独立解析器,维护成本指数级上升。所以它用Java写了三层抽象:
第一层是协议适配层(ProtocolAdapter):统一处理HTTP请求头(自动添加User-Agent、Referer)、Cookie持久化(用SharedPreferences存会话)、重试策略(默认3次,间隔1s/2s/4s)。这里没用OkHttp高级特性,就用原生HttpURLConnection,因为实测发现某些老旧站点(比如一些用ASP.NET WebForms的老站)对OkHttp的Connection: keep-alive头特别敏感,反而容易503。
第二层是结构化解析层(StructureParser):核心就两个方法——parseList(Document doc, String xpath)和parseContent(String html, String regex)。前者用Jsoup的selectXpath()(注意:不是原生Jsoup自带的,是项目里自己封装的轻量XPath解析器,仅支持//div[@class='list']/a/@href这类基础语法,不支持函数调用,牺牲灵活性换稳定性);后者用标准Java正则,但强制要求必须包含至少一个命名捕获组(?<content>[\s\S]+),否则解析失败时会明确提示“正则未定义content组”,避免新手写错正则却找不到原因。
第三层是业务逻辑层(BusinessLogic):这部分才按书源隔离。比如“顶点小说网”需要在章节内容里手动过滤广告段落(正则(?s)<div class=\"ad.*?</div>),而“起点中文网”免费章节需要额外请求/api/chapter?cid=xxx接口获取真实内容。这些差异逻辑写在各自书源配置的preProcessScript和postProcessScript字段里,本质是嵌入式JavaScript(用Android内置的JavaScriptEngine,非WebView),脚本执行结果会注入到后续解析流程中。这样既保持核心引擎干净,又赋予单个书源定制能力。
至于为什么坚持用Java?我跟原作者聊过,他说有三个硬原因:一是团队里有两位主力开发用Eclipse写了十年Java,转Kotlin学习成本高;二是Java字节码在ProGuard混淆后体积比Kotlin小12%,这对追求“APK小于8MB”的目标很关键;三是Java的ThreadLocal和synchronized在IO密集型场景下,比Kotlin协程更容易做线程安全审计——他们上线前做过压力测试:同时打开50本书、每本缓存前100章,Java版本内存峰值稳定在180MB,Kotlin协程版本因调度器开销波动到240MB以上。这不是教条主义,是拿真机跑出来的数据说话。
拒绝“智能”更是关键决策。项目里没有任何机器学习模块,不搞“自动识别章节标题”“智能去广告”。所有规则必须人工编写、人工验证、人工标注。理由很朴素:小说站点HTML结构千奇百怪,但人类一眼就能看出“这个<h3>是标题,那个<p style="text-align:center">是广告”。用算法去拟合,准确率永远卡在92%~95%,剩下的5%错误会导致整章内容错乱,用户信任度归零。而人工规则,写对一次,永久有效。配套的web-editor.html就是为此服务的——它是个纯前端页面,拖进去一个网页HTML,输入XPath,实时显示匹配结果;再粘贴一段正文HTML,写正则,右边立刻高亮捕获内容。我试过用它调试“晋江文学城”的章节提取,原来要花半小时试错,现在三分钟搞定:选中标题标签→右键“Copy XPath”→粘贴到编辑框→点“Test”,标题列表秒出;再选中正文段落→按Ctrl+Shift+C抓取→生成正则<div class="noveltext">(?<content>[\s\S]*?)</div>→测试通过。这种确定性,才是生产力。
3. 核心模块深度解析:书源管理、缓存、夜间模式怎么炼成的
3.1 书源管理:JSON驱动的热插拔系统
书源不是硬编码在Java类里的,而是存在sources.json这个文件里。打开它,你会看到类似这样的结构:
{ "sources": [ { "id": "biquge_com", "name": "笔趣阁", "baseUrl": "https://www.biquge.com.cn", "bookListUrl": "/book/{bookId}/", "bookListXpath": "//div[@class='l']/ul/li/a/@href", "bookNameXpath": "//div[@class='info']/h1/text()", "authorXpath": "//div[@class='info']/p[1]/text()", "chapterListXpath": "//div[@class='listmain']/dl/dd/a/@href", "chapterTitleXpath": "//div[@class='listmain']/dl/dd/a/text()", "contentRegex": "(?s)<div id=\"content\">(?<content>.*?)</div>", "encoding": "gbk", "timeout": 15000, "enabled": true } ] }关键点在于bookListUrl里的{bookId}占位符——这不是模板字符串,而是由BookSearchActivity.java里的generateBookUrl()方法动态替换的。当你在搜索框输入“斗破苍穹”,它会先请求https://www.biquge.com.cn/search.php?q=%E6%96%97%E7%A0%B4%E8%8B%8D%E7%A9%B9,解析返回的搜索结果页,提取<a href="/book/12345/">中的12345,再拼成https://www.biquge.com.cn/book/12345/。整个过程没有网络请求阻塞主线程,因为用了AsyncTask(注意:不是ExecutorService,因为AsyncTask在Android 11+已被废弃,但此项目最低只支持到Android 5.0,所以用它反而更轻量)。
新增书源的操作路径是:设置页→书源管理→右上角+号→填表单→保存。表单提交后,代码会做三件事:1)校验必填字段(id不能重复、baseUrl必须含http、contentRegex必须含content组);2)用Jsoup.connect(testUrl).get()拉取测试页HTML;3)用你填的XPath/正则当场解析,成功才写入JSON。这个“保存即验证”机制,避免了大量无效书源堆积。我曾经手误把chapterListXpath写成//dl/dd/a/@herf(少个f),点击保存时直接弹Toast:“XPath解析失败:未找到匹配元素”,并高亮显示错误字段——这种即时反馈,比编译报错有用十倍。
3.2 缓存机制:三级存储如何协同工作
缓存不是简单地把HTML存SD卡,而是精密的三级流水线:
一级:内存缓存(LruCache)
存的是ChapterContent对象(含标题、正文、格式化时间戳),大小设为Runtime.getRuntime().maxMemory() / 8,约24MB(在2GB内存手机上)。关键优化是:只缓存最近阅读的50章,且每章内容超过50KB自动降级到二级缓存。这样既保证滑动流畅,又防内存溢出。二级:SQLite索引缓存(ChapterCacheDBHelper)
表结构极简:chapter_id(TEXT, PK),book_id(TEXT),title(TEXT),content_hash(TEXT),last_read_time(INTEGER)。注意content_hash不是存全文,而是MD5(content.substring(0, 1000))——只取前1000字符哈希,用于快速判断内容是否变更。真正内容存在三级缓存里,这里只存指针。三级:文件缓存(FileCacheManager)
路径是/Android/data/com.xxx.mybookshelf/cache/chapters/,文件名是bookId_chapterId.content,内容是GZIP压缩后的UTF-8文本。为什么用GZIP?实测对比:未压缩50KB文本占磁盘52KB,GZIP后仅18KB,且Android解压耗时平均0.8ms(ZInputStream),远低于IO等待时间。删除策略是LRU+空间阈值双控:当缓存目录超500MB,或最久未访问文件超30天,触发清理。
三者协作流程:读取章节时,先查内存→命中则返回;未命中查SQLite→有记录则用content_hash比对本地文件→文件存在且哈希一致,解压读取;文件不存在或哈希不一致,则网络请求→存入三级→更新二级索引→写入一级缓存。整个过程对UI线程零阻塞,所有IO操作都在AsyncTask后台线程完成。我在MIUI 14上测试过:连续翻100章,内存占用曲线平滑,无GC抖动。
3.3 夜间模式:不只是换个颜色
夜间模式常被做成简单的Theme.AppCompat.DayNight切换,但这套代码做了更深的定制。它不依赖系统主题,而是自己维护两套CSS样式表:
- 白天模式CSS:
body { background:#fff; color:#333; } p { line-height:1.8; margin:1em 0; } - 夜间模式CSS:
body { background:#121212; color:#e0e0e0; } p { line-height:2.0; margin:1.2em 0; }
关键在WebView加载时,动态注入CSS。ReaderWebView.java里重写了onPageFinished(),在页面加载完成后执行:
webView.evaluateJavascript( "(function(){var style=document.createElement('style');" + "style.innerHTML='" + cssContent + "';" + "document.head.appendChild(style);})()", null );这样做的好处是:1)字体抗锯齿更优(系统主题切换有时导致WebView文字发虚);2)可单独控制段落间距、行高、首行缩进(白天模式首行缩进2em,夜间模式缩进3em,缓解暗光下视觉疲劳);3)支持“护眼绿”模式——在夜间CSS基础上,把color改成#98C379,背景微调为#0A1929,模拟墨水屏效果。这个功能藏在设置页底部“高级选项”里,开关状态存在SharedPreferences的night_mode_type字段,值为0(标准黑)/1(护眼绿)/2(深灰)。
4. 实操全流程:从导入工程到添加首个自定义书源
4.1 环境准备与工程导入(避坑指南)
第一步不是打开Android Studio,而是检查JDK版本。项目gradle.properties里写着org.gradle.java.home=/Library/Java/JavaVirtualMachines/jdk-11.0.2.jdk/Contents/Home(Mac路径),对应Gradle插件版本com.android.tools.build:gradle:4.2.2。这意味着你必须用JDK 11,用JDK 17会报Unsupported class file major version 61。我踩过的坑:在Windows上装了JDK 17,Android Studio自动选中它,编译时报错一堆@NonNull找不到——因为androidx.annotation:annotation:1.1.0在JDK 17下解析注解失败。解决方案:File → Project Structure → SDK Location → JDK location,手动指向JDK 11安装目录。
第二步是签名文件处理。key.properties.jks是加密的密钥库,但密码没写在代码里。打开app/build.gradle,找到signingConfigs块:
signingConfigs { release { storeFile file("../key.properties.jks") storePassword System.getenv("KEYSTORE_PASSWORD") ?: "changeit" keyAlias System.getenv("KEY_ALIAS") ?: "mykey" keyPassword System.getenv("KEY_PASSWORD") ?: "changeit" } }看到没?密码从环境变量读取。所以你要么在系统里设KEYSTORE_PASSWORD=yourpass,要么直接把?: "changeit"改成你的密码(推荐后者,省事)。changeit是Java密钥库默认密码,很多教程忽略这点,导致签名失败。
第三步是解决Gradle同步问题。build.gradle里有两处易错:
-compileSdkVersion 30→ 如果你用Android Studio Giraffe,需升级到33,但必须同步改targetSdkVersion,否则运行时报SecurityException。
-implementation 'androidx.appcompat:appcompat:1.2.0'→ 这个版本太老,和新SDK冲突。我改成1.6.1,并在gradle.properties加一行android.useAndroidX=true。
导入后,首次编译会卡在:app:mergeDebugResources,因为res/values/strings.xml里有中文引号“”,而旧版aapt2会把它当非法字符。解决方案:用Notepad++把整个strings.xml转成UTF-8无BOM格式,再重试。
4.2 添加自定义书源:手把手实战
我们以“新笔趣阁”(https://www.xbiquge.la)为例,演示完整流程:
Step 1:分析网页结构
打开https://www.xbiquge.la/10_10837/(《诡秘之主》主页),按F12看Elements。发现:
- 小说名在<div id="info"><h1>诡秘之主</h1></div>
- 章节列表在<div id="list"><dl><dd><a href="/10_10837/123456.html">第一章</a></dd></dl></div>
- 章节内容在<div id="content">正文文本...</div>
Step 2:写XPath/正则
用web-editor.html测试:
- 列表页XPath://div[@id='list']//dd/a/@href→ 测试返回/10_10837/123456.html
- 章节标题XPath://div[@id='list']//dd/a/text()→ 返回“第一章”
- 内容正则:(?s)<div id="content">(?<content>.*?)</div>→ 测试捕获成功
Step 3:构造JSON书源
复制book_source_template.json,填入:
{ "id": "xbiquge_la", "name": "新笔趣阁", "baseUrl": "https://www.xbiquge.la", "bookListUrl": "/{bookId}/", "bookListXpath": "//div[@id='info']/h1/text()", "chapterListXpath": "//div[@id='list']//dd/a/@href", "chapterTitleXpath": "//div[@id='list']//dd/a/text()", "contentRegex": "(?s)<div id=\"content\">(?<content>.*?)</div>", "encoding": "utf-8", "timeout": 12000 }注意bookListUrl是/{bookId}/,因为主页URL就是https://www.xbiquge.la/10_10837/,bookId直接取路径第一段。
Step 4:注入并验证
把JSON粘贴到APP的“书源管理”→“导入JSON”里,点确定。APP会自动解析并测试:拉取https://www.xbiquge.la/10_10837/,用bookListXpath提取书名,若成功则保存。然后你就能在书架里搜到《诡秘之主》,点开看第一章——内容完美呈现,连段落空行都保留。
5. 常见问题与排查技巧实录:那些文档没写的真相
5.1 典型问题速查表
| 问题现象 | 可能原因 | 排查步骤 | 解决方案 |
|---|---|---|---|
| 搜索不到书,提示“未找到结果” | bookListUrl拼接错误,或搜索页结构变更 | 1)用Chrome打开搜索URL,看返回HTML是否有<a href="/book/12345/">2)检查 BookSearchActivity.java里parseSearchResult()方法 | 修改XPath,或在preProcessScript里用JS修正HTML结构 |
| 章节内容为空白 | contentRegex未捕获到content组,或编码错误 | 1)在web-editor.html里粘贴章节HTML,测试正则2)查看Logcat过滤 "ParseError"日志 | 确保正则含(?<content>...),且encoding字段与网页<meta charset="...">一致 |
| 夜间模式失效 | WebView未注入CSS,或CSS语法错误 | 1)在ReaderWebView.java的onPageFinished()里加log2)检查CSS里是否有未闭合的 { | 用在线CSS校验工具检查语法,确保无@import等不支持语法 |
| 缓存不更新,旧内容一直显示 | SQLite索引未刷新,或文件缓存哈希未更新 | 1)用adb shell进入/data/data/com.xxx.mybookshelf/files/,删sources.json2)查 chapter_cache.db里对应chapter_id的content_hash | 在FileCacheManager.java的saveChapter()里,确认updateHashInDB()被调用 |
5.2 独家避坑技巧
技巧1:XPath调试的“三明治法”
别一上来就写复杂XPath。先用最粗粒度://*,看Jsoup返回多少节点;再加一层//div,看数量是否合理;最后精确到//div[@id='content']。我调试“起点中文网”时,发现它用<div id="readfree">包裹正文,但XPath写成//div[@id='content']永远为空——因为真实ID是readfree。用“三明治法”层层缩小范围,比盲目猜高效十倍。
技巧2:正则的“最小贪婪”原则(?s)<div>(?<content>.*?)</div>里的?不是可有可无。没有它,.*会贪婪匹配到最后一个</div>,导致整页HTML被吞。加?变成惰性匹配,只到第一个</div>就停。我在处理“晋江文学城”时,原文有多个<div>嵌套,没加?导致捕获内容包含大量无关HTML标签。
技巧3:书源启用/禁用的隐藏逻辑sources.json里"enabled": false的书源,不是简单跳过,而是参与搜索但不显示结果。这是为了防止误操作禁用后,用户以为功能坏了。如果你想彻底移除,得删掉整个JSON对象,或改id为"disabled_xbiquge_la"(加前缀),让它不被识别。
技巧4:ProGuard混淆的致命陷阱proguard-rules.pro里有一行-keep class com.mybookshelf.reader.parser.** { *; },但漏了HtmlParser的内部类。某次我升级Jsoup到1.15.3,它新增了Elements内部类,结果混淆后parseList()返回空集合。解决方案:在ProGuard里加-keep class org.jsoup.select.** { *; },把Jsoup所有类都保留。
最后分享个小技巧:想快速验证某个书源是否还活着?不用打开APP,直接用命令行:
curl -s "https://www.biquge.com.cn/book/12345/" \| grep -o "<title>.*</title>"如果返回<title>斗破苍穹最新章节_斗破苍穹无弹窗广告_斗破苍穹全文阅读</title>,说明站点正常,XPath大概率还能用。这比每次编译APP快多了。
本文还有配套的精品资源,点击获取
简介:一套开箱即用的Android小说阅读APP源码,纯Java开发,基于Android Studio构建,支持直接编译安装运行。内置超300个已验证可用的小说书源,覆盖主流免费小说站点,所有书源均按标准JSON格式组织,便于查看与复用。提供完整的书源管理界面,用户可手动添加新书源、编辑现有规则(如列表页XPath、内容页正则、章节提取逻辑等),无需重新编译即可生效。源码模块划分清晰:包含书源解析引擎、缓存机制、离线下载、夜间模式、字体/排版调节、书架同步等实用功能。工程结构符合Android官方推荐规范,含gradle配置、签名文件(key.properties.jks)、混淆规则(proguard-rules.pro)、README文档及常见问题说明。配套提供书源整理工具和web-editor.html在线规则调试页面,方便快速验证XPath与正则表达式。适配Android 5.0至Android 14,无第三方SDK依赖,导入即调,适合Java安卓开发者学习源码逻辑、二次定制或搭建私有阅读平台。
本文还有配套的精品资源,点击获取