/** * 第 9 章配套代码 · 动态模块管理器(教学版伪代码) * * 文件位置:shared/src/androidMain/kotlin/com/example/kuikly/dynamic/DynamicModuleManager.kt * * 演示 Android 端动态模块加载的核心流程: * ① 检查本地缓存 * ② 后台拉远程版本 * ③ 校验签名 + 写缓存 * ④ 用 DexClassLoader 加载 * ⑤ 注册 Pager 到全局路由表 * * ⚠️ 这是教学示意代码,真实工程需要: * - 严格的签名校验 * - 灰度策略 * - 回滚机制 * - 安全沙箱 * - iOS 端走另一套(合规约束) */ package com.example.kuikly.dynamic import dalvik.system.DexClassLoader import java.io.File import java.security.MessageDigest // ═══════════════════════════════════════════════════════════════════ // 数据模型 // ═══════════════════════════════════════════════════════════════════ data class DynamicModuleInfo( val moduleId: String, // 业务模块 ID,如 "home" "detail" val version: String, // 版本号 "1.2.3" val downloadUrl: String, // CDN 地址 val checksum: String, // SHA-256 校验 val rolloutPercent: Int, // 灰度比例 0-100 val rollbackVersion: String? // 紧急回滚指定版本 ) // ═══════════════════════════════════════════════════════════════════ // 管理器(伪代码) // ═══════════════════════════════════════════════════════════════════ object DynamicModuleManager { private const val TAG = "DynamicModule" private val loadedClassLoaders = mutableMapOf() /** * 启动入口:检查并加载所有需要的业务模块 * 一般在 Application.onCreate 调用 */ fun init(modules: List) { modules.forEach { moduleId -> // ① 优先用本地缓存(启动速度优先) tryLoadLocal(moduleId) // ② 后台异步拉远程更新(下次启动生效) backgroundFetchUpdate(moduleId) } } /** * ① 用本地缓存的最新版本加载 */ private fun tryLoadLocal(moduleId: String) { val info = LocalModuleStore.getLatest(moduleId) ?: return val dexFile = LocalModuleStore.getDexFile(moduleId, info.version) if (!dexFile.exists()) { log("模块 $moduleId 本地缓存不存在") return } // 校验签名 if (!verifyChecksum(dexFile, info.checksum)) { log("模块 $moduleId 签名校验失败,删除") dexFile.delete() return } // 用 DexClassLoader 加载 loadDex(moduleId, dexFile) } /** * ② 后台拉远程版本 */ private fun backgroundFetchUpdate(moduleId: String) { // ⚠️ 真实代码用协程 + 限流 Thread { try { // 拉取版本配置 val info = ConfigCenter.fetchModuleInfo(moduleId) ?: return@Thread // 灰度判断 if (!isInRolloutPool(info)) { log("模块 $moduleId 未命中灰度,跳过") return@Thread } // 已是最新? val local = LocalModuleStore.getLatest(moduleId) if (local?.version == info.version) { log("模块 $moduleId 已是最新版本 ${info.version}") return@Thread } // 下载 val dexFile = downloadDex(info) ?: return@Thread // 校验 if (!verifyChecksum(dexFile, info.checksum)) { log("模块 $moduleId 远程产物签名校验失败,丢弃") dexFile.delete() return@Thread } // 写入本地缓存(下次启动生效) LocalModuleStore.save(moduleId, info, dexFile) log("模块 $moduleId 下载完成,下次启动加载 ${info.version}") } catch (e: Exception) { log("模块 $moduleId 更新失败: ${e.message}") } }.start() } /** * ③ 用 DexClassLoader 加载 dex,扫描 @Page 注解 → 注册到路由表 */ private fun loadDex(moduleId: String, dexFile: File) { val parentLoader = this::class.java.classLoader val optimizedDir = File(dexFile.parent, "oat").apply { mkdirs() } val loader = DexClassLoader( dexFile.absolutePath, optimizedDir.absolutePath, null, parentLoader, ) loadedClassLoaders[moduleId] = loader // 调用 KSP 编译期生成的入口方法 // (真实 Kuikly 在每个模块里都会有 ModuleEntry.registerPages) try { val entryClass = loader.loadClass("com.example.kuikly.${moduleId}.ModuleEntry") val method = entryClass.getMethod("registerPages") method.invoke(null) log("模块 $moduleId Pager 注册成功") } catch (e: Exception) { log("模块 $moduleId 入口加载失败: ${e.message}") } } /** * 灰度判断:按设备 ID hash → 落在 0-99 的桶里 */ private fun isInRolloutPool(info: DynamicModuleInfo): Boolean { val deviceBucket = (DeviceInfo.deviceId().hashCode() and 0x7FFFFFFF) % 100 return deviceBucket < info.rolloutPercent } /** * SHA-256 签名校验 */ private fun verifyChecksum(file: File, expected: String): Boolean { val md = MessageDigest.getInstance("SHA-256") file.inputStream().use { ins -> val buf = ByteArray(8 * 1024) var n: Int while (ins.read(buf).also { n = it } > 0) md.update(buf, 0, n) } val actual = md.digest().joinToString("") { "%02x".format(it) } return actual.equals(expected, ignoreCase = true) } private fun downloadDex(info: DynamicModuleInfo): File? { // 用 OkHttp 下载到临时文件,省略具体实现 return null } private fun log(msg: String) { // 统一日志通道,方便统计 / 排错 println("[$TAG] $msg") } } // ─── 占位实现 ─── object LocalModuleStore { fun getLatest(moduleId: String): DynamicModuleInfo? = null fun getDexFile(moduleId: String, version: String): File = File("/tmp/$moduleId-$version.dex") fun save(moduleId: String, info: DynamicModuleInfo, dex: File) { } } object ConfigCenter { fun fetchModuleInfo(moduleId: String): DynamicModuleInfo? = null } object DeviceInfo { fun deviceId(): String = "device_xxx" }