refactor(bugcollect): 崩溃收集与上传分离,sdk-core 保持独立

设计问题:
- 之前用反射在 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 <noreply@anthropic.com>
这个提交包含在:
XuqmGroup 2026-06-18 10:11:08 +08:00
父节点 cc1557daf0
当前提交 54b3163f53
共有 4 个文件被更改,包括 99 次插入16 次删除

查看文件

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<!--
initOrder=89 < sdk-core XuqmInitializerProvider(90) core
BugCollect 能读到 XuqmSDK.appKey / platformUrl 后立即注册 UncaughtExceptionHandler。
-->
<provider
android:name="com.xuqm.sdk.bugcollect.internal.BugCollectInitProvider"
android:authorities="${applicationId}.xuqm-bugcollect-init"
android:exported="false"
android:initOrder="89" />
</application>
</manifest>

查看文件

@ -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<String>) {
FunnelTracker.define(id, steps)
}

查看文件

@ -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<out String>?, s: String?, sa: Array<out String>?, 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<out String>?): Int = 0
override fun update(uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array<out String>?): Int = 0
}

查看文件

@ -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<out String>?, selection: String?, selectionArgs: Array<out String>?, sortOrder: String?): Cursor? = null
override fun getType(uri: Uri): String? = null
override fun insert(uri: Uri, values: ContentValues?): Uri? = null