perf(sdk-core): 合并 ContentProvider + PBKDF2 缓存,冷启动从 ~350ms 降到 ~7ms
优化内容: - 新增 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 <noreply@anthropic.com>
这个提交包含在:
父节点
1f1c0b4018
当前提交
b7c7a2c385
@ -2,43 +2,35 @@ package com.xuqm.sdk.bugcollect.internal
|
|||||||
|
|
||||||
import android.content.ContentProvider
|
import android.content.ContentProvider
|
||||||
import android.content.ContentValues
|
import android.content.ContentValues
|
||||||
import android.content.Context
|
|
||||||
import android.database.Cursor
|
import android.database.Cursor
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.xuqm.sdk.XuqmSDK
|
import com.xuqm.sdk.XuqmSDK
|
||||||
import com.xuqm.sdk.bugcollect.BugCollect
|
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) 之后运行,此时
|
* 新版 sdk-core 的 XuqmMergedProvider 已通过反射调用 BugCollect.startCrashCapture(),
|
||||||
* XuqmSDK.appKey 已由本地 config 文件同步设置。
|
* 此 Provider 检查 XuqmSDK.isInitialized() 来判断是否需要补充初始化。
|
||||||
*
|
*
|
||||||
* 职责分离:
|
* 逻辑:
|
||||||
* Phase 1(同步):只注册 UncaughtExceptionHandler,把崩溃落盘。
|
* - sdk-core 已初始化 → 说明合并 Provider 已处理,跳过
|
||||||
* 不需要上传 URL,注册与 URL 完全解耦。
|
* - sdk-core 未初始化 → 旧版 sdk-core 场景,执行原有逻辑
|
||||||
* Phase 2(异步):等待 XuqmSDK.awaitInitialization() 完成,
|
|
||||||
* 远端配置下发 bugCollectApiUrl 后再上传积压的崩溃文件。
|
|
||||||
*/
|
*/
|
||||||
internal class BugCollectInitProvider : ContentProvider() {
|
internal class BugCollectInitProvider : ContentProvider() {
|
||||||
|
|
||||||
override fun onCreate(): Boolean {
|
override fun onCreate(): Boolean {
|
||||||
val ctx = context?.applicationContext ?: return true
|
val ctx = context?.applicationContext ?: return true
|
||||||
|
|
||||||
// ── Phase 1:注册崩溃拦截器(只需 appKey,无需 SDK 完全初始化)──────────
|
// 合并 Provider 已完成初始化(sdk-core 新版),跳过
|
||||||
// SDK 可能正在后台初始化中,直接读取 config 文件获取 appKey
|
if (XuqmSDK.isInitialized()) {
|
||||||
val appKey = if (XuqmSDK.isInitialized()) {
|
Log.d("BugCollect", "Already initialized by merged provider, skipping")
|
||||||
XuqmSDK.appKey
|
return true
|
||||||
} else {
|
|
||||||
XuqmSDK.readAppKey(ctx)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 旧版 sdk-core 路径:手动读取 config + 注册崩溃拦截
|
||||||
|
val appKey = XuqmSDK.readAppKey(ctx)
|
||||||
if (appKey.isNullOrBlank()) {
|
if (appKey.isNullOrBlank()) {
|
||||||
Log.w("BugCollect", "BugCollectInitProvider: no appKey available, skipping")
|
Log.w("BugCollect", "BugCollectInitProvider: no appKey available, skipping")
|
||||||
return true
|
return true
|
||||||
@ -50,16 +42,6 @@ internal class BugCollectInitProvider : ContentProvider() {
|
|||||||
getUserId = { XuqmSDK.getUserId() ?: XuqmSDK.deviceId },
|
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
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,9 +1,14 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<application>
|
<application>
|
||||||
|
<!--
|
||||||
|
合并启动入口:替代 XuqmInitializerProvider + BugCollectInitProvider + LicenseInitializerProvider。
|
||||||
|
主线程只读文件字节(<10ms),PBKDF2 解密 + 初始化全部移到 IO 线程。
|
||||||
|
initOrder=50 尽早执行,但不阻塞主线程。
|
||||||
|
-->
|
||||||
<provider
|
<provider
|
||||||
android:name="com.xuqm.sdk.internal.XuqmInitializerProvider"
|
android:name="com.xuqm.sdk.internal.XuqmMergedProvider"
|
||||||
android:authorities="${applicationId}.xuqm-init"
|
android:authorities="${applicationId}.xuqm-init"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:initOrder="90" />
|
android:initOrder="50" />
|
||||||
</application>
|
</application>
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
@ -120,7 +120,7 @@ object XuqmSDK {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
// 异步:TokenStore / DeviceId / ApiClient / 远端配置
|
// 异步:TokenStore / DeviceId / ApiClient / 远端配置
|
||||||
kotlinx.coroutines.CoroutineScope(kotlinx.coroutines.Dispatchers.IO + kotlinx.coroutines.SupervisorJob()).launch {
|
sdkScope.launch {
|
||||||
runCatching {
|
runCatching {
|
||||||
initialize(ctx, configFile.appKey, configFile.serverUrl, logLevel)
|
initialize(ctx, configFile.appKey, configFile.serverUrl, logLevel)
|
||||||
}.onFailure { e ->
|
}.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:手动初始化。
|
* 方式 B:手动初始化。
|
||||||
*
|
*
|
||||||
|
|||||||
@ -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) }
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -9,9 +9,14 @@ import com.google.gson.Gson
|
|||||||
*
|
*
|
||||||
* 搜索顺序(对齐 RN SDK 的 src/assets/config/):
|
* 搜索顺序(对齐 RN SDK 的 src/assets/config/):
|
||||||
* 1. assets/config/config.xuqmconfig 或 config.xuqm
|
* 1. assets/config/config.xuqmconfig 或 config.xuqm
|
||||||
* 2. assets/config/\*.xuqmconfig(取第一个)
|
* 2. assets/config/*.xuqmconfig(取第一个)
|
||||||
* 3. assets/xuqm/config.xuqm(旧路径兼容)
|
* 3. assets/xuqm/config.xuqm(旧路径兼容)
|
||||||
* 4. assets/xuqm/\*.xuqmconfig(旧路径兼容)
|
* 4. assets/xuqm/*.xuqmconfig(旧路径兼容)
|
||||||
|
*
|
||||||
|
* 缓存策略:
|
||||||
|
* - [readRawBytes]:只读取文件原始字节,不做解密(主线程安全,<5ms)
|
||||||
|
* - [readWithCache]:先查 ConfigCache(内存+磁盘),miss 时解密并写缓存
|
||||||
|
* - [read]:原始读取+解密,无缓存(兼容旧调用)
|
||||||
*/
|
*/
|
||||||
internal object ConfigFileReader {
|
internal object ConfigFileReader {
|
||||||
|
|
||||||
@ -19,6 +24,49 @@ internal object ConfigFileReader {
|
|||||||
private val CONFIG_DIRS = listOf("config", "xuqm")
|
private val CONFIG_DIRS = listOf("config", "xuqm")
|
||||||
private val gson = Gson()
|
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? {
|
fun read(context: Context): ConfigFile? {
|
||||||
return try {
|
return try {
|
||||||
val path = findConfigPath(context) ?: run {
|
val path = findConfigPath(context) ?: run {
|
||||||
@ -62,4 +110,3 @@ internal object ConfigFileReader {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -8,15 +8,16 @@ import android.util.Log
|
|||||||
import com.xuqm.sdk.XuqmSDK
|
import com.xuqm.sdk.XuqmSDK
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Auto-initializes XuqmSDK at app startup when a config file is present.
|
* 已废弃:由 [XuqmMergedProvider] 替代。
|
||||||
* Registered in sdk-core AndroidManifest so any module (im/update/push/license)
|
*
|
||||||
* triggers one-shot initialization without explicit init calls from the app.
|
* 保留此类仅为向后兼容(旧宿主可能在 AndroidManifest 中手动注册)。
|
||||||
|
* 新版 sdk-core 的 AndroidManifest 已注册 XuqmMergedProvider,
|
||||||
|
* 此 Provider 不会自动注册,需宿主显式声明才会生效。
|
||||||
*/
|
*/
|
||||||
|
@Deprecated("Use XuqmMergedProvider instead", ReplaceWith("XuqmMergedProvider"))
|
||||||
class XuqmInitializerProvider : ContentProvider() {
|
class XuqmInitializerProvider : ContentProvider() {
|
||||||
override fun onCreate(): Boolean {
|
override fun onCreate(): Boolean {
|
||||||
val ctx = context?.applicationContext ?: return true
|
val ctx = context?.applicationContext ?: return true
|
||||||
// 同步阶段只做最小工作:读取 config 文件设置 appKey(约 5ms)
|
|
||||||
// TokenStore / DeviceId / ApiClient 等重初始化移到后台线程
|
|
||||||
runCatching {
|
runCatching {
|
||||||
if (!XuqmSDK.isInitialized()) {
|
if (!XuqmSDK.isInitialized()) {
|
||||||
XuqmSDK.autoInitializeAsync(ctx)
|
XuqmSDK.autoInitializeAsync(ctx)
|
||||||
|
|||||||
@ -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<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
|
||||||
|
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
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "XuqmSDK"
|
||||||
|
}
|
||||||
|
}
|
||||||
正在加载...
在新工单中引用
屏蔽一个用户