一、架构解析与技术选型
EhViewer作为开源漫画阅读应用
APK包⬇️
link3.cc/yunyunjie
采用现代化Android开发技术栈构建:
// 核心技术架构示意图
class EhViewerArchitecture {
// 数据层
val repository: MangaRepository by lazy {
DefaultMangaRepository(
localDataSource = RoomMangaDatabase.getInstance(context),
remoteDataSource = EhApiService.create()
)
}
// 视图层
val viewModel: MangaViewModel by viewModels {
MangaViewModelFactory(repository)
}
// 网络层
val okHttpClient = OkHttpClient.Builder()
.addInterceptor(HttpLoggingInterceptor())
.addInterceptor(CacheInterceptor())
.build()
}
二、环境搭建与源码编译
(一)开发环境初始化脚本
#!/bin/bash
# EhViewer开发环境初始化脚本
# 安装依赖
echo "正在安装开发依赖..."
sudo apt update
sudo apt install -y openjdk-11-jdk android-sdk-build-tools android-sdk-platform-tools
# 配置Android SDK路径
echo "配置Android SDK环境变量..."
echo "export ANDROID_HOME=$HOME/Android/Sdk" >> ~/.bashrc
echo "export PATH=$PATH:$ANDROID_HOME/tools" >> ~/.bashrc
echo "export PATH=$PATH:$ANDROID_HOME/platform-tools" >> ~/.bashrc
source ~/.bashrc
# 安装必要的SDK组件
echo "安装Android SDK组件..."
sdkmanager "platforms;android-33" "build-tools;33.0.2" "sources;android-33"
echo "开发环境初始化完成!"
(二)Gradle构建优化配置
// gradle.properties 优化配置
org.gradle.jvmargs=-Xmx4g -XX:MaxPermSize=2048m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
android.enableBuildCache=true
android.enableR8=true
kotlin.incremental=true
三、核心功能实现解析
(一)漫画源插件系统
// 插件接口定义
interface SourcePlugin {
val sourceId: String
val sourceName: String
val baseUrl: String
suspend fun fetchLatestManga(page: Int): Flow<List<MangaInfo>>
suspend fun searchManga(keyword: String, page: Int): Flow<List<MangaInfo>>
suspend fun getMangaDetails(mangaId: String): Flow<MangaDetail>
suspend fun getChapterPages(chapterId: String): Flow<List<String>>
// 源配置接口
fun getConfigItems(): List<ConfigItem>
}
// 示例:实现本地文件源
class LocalFileSystem : SourcePlugin {
override val sourceId = "local_file"
override val sourceName = "本地文件"
override val baseUrl = "file://"
// 实现文件扫描逻辑
private suspend fun scanLocalMangaDir(): List<MangaInfo> {
return withContext(Dispatchers.IO) {
val dir = File(Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_PICTURES), "Manga")
if (!dir.exists() || !dir.isDirectory) return@withContext emptyList()
dir.listFiles()?.filter { it.isDirectory }?.map { dir ->
MangaInfo(
id = dir.absolutePath,
title = dir.name,
thumbnailUrl = findCoverImage(dir)?.absolutePath ?: ""
)
} ?: emptyList()
}
}
// 其他接口实现...
}
(二)下载管理器实现
// 下载任务状态管理
sealed class DownloadState {
object Pending : DownloadState()
data class Downloading(val progress: Int) : DownloadState()
object Completed : DownloadState()
data class Failed(val error: Throwable) : DownloadState()
}
// 下载管理器
class DownloadManager(
private val downloadDao: DownloadDao,
private val networkService: NetworkService
) {
private val _downloadStates = mutableMapOf<String, MutableStateFlow<DownloadState>>()
val downloadStates: Map<String, StateFlow<DownloadState>> = _downloadStates
suspend fun enqueueDownload(mangaId: String, chapterId: String) {
withContext(Dispatchers.IO) {
// 检查是否已下载
if (downloadDao.isChapterDownloaded(chapterId)) return@withContext
// 创建下载任务
val task = DownloadTask(
id = UUID.randomUUID().toString(),
mangaId = mangaId,
chapterId = chapterId,
state = DownloadState.Pending
)
// 保存到数据库
downloadDao.insertTask(task)
// 开始下载
startDownload(task)
}
}
// 并行下载实现
private suspend fun startDownload(task: DownloadTask) {
val stateFlow = MutableStateFlow<DownloadState>(DownloadState.Pending)
_downloadStates[task.id] = stateFlow
stateFlow.value = DownloadState.Downloading(0)
try {
// 获取章节页面
val pages = networkService.getChapterPages(task.chapterId)
// 创建下载目录
val downloadDir = createDownloadDirectory(task.mangaId, task.chapterId)
// 并行下载页面
pages.mapIndexed { index, pageUrl ->
async(Dispatchers.IO) {
downloadPage(pageUrl, File(downloadDir, "${index+1}.jpg"))
index + 1
}
}.awaitAll()
// 更新状态
stateFlow.value = DownloadState.Completed
downloadDao.updateTask(task.copy(state = DownloadState.Completed))
} catch (e: Exception) {
stateFlow.value = DownloadState.Failed(e)
downloadDao.updateTask(task.copy(state = DownloadState.Failed(e)))
}
}
// 其他核心方法...
}
四、UI组件与用户体验优化
(一)自定义阅读器实现
// 阅读器视图组件
class ReaderView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : FrameLayout(context, attrs, defStyleAttr) {
private val imageView: ZoomableImageView
private val pageIndicator: TextView
init {
// 初始化视图
LayoutInflater.from(context).inflate(R.layout.view_reader, this, true)
imageView = findViewById(R.id.reader_image_view)
pageIndicator = findViewById(R.id.page_indicator)
// 设置手势监听
setupGestureListeners()
}
// 加载页面
fun loadPage(imageUrl: String, pageIndex: Int, totalPages: Int) {
// 更新指示器
pageIndicator.text = "$pageIndex/$totalPages"
// 加载图片
Glide.with(context)
.load(imageUrl)
.diskCacheStrategy(DiskCacheStrategy.ALL)
.listener(object : RequestListener<Drawable> {
override fun onLoadFailed(
e: GlideException?,
model: Any?,
target: Target<Drawable>?,
isFirstResource: Boolean
): Boolean {
// 处理加载失败
return false
}
override fun onResourceReady(
resource: Drawable?,
model: Any?,
target: Target<Drawable>?,
dataSource: DataSource?,
isFirstResource: Boolean
): Boolean {
// 处理加载成功
return false
}
})
.into(imageView)
}
// 手势处理
private fun setupGestureListeners() {
// 实现翻页手势
val gestureDetector = GestureDetector(context, object : GestureDetector.SimpleOnGestureListener() {
override fun onFling(
e1: MotionEvent,
e2: MotionEvent,
velocityX: Float,
velocityY: Float
): Boolean {
// 检测左右滑动手势
val deltaX = e2.x - e1.x
if (deltaX > SWIPE_THRESHOLD && Math.abs(velocityX) > SWIPE_VELOCITY_THRESHOLD) {
// 左翻页
onPageTurnListener?.onPreviousPage()
return true
} else if (deltaX < -SWIPE_THRESHOLD && Math.abs(velocityX) > SWIPE_VELOCITY_THRESHOLD) {
// 右翻页
onPageTurnListener?.onNextPage()
return true
}
return false
}
})
// 设置触摸监听
setOnTouchListener { _, event ->
gestureDetector.onTouchEvent(event)
return@setOnTouchListener true
}
}
// 翻页回调接口
var onPageTurnListener: OnPageTurnListener? = null
interface OnPageTurnListener {
fun onPreviousPage()
fun onNextPage()
}
companion object {
private const val SWIPE_THRESHOLD = 100
private const val SWIPE_VELOCITY_THRESHOLD = 100
}
}
(二)主题系统实现
<!-- 主题配置文件 res/values/themes.xml -->
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- 基础主题 -->
<style name="AppTheme" parent="Theme.MaterialComponents.DayNight.NoActionBar">
<!-- 自定义属性 -->
<item name="colorPrimary">@color/primary</item>
<item name="colorPrimaryVariant">@color/primaryVariant</item>
<item name="colorSecondary">@color/secondary</item>
<item name="colorSecondaryVariant">@color/secondaryVariant</item>
<item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:windowLightStatusBar">true</item>
<!-- 自定义组件样式 -->
<item name="toolbarStyle">@style/AppToolbarStyle</item>
<item name="bottomNavigationStyle">@style/AppBottomNavigationStyle</item>
</style>
<!-- 暗黑主题 -->
<style name="AppTheme.Dark" parent="AppTheme">
<item name="android:statusBarColor">@android:color/black</item>
<item name="android:windowLightStatusBar">false</item>
<item name="colorSurface">@color/dark_surface</item>
<item name="colorOnSurface">@color/dark_on_surface</item>
</style>
<!-- 阅读器主题 -->
<style name="ReaderTheme" parent="AppTheme">
<item name="android:windowFullscreen">true</item>
<item name="android:windowContentOverlay">@null</item>
<item name="android:windowBackground">@android:color/black</item>
</style>
</resources>
五、性能优化与问题排查
(一)内存优化工具类
// 内存管理工具类
object MemoryManager {
private const val MIN_FREE_MEMORY_MB = 100
// 检查内存状态
fun checkMemoryStatus(context: Context): Boolean {
val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
val memoryInfo = ActivityManager.MemoryInfo()
activityManager.getMemoryInfo(memoryInfo)
val availableMemoryMb = memoryInfo.availMem / 1024 / 1024
return availableMemoryMb > MIN_FREE_MEMORY_MB
}
// 释放图片缓存
fun clearImageCache(context: Context) {
Thread {
// 清除Glide内存缓存
Glide.get(context).clearMemory()
// 清除Glide磁盘缓存(需在后台线程执行)
Glide.get(context).clearDiskCache()
}.start()
}
// 优化图片加载
fun optimizeImageLoading(options: RequestOptions): RequestOptions {
return options
.format(DecodeFormat.PREFER_RGB_565) // 降低内存占用
.override(Target.SIZE_ORIGINAL / 2) // 加载缩放后的图片
.diskCacheStrategy(DiskCacheStrategy.ALL) // 启用磁盘缓存
}
}
(二)网络请求拦截器
// 网络请求拦截器
class NetworkInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val originalRequest = chain.request()
// 添加User-Agent
val requestWithUserAgent = originalRequest.newBuilder()
.header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
.build()
// 执行请求
val response = chain.proceed(requestWithUserAgent)
// 缓存控制
if (response.isSuccessful) {
val cacheControl = CacheControl.Builder()
.maxAge(1, TimeUnit.DAYS) // 缓存1天
.build()
return response.newBuilder()
.header("Cache-Control", cacheControl.toString())
.build()
}
return response
}
}
六、扩展与贡献指南
(一)创建自定义插件
// 自定义插件示例
class MyCustomSource : SourcePlugin {
override val sourceId = "my_custom_source"
override val sourceName = "我的自定义源"
override val baseUrl = "https://mycustomsource.example.com"
// 实现接口方法
override suspend fun fetchLatestManga(page: Int): Flow<List<MangaInfo>> = flow {
// 模拟网络请求
delay(500)
// 返回示例数据
emit(
listOf(
MangaInfo(
id = "1",
title = "示例漫画1",
thumbnailUrl = "https://picsum.photos/200/300?random=1",
author = "作者1",
status = "连载中"
),
MangaInfo(
id = "2",
title = "示例漫画2",
thumbnailUrl = "https://picsum.photos/200/300?random=2",
author = "作者2",
status = "已完结"
)
)
)
}
// 其他方法实现...
}
// 插件注册
class MyPluginProvider : PluginProvider {
override fun getSourcePlugins(): List<SourcePlugin> {
return listOf(MyCustomSource())
}
}
(二)提交贡献流程
# 贡献代码流程指南
# 1. 克隆仓库
git clone https://github.com/ehviewer/ehviewer.git
cd ehviewer
# 2. 创建特性分支
git checkout -b feature/add-new-source
# 3. 开发并测试你的功能
# ...
# 4. 提交代码
git add .
git commit -m "feat: add new custom source plugin"
# 5. 同步上游更新
git remote add upstream https://github.com/ehviewer/ehviewer.git
git fetch upstream
git rebase upstream/stable
# 6. 推送分支
git push origin feature/add-new-source
# 7. 创建Pull Request
# 在GitHub上提交PR并等待审核
本指南详细介绍了EhViewer的技术架构、开发环境搭建、核心功能实现以及性能优化方法。通过阅读和实践,你可以深入理解这款开源应用的设计理念,并根据自己的需求进行定制开发。