fix(bugcollect): 修复 fatal crash 不上报——startCrashCapture 从未被调用

根本原因链:
1. XuqmInitializerProvider 只调用 XuqmSDK.autoInitialize,从不调用 startCrashCapture
2. 即使调用,startCrashCapture 检查 bugCollectApiUrl == null 即提前返回;
   而 bugCollectApiUrl 由远端配置下发,崩溃发生时往往尚未到达
3. UncaughtExceptionHandler 从未注册,崩溃文件没有写入,下次启动无数据可上报

修复:
- XuqmSDK 新增 platformUrl 属性(同步可用,初始化即有值)
- BugCollect.startCrashCapture 不再依赖 bugCollectApiUrl,
  用 platformUrl + "/api/bugcollect/" 作为兜底 URL 写入崩溃文件
- LogQueue.uploadPendingCrashes 优先使用崩溃文件内保存的 logApiUrl
- XuqmInitializerProvider 通过反射调用 BugCollect.startCrashCapture,
  在 SDK 初始化后立即注册 UncaughtExceptionHandler

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
这个提交包含在:
XuqmGroup 2026-06-18 10:05:43 +08:00
父节点 e300cbdb83
当前提交 cc1557daf0
共有 4 个文件被更改,包括 26 次插入3 次删除

查看文件

@ -131,11 +131,15 @@ object BugCollect {
fun startCrashCapture() { fun startCrashCapture() {
if (crashCaptureStarted) return if (crashCaptureStarted) return
val bugCollectApiUrl = XuqmSDK.bugCollectApiUrl ?: return if (!XuqmSDK.isInitialized()) return
// bugCollectApiUrl 由远端配置下发,启动时可能尚未到达。
// 用 platformUrl 推导兜底地址:崩溃文件里会保存此 URL,下次启动时用它上报。
val logApiUrl = XuqmSDK.bugCollectApiUrl
?: (XuqmSDK.platformUrl.trimEnd('/') + "/api/bugcollect/")
crashCaptureStarted = true crashCaptureStarted = true
CrashCapture.start( CrashCapture.start(
appContext = XuqmSDK.appContext, appContext = XuqmSDK.appContext,
logApiUrl = bugCollectApiUrl, logApiUrl = logApiUrl,
appKey = XuqmSDK.appKey, appKey = XuqmSDK.appKey,
getUserId = { XuqmSDK.getUserId() }, getUserId = { XuqmSDK.getUserId() },
) )

查看文件

@ -94,7 +94,9 @@ internal class LogQueue(
userId = obj.optNullableString("userId"), userId = obj.optNullableString("userId"),
user = IssueEvent.UserInfo(id = obj.optNullableString("userId")), user = IssueEvent.UserInfo(id = obj.optNullableString("userId")),
) )
LogUploader.uploadIssue(logApiUrl, event) // 优先使用崩溃文件里保存的 URL崩溃时可能远端配置未到达
val uploadUrl = obj.optString("logApiUrl").takeIf { it.isNotBlank() } ?: logApiUrl
LogUploader.uploadIssue(uploadUrl, event)
file.delete() file.delete()
}.onFailure { e -> }.onFailure { e ->
Log.e(TAG, "Failed to upload crash: ${file.name}", e) Log.e(TAG, "Failed to upload crash: ${file.name}", e)

查看文件

@ -40,6 +40,9 @@ object XuqmSDK {
private var initialized = false private var initialized = false
@Volatile private var initializedAppKey: String? = null @Volatile private var initializedAppKey: String? = null
@Volatile private var resolvedPlatformUrl: String = DEFAULT_PLATFORM_URL @Volatile private var resolvedPlatformUrl: String = DEFAULT_PLATFORM_URL
/** 初始化时使用的平台地址(同步可用,远端配置到达前即有值)。 */
val platformUrl: String get() = resolvedPlatformUrl
@Volatile private var loginSession: XuqmLoginSession? = null @Volatile private var loginSession: XuqmLoginSession? = null
@Volatile private var userInfoValue: XuqmUserInfo? = null @Volatile private var userInfoValue: XuqmUserInfo? = null
@Volatile var platformConfig: SdkPlatformConfig? = null @Volatile var platformConfig: SdkPlatformConfig? = null

查看文件

@ -19,12 +19,26 @@ class XuqmInitializerProvider : ContentProvider() {
if (!XuqmSDK.isInitialized()) { if (!XuqmSDK.isInitialized()) {
XuqmSDK.autoInitialize(ctx) XuqmSDK.autoInitialize(ctx)
} }
// 在 SDK 初始化后立即启动崩溃捕获,确保任何后续崩溃都能被落盘。
// startCrashCapture 内部使用 platformUrl 兜底,无需等远端配置到达。
startCrashCaptureIfAvailable()
}.onFailure { e -> }.onFailure { e ->
Log.w("XuqmSDK", "Auto-initialization skipped: ${e.message}") Log.w("XuqmSDK", "Auto-initialization skipped: ${e.message}")
} }
return true 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<out String>?, selection: String?, selectionArgs: Array<out String>?, sortOrder: String?): Cursor? = null override fun query(uri: Uri, projection: Array<out String>?, selection: String?, selectionArgs: Array<out String>?, sortOrder: String?): Cursor? = null
override fun getType(uri: Uri): String? = null override fun getType(uri: Uri): String? = null
override fun insert(uri: Uri, values: ContentValues?): Uri? = null override fun insert(uri: Uri, values: ContentValues?): Uri? = null