package com.bitmart.exchange.module.loader import android.util.Log import com.facebook.react.bridge.Promise import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.bridge.ReactContextBaseJavaModule import com.facebook.react.bridge.ReactMethod import com.facebook.react.bridge.Arguments import com.facebook.react.bridge.JSBundleLoader import com.facebook.react.module.annotations.ReactModule import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.json.JSONObject import java.io.BufferedReader import java.io.File import java.io.FileReader import java.io.IOException import java.io.InputStreamReader /** * 简单的 Bundle 加载模块 * * 新架构下通过反射获取 ReactInstance,然后调用 loadJSBundle 方法 * 注意:ReactInstance 是 internal 类,必须完全通过反射操作 * * Bundle 加载优先级(解决 CodePush 增量更新只包含变化 bundle 的问题): * 1. CodePush 当前 package hash 目录(最新热更新的 bundle) * 2. CodePush 历史 package hash 目录(按时间从新到旧遍历) * 3. Assets(APK 内置的 bundle,最终 fallback) */ @ReactModule(name = ModuleLoaderModule.NAME) class ModuleLoaderModule(private val reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) { companion object { const val NAME = "ModuleLoader" private const val TAG = "ModuleLoaderModule" // CodePush 相关常量 private const val CODE_PUSH_FOLDER_PREFIX = "CodePush" private const val STATUS_FILE = "codepush.json" private const val CURRENT_PACKAGE_KEY = "currentPackage" // 缓存反射获取的类和方法,避免重复反射 private var reactHostImplClass: Class<*>? = null private var reactInstanceClass: Class<*>? = null init { try { reactHostImplClass = Class.forName("com.facebook.react.runtime.ReactHostImpl") reactInstanceClass = Class.forName("com.facebook.react.runtime.ReactInstance") } catch (e: ClassNotFoundException) { Log.w(TAG, "New architecture classes not found, will use old architecture") } } } // 协程作用域,用于异步操作 private val moduleScope = CoroutineScope(SupervisorJob() + Dispatchers.Main) override fun getName(): String = NAME /** * 加载子 bundle * * 加载策略(按优先级): * 1. 旧架构:CatalystInstance(当前 RN 0.79.5) * 2. 新架构:ReactHostImpl(完全 Bridgeless) */ @ReactMethod fun loadBusinessBundle(bundleId: String, bundlePath: String, promise: Promise) { Log.d(TAG, "loadBusinessBundle: bundleId=$bundleId, bundlePath=$bundlePath") // 1️⃣ 优先:旧架构 CatalystInstance if (tryLoadWithCatalystInstance(bundleId, bundlePath, promise)) { return } // 2️⃣ 其次:新架构 ReactHostImpl if (tryLoadWithReactHost(bundleId, bundlePath, promise)) { return } // 都失败 Log.e(TAG, "No suitable runtime found") promise.resolve(Arguments.createMap().apply { putBoolean("success", false) putString("errorMessage", "No CatalystInstance or ReactHostImpl available") }) } /** * 尝试使用旧架构 CatalystInstance 加载 * @return true 如果成功处理(包括成功加载或失败),false 如果 CatalystInstance 不可用 */ @Suppress("DEPRECATION") private fun tryLoadWithCatalystInstance(bundleId: String, bundlePath: String, promise: Promise): Boolean { val catalystInstance = reactContext.catalystInstance ?: return false Log.d(TAG, "Using CatalystInstance (same JS Runtime guaranteed)") try { // 1️⃣ 优先从 CodePush 目录加载(当前包 → 历史包) val codePushBundlePath = getCodePushBundlePath(bundlePath) if (codePushBundlePath != null) { Log.d(TAG, "Loading bundle from CodePush: $codePushBundlePath") catalystInstance.loadScriptFromFile(codePushBundlePath, codePushBundlePath, false) Log.d(TAG, "✓ Bundle loaded from CodePush: $bundleId") promise.resolve(Arguments.createMap().apply { putBoolean("success", true) }) return true } // 2️⃣ 回退到 assets 加载(APK 内置) val assetPath = "assets://$bundlePath" Log.d(TAG, "Loading bundle from assets: $assetPath") catalystInstance.loadScriptFromAssets(reactContext.assets, assetPath, false) Log.d(TAG, "✓ Bundle loaded from assets: $bundleId") promise.resolve(Arguments.createMap().apply { putBoolean("success", true) }) } catch (e: Exception) { Log.e(TAG, "Failed to load bundle: $bundleId", e) promise.resolve(Arguments.createMap().apply { putBoolean("success", false) putString("errorMessage", e.message ?: "Unknown error") }) } return true } /** * 尝试使用新架构 ReactHostImpl 加载 * @return true 如果成功处理,false 如果 ReactHostImpl 不可用 */ private fun tryLoadWithReactHost(bundleId: String, bundlePath: String, promise: Promise): Boolean { val application = reactContext.applicationContext as? android.app.Application val reactApplication = application as? com.facebook.react.ReactApplication ?: return false val reactHost = reactApplication.reactHost val hostImplClass = reactHostImplClass if (reactHost == null || hostImplClass == null || !hostImplClass.isInstance(reactHost)) { return false } Log.d(TAG, "Using ReactHostImpl (new architecture)") // 1️⃣ 优先从 CodePush 目录加载(当前包 → 历史包) val codePushBundlePath = getCodePushBundlePath(bundlePath) val bundleLoader = if (codePushBundlePath != null) { Log.d(TAG, "Loading bundle from CodePush: $codePushBundlePath") JSBundleLoader.createFileLoader(codePushBundlePath) } else { // 2️⃣ 回退到 assets 加载(APK 内置) val assetUrl = "assets://$bundlePath" Log.d(TAG, "Loading bundle from assets: $assetUrl") JSBundleLoader.createAssetLoader(reactContext, assetUrl, false) } loadBundleViaReflection(reactHost, bundleLoader, bundleId, promise) return true } /** * 新架构方案:完全通过反射获取 ReactInstance 并加载 bundle */ private fun loadBundleViaReflection( reactHost: Any, bundleLoader: JSBundleLoader, bundleId: String, promise: Promise ) { try { val hostImplClass = reactHostImplClass ?: throw Exception("ReactHostImpl class not found") val instanceClass = reactInstanceClass ?: throw Exception("ReactInstance class not found") // 通过反射获取 private 的 reactInstance 字段 val reactInstanceField = hostImplClass.getDeclaredField("reactInstance") reactInstanceField.isAccessible = true val reactInstance = reactInstanceField.get(reactHost) if (reactInstance == null) { Log.e(TAG, "ReactInstance is null") val result = Arguments.createMap().apply { putBoolean("success", false) putString("errorMessage", "ReactInstance not available") } promise.resolve(result) return } // 通过反射调用 ReactInstance.loadJSBundle 方法 val loadJSBundleMethod = instanceClass.getMethod("loadJSBundle", JSBundleLoader::class.java) loadJSBundleMethod.invoke(reactInstance, bundleLoader) Log.d(TAG, "Bundle loaded successfully via ReactInstance: $bundleId") val result = Arguments.createMap().apply { putBoolean("success", true) } promise.resolve(result) } catch (e: Exception) { Log.e(TAG, "Failed to load bundle via ReactInstance: $bundleId", e) val result = Arguments.createMap().apply { putBoolean("success", false) putString("errorMessage", e.message ?: "Unknown error") } promise.resolve(result) } } // ==================== CodePush Bundle Path Resolution ==================== /** * 获取 CodePush 基础目录 * 路径: /CodePush/ */ private fun getCodePushPath(): String { return "${reactContext.filesDir.absolutePath}/$CODE_PUSH_FOLDER_PREFIX" } /** * 获取当前 CodePush 包的 hash * 从 /codepush.json 读取 currentPackage 字段 */ private fun getCurrentPackageHash(): String? { return try { val statusFilePath = "${getCodePushPath()}/$STATUS_FILE" val statusFile = File(statusFilePath) if (!statusFile.exists()) { Log.d(TAG, "CodePush status file not found: $statusFilePath") return null } val content = statusFile.readText() val json = JSONObject(content) val packageHash = json.optString(CURRENT_PACKAGE_KEY, null) if (packageHash.isNullOrEmpty()) { Log.d(TAG, "No current package hash in CodePush status") return null } Log.d(TAG, "Current CodePush package hash: $packageHash") packageHash } catch (e: Exception) { Log.e(TAG, "Error reading CodePush status: ${e.message}") null } } /** * 获取 CodePush 包目录路径 * 路径: // */ private fun getPackageFolderPath(packageHash: String): String { return "${getCodePushPath()}/$packageHash" } /** * 从 CodePush 目录查找子 bundle 文件 * * 查找优先级(解决增量更新只包含变化 bundle 的问题): * 1. 当前 package hash 目录 * 2. 按时间从新到旧遍历其他 package hash 目录 * 3. 如果都找不到,返回 null,由调用方从 assets:// 加载 * * 每个目录内的查找顺序: * 1. /modules/ * 2. / * 3. / * * @param bundlePath 原始 bundle 路径,如 "modules/home.jsbundle" * @return 完整的文件路径,如果不存在返回 null(调用方会从 assets:// 加载) */ private fun getCodePushBundlePath(bundlePath: String): String? { val codePushDir = File(getCodePushPath()) // CodePush 目录不存在,直接返回 null(调用方会从 assets:// 加载) if (!codePushDir.exists()) { Log.d(TAG, "CodePush directory not found, will load from assets") return null } val bundleFileName = File(bundlePath).name val currentPackageHash = getCurrentPackageHash() // 1️⃣ 优先从当前 package hash 目录查找 if (currentPackageHash != null) { val currentPackageFolder = getPackageFolderPath(currentPackageHash) val foundPath = findBundleInFolder(currentPackageFolder, bundlePath, bundleFileName) if (foundPath != null) { Log.d(TAG, "Found bundle in current package: $foundPath") return foundPath } Log.d(TAG, "Bundle not found in current package ($currentPackageHash), searching other packages...") } // 2️⃣ 按时间从新到旧遍历其他 package hash 目录 val otherPackageFolders = getPackageFoldersSortedByTime(codePushDir, currentPackageHash) for (packageFolder in otherPackageFolders) { val foundPath = findBundleInFolder(packageFolder.absolutePath, bundlePath, bundleFileName) if (foundPath != null) { Log.d(TAG, "Found bundle in previous package (${packageFolder.name}): $foundPath") return foundPath } } Log.d(TAG, "Bundle not found in CodePush directories, will load from assets: $bundlePath") return null } /** * 在指定文件夹中查找 bundle 文件 * * 查找顺序: * 1. /modules/ * 2. / * 3. / */ private fun findBundleInFolder(folderPath: String, bundlePath: String, bundleFileName: String): String? { val folder = File(folderPath) if (!folder.exists() || !folder.isDirectory) { return null } val searchPaths = listOf( "$folderPath/modules/$bundleFileName", "$folderPath/$bundlePath", "$folderPath/$bundleFileName" ) for (path in searchPaths) { val file = File(path) if (file.exists() && file.isFile) { return path } } return null } /** * 获取所有 package 目录,按修改时间从新到旧排序 * 排除当前 package、assets-base 和 codepush.json 等非 package 目录/文件 */ private fun getPackageFoldersSortedByTime(codePushDir: File, currentPackageHash: String?): List { val excludedNames = setOf(STATUS_FILE, "download", "unzipped") return codePushDir.listFiles() ?.filter { file -> file.isDirectory && file.name != currentPackageHash && !excludedNames.contains(file.name) } ?.sortedByDescending { it.lastModified() } ?: emptyList() } // ==================== Bundle Manifest Support ==================== /** * 获取当前 bundle manifest 文件路径 * * 查找优先级: * 1. CodePush 当前包目录(热更新后的 manifest) * 2. 返回 null(调用方应使用 getCurrentBundleManifestContent 获取内容) * * 注意:如果需要从 assets 读取,请使用 getCurrentBundleManifestContent */ @ReactMethod fun getCurrentBundleManifest(promise: Promise) { try { // 优先从 CodePush 当前包目录获取 val packageHash = getCurrentPackageHash() if (packageHash != null) { val packageFolder = getPackageFolderPath(packageHash) val codePushManifestPath = "$packageFolder/bundle-manifest.json" val codePushManifestFile = File(codePushManifestPath) if (codePushManifestFile.exists()) { Log.d(TAG, "Found manifest in CodePush package: $codePushManifestPath") promise.resolve(codePushManifestPath) return } } // CodePush 中找不到,返回 null(调用方应使用 getCurrentBundleManifestContent) Log.d(TAG, "Manifest not in CodePush, use getCurrentBundleManifestContent to read from assets") promise.resolve(null) } catch (e: Exception) { Log.e(TAG, "Failed to get current bundle manifest: ${e.message}") promise.reject("MANIFEST_ERROR", "Failed to get manifest path", e) } } /** * 获取当前 bundle manifest 文件内容 * * 查找优先级: * 1. CodePush 当前包目录(热更新后的 manifest) * 2. APK 内置 assets(最终 fallback) */ @ReactMethod fun getCurrentBundleManifestContent(promise: Promise) { moduleScope.launch { try { val manifestContent = withContext(Dispatchers.IO) { // 1️⃣ 优先从 CodePush 当前包目录读取 val packageHash = getCurrentPackageHash() if (packageHash != null) { val packageFolder = getPackageFolderPath(packageHash) val codePushManifestPath = "$packageFolder/bundle-manifest.json" val codePushManifestFile = File(codePushManifestPath) if (codePushManifestFile.exists()) { val content = readFileContent(codePushManifestFile) if (content != null) { Log.d(TAG, "✓ Read bundle manifest from CodePush package") return@withContext content } } } // 2️⃣ 从 APK 内置 assets 读取 val assetsContent = readAssetContent("bundle-manifest.json") if (assetsContent != null) { Log.d(TAG, "✓ Read bundle manifest from assets") return@withContext assetsContent } Log.w(TAG, "Bundle manifest not found in any location") null } promise.resolve(manifestContent) } catch (e: Exception) { Log.e(TAG, "Failed to get bundle manifest content: ${e.message}") promise.reject("MANIFEST_CONTENT_ERROR", "Failed to read manifest content", e) } } } /** * 从 assets 读取文件内容 */ private fun readAssetContent(assetPath: String): String? { var reader: BufferedReader? = null return try { val inputStream = reactContext.assets.open(assetPath) reader = BufferedReader(InputStreamReader(inputStream)) val content = StringBuilder() var line: String? while (reader.readLine().also { line = it } != null) { content.append(line).append("\n") } content.toString() } catch (e: IOException) { Log.e(TAG, "Failed to read asset: $assetPath - ${e.message}") null } finally { try { reader?.close() } catch (e: IOException) { // Ignore } } } /** * 读取文件内容 */ private fun readFileContent(file: File): String? { var reader: BufferedReader? = null return try { reader = BufferedReader(FileReader(file)) val content = StringBuilder() var line: String? while (reader.readLine().also { line = it } != null) { content.append(line).append("\n") } content.toString() } catch (e: IOException) { Log.e(TAG, "Failed to read file: ${file.absolutePath} - ${e.message}") null } finally { try { reader?.close() } catch (e: IOException) { // Ignore } } } }