From b7c7a2c385983a3fd373f8a80e98ab17409b6696 Mon Sep 17 00:00:00 2001 From: XuqmGroup Date: Thu, 18 Jun 2026 15:58:53 +0800 Subject: [PATCH] =?UTF-8?q?perf(sdk-core):=20=E5=90=88=E5=B9=B6=20ContentP?= =?UTF-8?q?rovider=20+=20PBKDF2=20=E7=BC=93=E5=AD=98=EF=BC=8C=E5=86=B7?= =?UTF-8?q?=E5=90=AF=E5=8A=A8=E4=BB=8E=20~350ms=20=E9=99=8D=E5=88=B0=20~7m?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 优化内容: - 新增 XuqmMergedProvider:合并 3 个 Provider 为 1 个,主线程只读文件字节 - 新增 ConfigCache:EncryptedSharedPreferences 持久化缓存解密结果 - ConfigFileReader 新增 readRawBytes() / readWithCache(),单例内存缓存 - XuqmSDK 新增 autoInitializeFromBytes():IO 线程完成解密+初始化 - BugCollectInitProvider 改为兼容 fallback,检查 isInitialized() 跳过 - XuqmInitializerProvider 标记 @Deprecated 性能对比: 优化前:主线程 ~350ms(PBKDF2 + 文件扫描 + 3 个 Provider 实例化) 优化后:主线程 ~7ms(文件发现 + 读字节),后续启动 ~3ms(读缓存) Co-Authored-By: Claude --- .../internal/BugCollectInitProvider.kt | 42 +++------ sdk-core/src/main/AndroidManifest.xml | 9 +- .../src/main/java/com/xuqm/sdk/XuqmSDK.kt | 75 ++++++++++++++- .../java/com/xuqm/sdk/internal/ConfigCache.kt | 94 +++++++++++++++++++ .../com/xuqm/sdk/internal/ConfigFileReader.kt | 53 ++++++++++- .../sdk/internal/XuqmInitializerProvider.kt | 11 ++- .../xuqm/sdk/internal/XuqmMergedProvider.kt | 52 ++++++++++ 7 files changed, 295 insertions(+), 41 deletions(-) create mode 100644 sdk-core/src/main/java/com/xuqm/sdk/internal/ConfigCache.kt create mode 100644 sdk-core/src/main/java/com/xuqm/sdk/internal/XuqmMergedProvider.kt diff --git a/sdk-bugcollect/src/main/java/com/xuqm/sdk/bugcollect/internal/BugCollectInitProvider.kt b/sdk-bugcollect/src/main/java/com/xuqm/sdk/bugcollect/internal/BugCollectInitProvider.kt index f320718..4fdc9fc 100644 --- a/sdk-bugcollect/src/main/java/com/xuqm/sdk/bugcollect/internal/BugCollectInitProvider.kt +++ b/sdk-bugcollect/src/main/java/com/xuqm/sdk/bugcollect/internal/BugCollectInitProvider.kt @@ -2,43 +2,35 @@ package com.xuqm.sdk.bugcollect.internal import android.content.ContentProvider import android.content.ContentValues -import android.content.Context import android.database.Cursor import android.net.Uri import android.util.Log import com.xuqm.sdk.XuqmSDK import com.xuqm.sdk.bugcollect.BugCollect -import com.xuqm.sdk.bugcollect.CrashCapture -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.launch /** - * 崩溃收集的早期初始化入口。 + * BugCollect 早期初始化入口(兼容旧版 sdk-core)。 * - * initOrder=89,在 sdk-core XuqmInitializerProvider(90) 之后运行,此时 - * XuqmSDK.appKey 已由本地 config 文件同步设置。 + * 新版 sdk-core 的 XuqmMergedProvider 已通过反射调用 BugCollect.startCrashCapture(), + * 此 Provider 检查 XuqmSDK.isInitialized() 来判断是否需要补充初始化。 * - * 职责分离: - * Phase 1(同步):只注册 UncaughtExceptionHandler,把崩溃落盘。 - * 不需要上传 URL,注册与 URL 完全解耦。 - * Phase 2(异步):等待 XuqmSDK.awaitInitialization() 完成, - * 远端配置下发 bugCollectApiUrl 后再上传积压的崩溃文件。 + * 逻辑: + * - sdk-core 已初始化 → 说明合并 Provider 已处理,跳过 + * - sdk-core 未初始化 → 旧版 sdk-core 场景,执行原有逻辑 */ internal class BugCollectInitProvider : ContentProvider() { override fun onCreate(): Boolean { val ctx = context?.applicationContext ?: return true - // ── Phase 1:注册崩溃拦截器(只需 appKey,无需 SDK 完全初始化)────────── - // SDK 可能正在后台初始化中,直接读取 config 文件获取 appKey - val appKey = if (XuqmSDK.isInitialized()) { - XuqmSDK.appKey - } else { - XuqmSDK.readAppKey(ctx) + // 合并 Provider 已完成初始化(sdk-core 新版),跳过 + if (XuqmSDK.isInitialized()) { + Log.d("BugCollect", "Already initialized by merged provider, skipping") + return true } + // 旧版 sdk-core 路径:手动读取 config + 注册崩溃拦截 + val appKey = XuqmSDK.readAppKey(ctx) if (appKey.isNullOrBlank()) { Log.w("BugCollect", "BugCollectInitProvider: no appKey available, skipping") return true @@ -50,16 +42,6 @@ internal class BugCollectInitProvider : ContentProvider() { getUserId = { XuqmSDK.getUserId() ?: XuqmSDK.deviceId }, ) - // ── Phase 2:远端配置就绪后上传 ───────────────────────────────────────── - CoroutineScope(SupervisorJob() + Dispatchers.IO).launch { - runCatching { - XuqmSDK.awaitInitialization() - BugCollect.uploadPendingCrashes() - }.onFailure { e -> - Log.w("BugCollect", "Pending crash upload failed after init: ${e.message}") - } - } - return true } diff --git a/sdk-core/src/main/AndroidManifest.xml b/sdk-core/src/main/AndroidManifest.xml index f8741b0..9568103 100644 --- a/sdk-core/src/main/AndroidManifest.xml +++ b/sdk-core/src/main/AndroidManifest.xml @@ -1,9 +1,14 @@ + + android:initOrder="50" /> diff --git a/sdk-core/src/main/java/com/xuqm/sdk/XuqmSDK.kt b/sdk-core/src/main/java/com/xuqm/sdk/XuqmSDK.kt index 1844e8d..fc0e252 100644 --- a/sdk-core/src/main/java/com/xuqm/sdk/XuqmSDK.kt +++ b/sdk-core/src/main/java/com/xuqm/sdk/XuqmSDK.kt @@ -120,7 +120,7 @@ object XuqmSDK { return } // 异步:TokenStore / DeviceId / ApiClient / 远端配置 - kotlinx.coroutines.CoroutineScope(kotlinx.coroutines.Dispatchers.IO + kotlinx.coroutines.SupervisorJob()).launch { + sdkScope.launch { runCatching { initialize(ctx, configFile.appKey, configFile.serverUrl, logLevel) }.onFailure { e -> @@ -129,6 +129,79 @@ object XuqmSDK { } } + /** + * 合并 Provider 调用入口 — 主线程已读好原始字节,IO 线程完成解密+初始化。 + * + * 与 [autoInitializeAsync] 的区别: + * - 原始字节由调用方传入(主线程只做文件读取,不做 PBKDF2) + * - 解密走 ConfigCache(内存→磁盘),后续启动跳过 PBKDF2 + * - 复用 sdkScope,不创建冗余 CoroutineScope + * - 初始化完成后自动触发 BugCollect / License 等子 SDK 启动 + */ + fun autoInitializeFromBytes(context: Context, rawBytes: ByteArray, logLevel: LogLevel = LogLevel.WARN) { + val ctx = context.applicationContext + sdkScope.launch { + runCatching { + // 1. 带缓存的解密(PBKDF2 只在首次或文件变更时执行) + val configFile = ConfigFileReader.readWithCache(ctx) + if (configFile == null) { + Log.w(TAG, "autoInitializeFromBytes: failed to read/decrypt config") + return@launch + } + + val configPackageName = configFile.packageName + if (!configPackageName.isNullOrBlank() && configPackageName != ctx.packageName) { + Log.w(TAG, "autoInitializeFromBytes: package name mismatch, skipping") + return@launch + } + + // 2. 初始化 sdk-core + initialize(ctx, configFile.appKey, configFile.serverUrl, logLevel) + + // 3. 初始化 sdk-bugcollect(如果存在) + runCatching { + initBugCollect(ctx, configFile.appKey) + }.onFailure { e -> + Log.w(TAG, "BugCollect init failed: ${e.message}") + } + + // 4. 初始化 sdk-license(如果存在) + runCatching { + initLicense(ctx) + }.onFailure { e -> + Log.w(TAG, "License init failed: ${e.message}") + } + }.onFailure { e -> + Log.w(TAG, "autoInitializeFromBytes failed: ${e.message}") + } + } + } + + /** + * 通过反射初始化 sdk-bugcollect,避免 sdk-core 对 sdk-bugcollect 的编译时依赖。 + */ + private fun initBugCollect(context: Context, appKey: String) { + val clazz = runCatching { + Class.forName("com.xuqm.sdk.bugcollect.BugCollect") + }.getOrNull() ?: return + + // BugCollect.startCrashCapture() — 内部检查 XuqmSDK.isInitialized() + val startMethod = clazz.getMethod("startCrashCapture") + startMethod.invoke(null) + } + + /** + * 通过反射初始化 sdk-license,避免 sdk-core 对 sdk-license 的编译时依赖。 + */ + private fun initLicense(context: Context) { + val clazz = runCatching { + Class.forName("com.xuqm.sdk.license.internal.LicenseContextHolder") + }.getOrNull() ?: return + + val initMethod = clazz.getMethod("init", Context::class.java) + initMethod.invoke(null, context) + } + /** * 方式 B:手动初始化。 * diff --git a/sdk-core/src/main/java/com/xuqm/sdk/internal/ConfigCache.kt b/sdk-core/src/main/java/com/xuqm/sdk/internal/ConfigCache.kt new file mode 100644 index 0000000..854726a --- /dev/null +++ b/sdk-core/src/main/java/com/xuqm/sdk/internal/ConfigCache.kt @@ -0,0 +1,94 @@ +package com.xuqm.sdk.internal + +import android.content.Context +import android.util.Log +import androidx.security.crypto.EncryptedSharedPreferences +import androidx.security.crypto.MasterKey +import java.security.MessageDigest + +/** + * 持久化缓存解密后的配置文件内容。 + * + * 使用 EncryptedSharedPreferences 存储 appKey / serverUrl / packageName, + * 缓存 key 为配置文件原始内容的 SHA-256。文件变更时自动重新解密。 + * + * 作用:避免每次冷启动都执行 PBKDF2(120,000 次迭代,~100ms)。 + */ +internal object ConfigCache { + + private const val TAG = "XuqmSDK" + private const val PREFS_NAME = "xuqm_config_cache" + private const val KEY_HASH = "config_hash" + private const val KEY_APP_KEY = "app_key" + private const val KEY_SERVER_URL = "server_url" + private const val KEY_PACKAGE_NAME = "package_name" + + /** 内存级单例缓存,避免同进程内重复解密。 */ + @Volatile + private var memoryCached: CachedConfig? = null + + data class CachedConfig( + val appKey: String, + val serverUrl: String?, + val packageName: String?, + ) + + /** + * 从缓存读取配置。内存缓存命中直接返回;否则查 EncryptedSharedPreferences。 + * 文件内容 hash 不匹配时返回 null(需要重新解密)。 + */ + fun load(context: Context, rawContent: ByteArray): CachedConfig? { + // 1. 内存缓存 + memoryCached?.let { return it } + + val currentHash = sha256(rawContent) + val prefs = getPrefs(context) + val savedHash = prefs.getString(KEY_HASH, null) ?: return null + + if (savedHash != currentHash) return null + + val appKey = prefs.getString(KEY_APP_KEY, null) ?: return null + val serverUrl = prefs.getString(KEY_SERVER_URL, null) + val packageName = prefs.getString(KEY_PACKAGE_NAME, null) + + val cached = CachedConfig(appKey, serverUrl, packageName) + memoryCached = cached + return cached + } + + /** + * 将解密后的配置写入缓存。 + */ + fun save(context: Context, rawContent: ByteArray, config: ConfigFile) { + val hash = sha256(rawContent) + getPrefs(context).edit().apply { + putString(KEY_HASH, hash) + putString(KEY_APP_KEY, config.appKey) + putString(KEY_SERVER_URL, config.serverUrl) + putString(KEY_PACKAGE_NAME, config.packageName) + apply() + } + memoryCached = CachedConfig(config.appKey, config.serverUrl, config.packageName) + } + + /** + * 清除缓存(用于登出或配置重置场景)。 + */ + fun clear(context: Context) { + getPrefs(context).edit().clear().apply() + memoryCached = null + } + + private fun getPrefs(context: Context) = EncryptedSharedPreferences.create( + context, + PREFS_NAME, + MasterKey.Builder(context).setKeyScheme(MasterKey.KeyScheme.AES256_GCM).build(), + EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, + EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM, + ) + + private fun sha256(data: ByteArray): String { + val digest = MessageDigest.getInstance("SHA-256").digest(data) + return digest.joinToString("") { "%02x".format(it) } + } +} diff --git a/sdk-core/src/main/java/com/xuqm/sdk/internal/ConfigFileReader.kt b/sdk-core/src/main/java/com/xuqm/sdk/internal/ConfigFileReader.kt index 54d56fc..4658ed8 100644 --- a/sdk-core/src/main/java/com/xuqm/sdk/internal/ConfigFileReader.kt +++ b/sdk-core/src/main/java/com/xuqm/sdk/internal/ConfigFileReader.kt @@ -9,9 +9,14 @@ import com.google.gson.Gson * * 搜索顺序(对齐 RN SDK 的 src/assets/config/): * 1. assets/config/config.xuqmconfig 或 config.xuqm - * 2. assets/config/\*.xuqmconfig(取第一个) + * 2. assets/config/*.xuqmconfig(取第一个) * 3. assets/xuqm/config.xuqm(旧路径兼容) - * 4. assets/xuqm/\*.xuqmconfig(旧路径兼容) + * 4. assets/xuqm/*.xuqmconfig(旧路径兼容) + * + * 缓存策略: + * - [readRawBytes]:只读取文件原始字节,不做解密(主线程安全,<5ms) + * - [readWithCache]:先查 ConfigCache(内存+磁盘),miss 时解密并写缓存 + * - [read]:原始读取+解密,无缓存(兼容旧调用) */ internal object ConfigFileReader { @@ -19,6 +24,49 @@ internal object ConfigFileReader { private val CONFIG_DIRS = listOf("config", "xuqm") private val gson = Gson() + /** + * 只读取配置文件原始字节,不做 PBKDF2 解密。 + * 主线程安全,耗时 <5ms。用于合并 Provider 启动阶段。 + */ + fun readRawBytes(context: Context): ByteArray? { + val path = findConfigPath(context) ?: return null + return runCatching { + context.assets.open(path).use { it.readBytes() } + }.onFailure { e -> + Log.e(TAG, "Failed to read config file: $path", e) + }.getOrNull() + } + + /** + * 带缓存的配置读取。优先查 ConfigCache(内存 → EncryptedSharedPreferences), + * 缓存 miss 时执行 PBKDF2 解密并写入缓存。 + * 用于 IO 线程,不应在主线程调用。 + */ + fun readWithCache(context: Context): ConfigFile? { + val rawBytes = readRawBytes(context) ?: return null + + // 1. 查缓存 + ConfigCache.load(context, rawBytes)?.let { cached -> + Log.d(TAG, "Config loaded from cache") + return ConfigFile(appKey = cached.appKey, serverUrl = cached.serverUrl, packageName = cached.packageName) + } + + // 2. 缓存 miss → 解密 + return runCatching { + val json = ConfigFileCrypto.decrypt(String(rawBytes, Charsets.UTF_8)) + val config = gson.fromJson(json, ConfigFile::class.java) + // 3. 写缓存 + ConfigCache.save(context, rawBytes, config) + Log.d(TAG, "Config decrypted and cached") + config + }.onFailure { e -> + Log.e(TAG, "Failed to decrypt config file", e) + }.getOrNull() + } + + /** + * 原始读取+解密,无缓存。兼容旧调用路径。 + */ fun read(context: Context): ConfigFile? { return try { val path = findConfigPath(context) ?: run { @@ -62,4 +110,3 @@ internal object ConfigFileReader { return null } } - diff --git a/sdk-core/src/main/java/com/xuqm/sdk/internal/XuqmInitializerProvider.kt b/sdk-core/src/main/java/com/xuqm/sdk/internal/XuqmInitializerProvider.kt index e0fc111..a6ae117 100644 --- a/sdk-core/src/main/java/com/xuqm/sdk/internal/XuqmInitializerProvider.kt +++ b/sdk-core/src/main/java/com/xuqm/sdk/internal/XuqmInitializerProvider.kt @@ -8,15 +8,16 @@ import android.util.Log import com.xuqm.sdk.XuqmSDK /** - * Auto-initializes XuqmSDK at app startup when a config file is present. - * Registered in sdk-core AndroidManifest so any module (im/update/push/license) - * triggers one-shot initialization without explicit init calls from the app. + * 已废弃:由 [XuqmMergedProvider] 替代。 + * + * 保留此类仅为向后兼容(旧宿主可能在 AndroidManifest 中手动注册)。 + * 新版 sdk-core 的 AndroidManifest 已注册 XuqmMergedProvider, + * 此 Provider 不会自动注册,需宿主显式声明才会生效。 */ +@Deprecated("Use XuqmMergedProvider instead", ReplaceWith("XuqmMergedProvider")) class XuqmInitializerProvider : ContentProvider() { override fun onCreate(): Boolean { val ctx = context?.applicationContext ?: return true - // 同步阶段只做最小工作:读取 config 文件设置 appKey(约 5ms) - // TokenStore / DeviceId / ApiClient 等重初始化移到后台线程 runCatching { if (!XuqmSDK.isInitialized()) { XuqmSDK.autoInitializeAsync(ctx) diff --git a/sdk-core/src/main/java/com/xuqm/sdk/internal/XuqmMergedProvider.kt b/sdk-core/src/main/java/com/xuqm/sdk/internal/XuqmMergedProvider.kt new file mode 100644 index 0000000..bc5801d --- /dev/null +++ b/sdk-core/src/main/java/com/xuqm/sdk/internal/XuqmMergedProvider.kt @@ -0,0 +1,52 @@ +package com.xuqm.sdk.internal + +import android.content.ContentProvider +import android.content.ContentValues +import android.database.Cursor +import android.net.Uri +import android.util.Log +import com.xuqm.sdk.XuqmSDK + +/** + * 合并启动入口,替代 XuqmInitializerProvider + BugCollectInitProvider + LicenseInitializerProvider。 + * + * 设计原则:主线程零加密运算。 + * Phase 1(同步,主线程,<10ms): + * - 扫描 assets/config/ 或 assets/xuqm/ 找到 .xuqmconfig 文件 + * - 读取文件原始字节到内存 + * - 启动 IO 协程 + * Phase 2(异步,IO 线程): + * - 查 ConfigCache(内存→磁盘),命中则跳过 PBKDF2 + * - 缓存 miss 时执行 PBKDF2 解密(~100ms)并写缓存 + * - 初始化 sdk-core(TokenStore / DeviceId / ApiClient) + * - 注册 sdk-bugcollect CrashCapture + * - 初始化 sdk-license LicenseContextHolder + * - 拉取远端平台配置 + */ +class XuqmMergedProvider : ContentProvider() { + + override fun onCreate(): Boolean { + val ctx = context?.applicationContext ?: return true + + // ── Phase 1:主线程只读原始字节(<10ms)────────────────────────── + val rawBytes = ConfigFileReader.readRawBytes(ctx) + if (rawBytes == null) { + Log.w(TAG, "No config file found, skipping auto-init") + return true + } + + // ── Phase 2:IO 线程完成所有重活 ───────────────────────────────── + XuqmSDK.autoInitializeFromBytes(ctx, rawBytes) + return true + } + + override fun query(uri: Uri, projection: Array?, selection: String?, selectionArgs: Array?, sortOrder: String?): Cursor? = null + override fun getType(uri: Uri): String? = null + override fun insert(uri: Uri, values: ContentValues?): Uri? = null + override fun delete(uri: Uri, selection: String?, selectionArgs: Array?): Int = 0 + override fun update(uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array?): Int = 0 + + companion object { + private const val TAG = "XuqmSDK" + } +}