From 54b3163f53772bd0cc3b7e9dbbd024ea91bc41b2 Mon Sep 17 00:00:00 2001 From: XuqmGroup Date: Thu, 18 Jun 2026 10:11:08 +0800 Subject: [PATCH] =?UTF-8?q?refactor(bugcollect):=20=E5=B4=A9=E6=BA=83?= =?UTF-8?q?=E6=94=B6=E9=9B=86=E4=B8=8E=E4=B8=8A=E4=BC=A0=E5=88=86=E7=A6=BB?= =?UTF-8?q?=EF=BC=8Csdk-core=20=E4=BF=9D=E6=8C=81=E7=8B=AC=E7=AB=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 设计问题: - 之前用反射在 XuqmInitializerProvider 里调用 BugCollect,sdk-core 隐式依赖 sdk-bugcollect - startCrashCapture 同时负责注册拦截器和触发上传,两者生命周期不同 新架构: - sdk-core 完全干净,XuqmInitializerProvider 还原为只调用 autoInitialize - sdk-bugcollect 自注册 BugCollectInitProvider(initOrder=89) - Phase 1(同步):core init 完成后立即注册 UncaughtExceptionHandler,用 platformUrl 派生兜底 URL,落盘不依赖远端配置 - Phase 2(异步):awaitInitialization() 后用最新 bugCollectApiUrl 上传积压崩溃 - BugCollect.uploadPendingCrashes() 公开,供 Phase 2 调用 - startCrashCapture() 保持幂等,app 侧可选择性调用,不影响自动流程 Co-Authored-By: Claude Sonnet 4.6 --- sdk-bugcollect/src/main/AndroidManifest.xml | 14 ++++ .../com/xuqm/sdk/bugcollect/BugCollect.kt | 12 ++- .../internal/BugCollectInitProvider.kt | 75 +++++++++++++++++++ .../sdk/internal/XuqmInitializerProvider.kt | 14 ---- 4 files changed, 99 insertions(+), 16 deletions(-) create mode 100644 sdk-bugcollect/src/main/AndroidManifest.xml create mode 100644 sdk-bugcollect/src/main/java/com/xuqm/sdk/bugcollect/internal/BugCollectInitProvider.kt diff --git a/sdk-bugcollect/src/main/AndroidManifest.xml b/sdk-bugcollect/src/main/AndroidManifest.xml new file mode 100644 index 0000000..ff7a1f0 --- /dev/null +++ b/sdk-bugcollect/src/main/AndroidManifest.xml @@ -0,0 +1,14 @@ + + + + + + + diff --git a/sdk-bugcollect/src/main/java/com/xuqm/sdk/bugcollect/BugCollect.kt b/sdk-bugcollect/src/main/java/com/xuqm/sdk/bugcollect/BugCollect.kt index 70cab38..1ee6442 100644 --- a/sdk-bugcollect/src/main/java/com/xuqm/sdk/bugcollect/BugCollect.kt +++ b/sdk-bugcollect/src/main/java/com/xuqm/sdk/bugcollect/BugCollect.kt @@ -129,11 +129,14 @@ object BugCollect { event("__log_info", tags + ("message" to message)) } + /** + * 注册全局崩溃拦截器(幂等)。 + * 通常由 BugCollectInitProvider 在 app 启动时自动调用,无需 app 代码主动触发。 + * 如需在特定时机手动注册,调用此方法也是安全的。 + */ fun startCrashCapture() { if (crashCaptureStarted) return if (!XuqmSDK.isInitialized()) return - // bugCollectApiUrl 由远端配置下发,启动时可能尚未到达。 - // 用 platformUrl 推导兜底地址:崩溃文件里会保存此 URL,下次启动时用它上报。 val logApiUrl = XuqmSDK.bugCollectApiUrl ?: (XuqmSDK.platformUrl.trimEnd('/') + "/api/bugcollect/") crashCaptureStarted = true @@ -146,6 +149,11 @@ object BugCollect { queue().uploadPendingCrashes() } + /** 上传上次崩溃落盘的文件(远端配置就绪后由 BugCollectInitProvider 自动调用)。 */ + fun uploadPendingCrashes() { + if (isReady()) queue().uploadPendingCrashes() + } + fun defineFunnel(id: String, steps: List) { FunnelTracker.define(id, steps) } 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 new file mode 100644 index 0000000..d139c78 --- /dev/null +++ b/sdk-bugcollect/src/main/java/com/xuqm/sdk/bugcollect/internal/BugCollectInitProvider.kt @@ -0,0 +1,75 @@ +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 + +/** + * 崩溃收集的早期初始化入口。 + * + * initOrder=89,在 sdk-core XuqmInitializerProvider(90) 之后运行,此时: + * - XuqmSDK.appKey / platformUrl 已由本地 config 文件同步设置 + * - 远端平台配置尚未到达(bugCollectApiUrl 可能还是 null) + * + * 职责: + * 1. 立即注册 UncaughtExceptionHandler,把崩溃落盘(只需 appKey + 派生 URL) + * 2. 后台等待 XuqmSDK.awaitInitialization() 完成后,上传积压的崩溃文件 + * + * sdk-core 不依赖本类,XuqmSDK 可独立使用。 + */ +internal class BugCollectInitProvider : ContentProvider() { + + override fun onCreate(): Boolean { + val ctx = context?.applicationContext ?: return true + + if (!XuqmSDK.isInitialized()) { + // core 未初始化(config 文件缺失等),跳过 + Log.w("BugCollect", "BugCollectInitProvider: XuqmSDK not initialized, skipping crash capture setup") + return true + } + + // ── Phase 1: 立即注册崩溃拦截,落盘不需要远端配置 ────────────────────── + val logApiUrl = XuqmSDK.bugCollectApiUrl + ?: (XuqmSDK.platformUrl.trimEnd('/') + "/api/bugcollect/") + + CrashCapture.start( + appContext = ctx, + logApiUrl = logApiUrl, + appKey = XuqmSDK.appKey, + getUserId = { XuqmSDK.getUserId() }, + ) + + // ── Phase 2: 等远端配置就绪后上传积压的崩溃文件 ───────────────────────── + uploadAfterInit(ctx) + + return true + } + + private fun uploadAfterInit(ctx: Context) { + CoroutineScope(SupervisorJob() + Dispatchers.IO).launch { + runCatching { + XuqmSDK.awaitInitialization() + // 远端配置到达后 bugCollectApiUrl 已更新,用最新 URL 上传 + BugCollect.uploadPendingCrashes() + }.onFailure { e -> + Log.w("BugCollect", "Failed to upload pending crashes after init: ${e.message}") + } + } + } + + override fun query(uri: Uri, p: Array?, s: String?, sa: Array?, so: 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 +} 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 f17b8c9..819af2d 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 @@ -19,26 +19,12 @@ class XuqmInitializerProvider : ContentProvider() { if (!XuqmSDK.isInitialized()) { XuqmSDK.autoInitialize(ctx) } - // 在 SDK 初始化后立即启动崩溃捕获,确保任何后续崩溃都能被落盘。 - // startCrashCapture 内部使用 platformUrl 兜底,无需等远端配置到达。 - startCrashCaptureIfAvailable() }.onFailure { e -> Log.w("XuqmSDK", "Auto-initialization skipped: ${e.message}") } return true } - private fun startCrashCaptureIfAvailable() { - runCatching { - val bugCollect = Class.forName("com.xuqm.sdk.bugcollect.BugCollect") - val companion = bugCollect.getField("INSTANCE").get(null) - val method = bugCollect.getMethod("startCrashCapture") - method.invoke(companion) - }.onFailure { - // sdk-bugcollect 未依赖时跳过,不影响其他功能 - } - } - 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