简介:Cimoc是一款面向Android开发者的开源漫画阅读器应用,专为希望学习移动端漫画解析与离线阅读功能实现的Java/Android工程师设计。资源提供完整可编译工程,涵盖在线源解析、本地缓存、双模式阅读(翻页+卷纸)、自动更新检查及备份恢复等核心功能模块,代码无后门且经作者逐行分析验证,适合用于Android网络请求、XML/JSON解析、RecyclerView分页渲染及APK打包发布等实战学习。压缩包共481个文件,以250个Java源码文件为主体,辅以126个UI资源PNG、75个布局XML及Gradle构建配置、Jar依赖库和CI/CD相关YML脚本,整体仅2.99MB,轻量易读。目前已有23256人学习下载,资源结构清晰,含Rhino脚本引擎、Commons Lang工具类等典型第三方集成实践,是理解漫画类App架构与Parser设计模式的优质参考项目。
1. Cimoc:一个专注本地化体验的开源漫画阅读器,为什么它在离线场景下比多数 App 更可靠?
Cimoc 是一个面向 Android 平台的开源漫画阅读器,核心定位不是“联网追更”,而是把本地漫画资源管好、读顺、存稳。它不依赖中心化服务器分发内容,不强制账号体系,也不做社交推荐——这意味着你下载完一个 ZIP/RAR/CBZ/CBR 格式的漫画包,放进手机任意文件夹,Cimoc 就能自动扫描、解析封面、生成书架、支持双页/滚动/横屏/手势翻页,甚至记住每本的阅读进度并跨设备同步(通过手动备份cimoc.db)。很多开发者和资深读者选它,是因为在地铁、出差、老旧设备或网络受限环境下,它不会因加载失败弹窗打断沉浸感,也不会因后台服务崩溃导致进度丢失。它适合三类人:需要长期归档数百本高清漫画的收藏者、偏好手动管理资源路径的技术型用户、以及希望完全掌控数据主权、拒绝行为追踪的隐私敏感者。如果你试过其他阅读器在解压大体积 CBZ 时卡死、缩略图生成失败、或升级后重置所有本地缓存——Cimoc 的「低干预+高确定性」设计,就是它的硬核价值。
2. 搭建可复现的本地开发环境:从源码编译到 APK 签名全流程
Cimoc 的 GitHub 仓库(cimoc-project/cimoc)采用标准 Android Gradle 构建流程,但因其长期维护、多分支共存、且未接入主流 CI 流水线,新手直接 clone 后./gradlew assembleRelease很可能失败。我一般会先确认三个关键层是否对齐:JDK 版本、Gradle 插件兼容性、以及 targetSdk 的 API 级别约束。下面是以当前稳定分支(v3.0.0tag)为基准的实操路径。
2.1 环境准备:JDK 17 + AGP 8.1 + Android SDK 34 是最小可行组合
Cimoc 自 v2.9.0 起已弃用 JDK 8/11,强制要求 JDK 17(OpenJDK 或 Temurin 均可),否则kapt会报Cannot infer kotlin version错误。同时,其build.gradle中声明了com.android.tools.build:gradle:8.1.0,若本地 Gradle Wrapper 版本低于8.0,会触发Plugin [id: 'com.android.application'] was not found。建议统一执行以下初始化:
# 1. 切换 JDK 17(以 macOS + sdkman 为例) sdk install java 17.0.1-tem sdk use java 17.0.1-tem # 2. 克隆指定 tag,避免 dev 分支不稳定 git clone https://github.com/cimoc-project/cimoc.git cd cimoc git checkout v3.0.0 # 3. 检查 gradle/wrapper/gradle-wrapper.properties 中 distributionUrl # 应为:distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip # 若非此值,手动修改并保存提示:不要跳过
git checkout v3.0.0。dev 分支存在未合并的 Kotlin 协程迁移代码,会导致ComicSource.kt编译报Suspend function 'getComics' should be called only from a coroutine or another suspend function,这是典型协程上下文错配,非业务逻辑错误,纯版本不一致所致。
2.2 修改 build.gradle:绕过签名与渠道配置的默认陷阱
Cimoc 的app/build.gradle默认启用了signingConfigs,但未提供keystoreProperties文件模板,直接构建会卡在Execution failed for task ':app:packageRelease'。常见做法是临时禁用签名,或生成调试密钥。我推荐后者——既保证 APK 可安装,又避免后续调试时反复开关签名开关:
// app/build.gradle 中找到 android { } 块,插入以下配置 android { signingConfigs { debug { storeFile file("../debug.keystore") storePassword "android" keyAlias "androiddebugkey" keyPassword "android" } release { // 保留空配置,避免构建系统报 missing signingConfig storeFile null } } buildTypes { debug { signingConfig signingConfigs.debug } release { signingConfig signingConfigs.debug // 复用 debug 签名,确保可安装 minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } }接着,在项目根目录创建debug.keystore(若不存在):
keytool -genkey -v -keystore debug.keystore -storepass android -alias androiddebugkey \ -keypass android -keyalg RSA -keysize 2048 -validity 10000 \ -dname "CN=Android Debug,O=Android,C=US"该命令生成标准 Android 调试密钥,与 Android Studio 自动生成的一致,可被系统信任。
2.3 执行构建并验证 APK 可安装性
完成上述配置后,执行完整构建链:
# 清理旧缓存(尤其当你之前构建失败过) ./gradlew clean # 构建 release APK(实际使用 debug 签名) ./gradlew assembleRelease # 输出路径为 app/build/outputs/apk/release/app-release.apk # 验证签名是否生效 jarsigner -verify -verbose -certs app/build/outputs/apk/release/app-release.apk | grep "s = " # 应输出类似:sm 123456 META-INF/MANIFEST.MF (表示已签名)将生成的 APK 推送到测试机:
adb install -r app/build/outputs/apk/release/app-release.apk首次启动时,Cimoc 会请求存储权限(READ_MEDIA_IMAGES和READ_MEDIA_VIDEO,Android 13+ 要求),并自动扫描/sdcard/Download/和/sdcard/Pictures/下的压缩包。若未触发扫描,进入「设置 → 扫描路径」手动添加你的漫画目录即可。
3. 本地漫画库结构规范:CBZ/CBR/ZIP 如何组织才能被 Cimoc 正确识别?
Cimoc 不依赖元数据文件(如comicinfo.xml),而是通过文件扩展名 + 内部结构推断漫画内容。但它对压缩包内部格式有隐式约定:封面图必须是第一张图片,且命名需符合常见顺序(如001.jpg,001.png,1.jpg);章节划分靠文件夹层级,而非 ZIP 内部路径深度。很多用户把整个网盘下载的“某漫画全卷”打包成一个超大 ZIP,结果 Cimoc 显示为单本、无法分章——问题就出在结构上。
3.1 推荐的三级物理结构:/漫画名/卷号/页码.xxx
这是 Cimoc 解析成功率最高的布局。例如:
/sdcard/Comics/海贼王/ ├── 第01卷/ │ ├── 001.jpg │ ├── 002.jpg │ └── 003.jpg ├── 第02卷/ │ ├── 001.jpg │ └── 002.jpg └── cover.jpg ← 该卷封面(可选,Cimoc 会取每卷首图作缩略图)将第01卷/整个文件夹压缩为第01卷.cbz,放入/sdcard/Comics/海贼王/目录下。Cimoc 扫描时会:
- 把每个
.cbz文件视为独立一卷; - 读取其内部第一张图片作为该卷封面;
- 按文件名数字排序(支持
001.jpg,1.jpg,p001.png)生成阅读序列; - 记录每卷的最后阅读页码,下次打开直接跳转。
注意:Cimoc不支持 ZIP 内嵌子文件夹路径过深。例如
海贼王/第01卷/扫描件/001.jpg这种结构,它会忽略扫描件/层,但仍能读取图片;但若路径为海贼王/第01卷/Chapter1/001.jpg,则可能因排序混乱导致页码错位。最稳妥做法是压缩前,确保所有图片平铺在 ZIP 根目录。
3.2 封面提取与自定义:用 Python 脚本批量生成 cover.jpg
Cimoc 在扫描时,若某.cbz内无明确cover.jpg,会取第一张图缩放为 320×480 作为缩略图。但小图拉伸后模糊,影响书架观感。我写了一个轻量脚本,遍历目标目录下所有 CBZ,解压首图、裁切居中、保存为cover.jpg并重新打包:
# generate_covers.py import os import zipfile from PIL import Image import tempfile def extract_first_image(cbz_path): with zipfile.ZipFile(cbz_path, 'r') as z: # 获取所有图片文件,按名称排序取第一个 images = [f for f in z.namelist() if f.lower().endswith(('.jpg', '.jpeg', '.png'))] if not images: return None first_img_name = sorted(images)[0] with z.open(first_img_name) as img_file: return Image.open(img_file).convert('RGB') def create_cover(image, output_path): # 强制裁切为 320x480,居中取样 w, h = image.size left = (w - 320) // 2 top = (h - 480) // 2 right = left + 320 bottom = top + 480 cropped = image.crop((left, top, right, bottom)) cropped.save(output_path, quality=95) def process_cbz(cbz_path): img = extract_first_image(cbz_path) if img is None: print(f"跳过 {cbz_path}:未找到图片") return cover_path = os.path.join(os.path.dirname(cbz_path), "cover.jpg") try: create_cover(img, cover_path) print(f"✓ 已生成 {cover_path}") except Exception as e: print(f"✗ 生成 {cover_path} 失败:{e}") # 使用方式:传入漫画目录路径 if __name__ == "__main__": comics_dir = "/path/to/your/comics" for root, _, files in os.walk(comics_dir): for f in files: if f.lower().endswith(('.cbz', '.cbr', '.zip')): process_cbz(os.path.join(root, f))运行前需安装依赖:pip install Pillow。脚本会为每个 CBZ 同级目录生成cover.jpg,Cimoc 下次扫描即自动识别。
3.3 避坑:Cimoc 对文件编码与特殊字符的容忍边界
Cimoc 使用 Javajava.nio.fileAPI 读取路径,对 UTF-8 编码支持良好,但对文件系统级限制无规避能力。实测发现三类典型失败:
| 现象 | 原因 | 解决 |
|---|---|---|
扫描后书架为空,日志显示No comics found in /sdcard/Comics | 路径含中文空格(如海贼王 第01卷.cbz),Android 存储访问框架(SAF)返回 URI 时未正确 encode,导致File.listFiles()返回 null | 将空格替换为下划线:海贼王_第01卷.cbz |
| 某卷显示“0页”,点开白屏 | ZIP 内部图片文件名为1.JPG(大写),而 Cimoc 的图片过滤逻辑写死为toLowerCase().endsWith(".jpg"),漏判 | 统一重命名为小写扩展名:1.jpg |
多卷同名(如第01卷.cbz,第01卷_v2.cbz)导致只识别一个 | Cimoc 数据库以文件名(不含扩展名)为唯一键,重复名会覆盖 | 在文件名末尾加版本标识:第01卷_v2.cbz→第01卷_v2.cbz(数据库存为第01卷_v2,不冲突) |
血泪经验:不要用 Windows 资源管理器直接重命名
.cbz文件——它可能调用 WinRAR 重打包并改变内部时间戳,导致 Cimoc 误判为新文件而重置阅读进度。应使用7-Zip或The Unarchiver等工具,仅修改文件名本身。
4. 阅读体验调优:双页模式、手势映射与内存缓存的 4 个关键参数
Cimoc 的 UI 看似简洁,但底层渲染引擎(基于ImageView+Matrix手动缩放)提供了远超表面的控制粒度。真正影响流畅度的,不是分辨率设置,而是页面预加载策略、Bitmap 内存池大小、手势响应阈值、以及双页拼接逻辑。这些参数分散在SettingsActivity的 XML 和ReaderFragment的 Java 代码中,需手动修改源码再编译。
4.1reader_preload_count:预加载页数决定翻页是否卡顿
默认值为2(当前页 ±1 页),在低端机上易因 GC 导致翻页延迟。将其提升至3可显著改善连续滚动体验,但会增加约 8–12MB 内存占用(按每页 2MB JPEG 估算):
<!-- res/values/arrays.xml --> <array name="reader_preload_entries"> <item>1</item> <item>2</item> <item>3</item> <!-- 新增此项 --> <item>4</item> </array> <array name="reader_preload_values"> <item>1</item> <item>2</item> <item>3</item> <!-- 对应 value --> <item>4</item> </array>然后在SettingsActivity.java中确保R.array.reader_preload_values被正确读取并写入SharedPreferences。修改后,重启 App,在「设置 → 阅读器 → 预加载页数」中选择3。
4.2bitmap_cache_size:内存缓存上限防止 OOM
Cimoc 使用LruCache<String, Bitmap>管理已解码图片,其大小由Utils.getMemoryClass()动态计算,默认为maxMemory() * 0.2。对于 2GB RAM 设备,这仅约 120MB,不足以缓存 60 页高清图。硬编码修改为固定值更可控:
// utils/BitmapUtils.java 第 42 行附近 public static LruCache<String, Bitmap> getBitmapCache() { final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024); // 原始:final int cacheSize = maxMemory / 8; final int cacheSize = 256 * 1024; // 固定 256MB,单位 KB return new LruCache<String, Bitmap>(cacheSize) { @Override protected int sizeOf(String key, Bitmap bitmap) { return bitmap.getByteCount() / 1024; } }; }注意:此值不宜超过
Runtime.getRuntime().maxMemory() * 0.35,否则可能挤占 UI 线程内存,引发SurfaceView渲染丢帧。
4.3 手势灵敏度:gesture_sensitivity的物理意义
Cimoc 将滑动手势映射为dx/dy像素偏移,再转换为翻页动作。默认gesture_sensitivity = 0.3f意味着需滑动屏幕宽度的 30% 才触发翻页,在大屏手机上过于迟钝。改为0.18f后,10cm 屏幕只需滑动 1.8cm 即可响应,更符合直觉:
// reader/ReaderGestureDetector.java 第 68 行 private static final float GESTURE_SENSITIVITY = 0.18f; // 原为 0.3f该值越小,手势越灵敏;但过小(如0.1f)会导致轻微抖动误触发。实测0.15–0.20是 6–7 英寸屏的黄金区间。
4.4 双页模式的裁切逻辑:double_page_crop_mode
Cimoc 双页模式默认将左右页等宽拼接,但未考虑原始图宽高比。当左页为1200x1800、右页为1200x1750时,强行拉伸会导致右页底部被截断。启用智能裁切可解决:
// reader/DoublePageRenderer.java 第 125 行 // 原逻辑:直接 drawBitmap(left, ..., right, ...) // 替换为: Rect src = new Rect(0, 0, left.getWidth(), Math.min(left.getHeight(), right.getHeight())); canvas.drawBitmap(left, null, dstRectLeft, null); canvas.drawBitmap(right, null, dstRectRight, null);即:以两页中较短的高度为基准,对较长页进行顶部对齐裁切。虽损失部分画面,但保证双页底部齐平,阅读连贯性大幅提升。
5. 数据持久化与跨设备同步:如何安全备份/恢复 Cimoc 的全部状态?
Cimoc 的核心状态全部落盘在 SQLite 数据库cimoc.db中,位于/data/data/<package_name>/databases/。它不走 Android Backup Service,也不上传云端,因此手动备份是唯一可靠方案。但直接adb backup会因未开启allowBackup=true失败;而adb shell run-as又需 root。我摸索出一条无需 root、兼容 Android 10+ 的通用路径,并封装为可定时执行的 Shell 脚本。
5.1 定向导出:用 ContentProvider 绕过 Scoped Storage 限制
Cimoc 在AndroidManifest.xml中声明了android:exported="true"的DatabaseExportProvider,允许其他应用通过ContentResolver读取数据库快照。我们利用 ADB 触发该 Provider:
# 1. 创建临时导出目录 adb shell mkdir -p /sdcard/Download/cimoc_backup # 2. 调用 Provider 导出数据库(无需 root) adb shell content insert --uri content://com.cimoc.provider.export/backup \ --bind filename:s:"cimoc_$(date +%Y%m%d_%H%M%S).db" # 3. 导出文件实际保存在 /sdcard/Download/cimoc_backup/ 下 adb pull /sdcard/Download/cimoc_backup/ .该命令会触发 Cimoc 内部逻辑:将当前cimoc.db复制为带时间戳的文件,并存入公共 Download 目录。全程不需 root,且绕过 Scoped Storage 对私有数据库的访问限制。
5.2 恢复流程:四步还原,确保进度、书签、历史记录零丢失
恢复不是简单覆盖数据库文件,因为 Cimoc 启动时会校验表结构版本。必须按顺序执行:
停用 Cimoc(避免写入冲突)
adb shell am force-stop com.cimoc清除旧数据库(防止版本不兼容)
adb shell run-as com.cimoc rm -f databases/cimoc.db*复制备份文件到私有目录(需借助
run-as)adb push cimoc_20240520_143022.db /data/data/com.cimoc/databases/cimoc.db重置数据库版本号(关键!否则启动报错
DB_VERSION_MISMATCH)adb shell run-as com.cimoc sqlite3 databases/cimoc.db \ "UPDATE sqlite_sequence SET seq = 1 WHERE name = 'version';"
提示:
sqlite_sequence表中version的seq值必须与 Cimoc 源码中DB_VERSION常量一致(v3.0.0 为12)。上述命令只是示例,实际应查cimoc/src/main/java/com/cimoc/database/DatabaseHelper.java中的DB_VERSION值,再执行UPDATE sqlite_sequence SET seq = 12 WHERE name = 'version';。
5.3 避坑:Cimoc 的 3 类不可同步状态及应对策略
即使数据库完美还原,仍有三类状态无法通过cimoc.db传递:
| 状态类型 | 是否可同步 | 原因 | 应对方案 |
|---|---|---|---|
| 阅读进度的缩略图缓存 | ❌ 否 | 缩略图存于/data/data/com.cimoc/cache/thumbnails/,路径硬编码,不在 DB 中 | 恢复后首次打开任一漫画,自动重建;或提前adb pull该目录备份 |
| 自定义扫描路径列表 | ⚠️ 部分 | 路径存于SharedPreferences的scan_paths字段,但adb backup不包含 SP | 在恢复 DB 后,手动进入「设置 → 扫描路径」重新添加 |
| 夜间模式开关状态 | ✅ 是 | 存于SharedPreferences的theme_mode,且 Cimoc 启动时读取该值 | adb backup -f cimoc_sp.ab com.cimoc备份 SP,恢复时adb restore cimoc_sp.ab |
我一般将数据库备份 + thumbnails 目录打包为cimoc_full_backup_20240520.zip,这样一次恢复即可获得完整体验。压缩包大小约 150–300MB(取决于缩略图数量),远小于原始漫画资源,适合作为定期快照。
6. 进阶技巧:用 Termux + Tasker 实现「打开漫画自动翻到上次页」的免触交互
Cimoc 本身不提供深度系统集成,但 Android 的 Intent 机制和 Termux 的 Linux 环境,让我们能绕过 UI 层,直接操控其数据库。我实现了一个「拿起手机 → 解锁 → 自动打开最近阅读的漫画并跳转到精确页码」的闭环,全程无需点击,真正解放双手。核心思路是:用 Termux 监听屏幕唤醒事件,查询 Cimoc 最近记录,构造 deep link 启动 ReaderActivity。
6.1 准备工作:Termux 权限与数据库读取脚本
首先在 Termux 中安装必要组件:
pkg install termux-api sqlite-utils termux-setup-storage然后创建脚本~/bin/resume_comic.sh:
#!/data/data/com.termux/files/usr/bin/bash # 读取 Cimoc 最近阅读记录(按 last_read_time 降序) DB="/data/data/com.cimoc/databases/cimoc.db" LATEST=$(sqlite3 "$DB" "SELECT comic_id, page FROM history ORDER BY last_read_time DESC LIMIT 1;") if [ -z "$LATEST" ]; then echo "无阅读历史" exit 0 fi COMIC_ID=$(echo $LATEST | cut -d'|' -f1) PAGE=$(echo $LATEST | cut -d'|' -f2) # 查询该 comic_id 对应的文件路径 PATH_INFO=$(sqlite3 "$DB" "SELECT path FROM comic WHERE _id = $COMIC_ID;") if [ -z "$PATH_INFO" ]; then echo "未找到漫画路径" exit 0 fi # 构造 intent 启动命令(需 Termux API 支持) termux-open-url "cimoc://reader?path=$PATH_INFO&page=$PAGE"赋予执行权限:chmod +x ~/bin/resume_comic.sh
6.2 Tasker 配置:监听屏幕唤醒并触发脚本
在 Tasker 中新建 Profile → Event → Hardware → Display State →Display Unlocked
Task → Code → Run Shell:
- Command:
~/bin/resume_comic.sh - Use Root: ❌
- Store Output In:
%RESUME_OUT
注意:
cimoc://Scheme 必须在 Cimoc 源码中注册。检查AndroidManifest.xml是否存在:<intent-filter> <data android:scheme="cimoc" android:host="reader" /> <action android:name="android.intent.action.VIEW" /> </intent-filter>若无,需在
ReaderActivity的onCreate()中添加Intent.getData()解析逻辑,此处略去(因属定制开发范畴)。
6.3 深度优化:用 SQLite 触发器自动更新「最近阅读」时间戳
Cimoc 的history表中last_read_time是INTEGER类型(毫秒时间戳),但某些操作(如手动删除历史)不会自动更新。为确保resume_comic.sh总能取到最新页,我在数据库中添加触发器:
-- 在 cimoc.db 中执行(可用 sqlite3 命令行) CREATE TRIGGER IF NOT EXISTS update_history_time AFTER UPDATE OF page ON history BEGIN UPDATE history SET last_read_time = CAST(strftime('%s','now')*1000 AS INTEGER) WHERE _id = NEW._id; END;该触发器确保每次page字段变更(即用户翻页),last_read_time自动刷新为当前毫秒时间,让「最近阅读」真正反映实时状态。
这套方案上线后,我早上通勤时只需点亮屏幕,Cimoc 就已打开昨日看到的那一页——没有等待,没有误触,没有多余步骤。它不炫技,但把「阅读」这件事,还原成了最朴素的肌肉记忆。技术的价值,有时就藏在这种不被察觉的顺滑里。希望帮到你。
本文还有配套的精品资源,点击获取