如何为 Crashlytics 添加自定义日志和用户标识?
解读
国内面试中,Crashlytics 通常与 Firebase 强绑定,面试官想确认两点:
- 你是否真的在灰度/线上环境用过 Crashlytics,而不是“只集成过 SDK”;
- 你对“可观测性”是否有体系化思维——日志、用户、上下文、性能、隐私合规能否一把抓。
因此,回答要给出“代码 + 时机 + 场景 + 合规”四维闭环,而不是背文档。
知识点
- Firebase Crashlytics 自定义日志接口:
FirebaseCrashlytics.getInstance().log(String)recordException(Throwable)/recordException(Throwable, Attributes)
- 用户标识接口:
setUserId(String)setCustomKey(String, String|int|long|float|double|boolean)
- 日志上限与采样策略:64 KB/会话,循环覆盖;高频写入需自行降采样。
- 合规要求:
- 国内《个人信息保护法》要求“用户可撤回”,故需封装“退出登录即清除”逻辑。
- 若用户未同意《隐私政策》,禁止调用
setUserId与含 PII 的setCustomKey。
- 性能与稳定性:
- 日志写入走 native 层环形缓冲区,主线程安全;但仍建议放后台线程,避免极端场景锁竞争。
- 应用多进程时,Crashlytics 默认只在主进程初始化,需手动在
:push、:download等子进程调用FirebaseApp.initializeApp,否则日志会丢失。
- 国内厂商兼容:
- 华为、荣耀无 GMS,需降级为“自采集 + 后端转发”方案,或切换为腾讯 Bugly、阿里 SLS 终端 SDK,但接口设计可保持一致,方便抽象层切换。
答案
步骤一:在 Application.onCreate() 中初始化,并做合规判断
class MyApp : Application() {
override fun onCreate() {
super.onCreate()
if (PrivacyManager.userAcceptedPrivacyPolicy()) {
FirebaseApp.initializeApp(this) // 多进程场景必调
val crashlytics = FirebaseCrashlytics.getInstance()
crashlytics.setCrashlyticsCollectionEnabled(true)
}
}
}
步骤二:封装统一日志门面,支持“延迟写入”与“隐私开关”
object CrashlyticsLogger {
private const val MAX_LOG_LEN = 800 // 预留中文双字节安全余量
private val buffer = StringBuilder()
@AnyThread
fun log(message: String) {
if (!PrivacyManager.userAcceptedPrivacyPolicy()) return
val ts = SimpleDateFormat("MM-dd HH:mm:ss.SSS", Locale.CHINA).format(Date())
val line = "$ts ${message.takeLast(MAX_LOG_LEN)}\n"
synchronized(buffer) {
buffer.append(line)
if (buffer.length > 56 * 1024) { // 提前触发裁剪,留 8 KB 余量
buffer.delete(0, buffer.length / 4)
}
}
// 异步落盘,避免 ANR
GlobalScope.launch(Dispatchers.IO) {
FirebaseCrashlytics.getInstance().log(line)
}
}
fun setUserId(userId: String) {
if (!PrivacyManager.userAcceptedPrivacyPolicy()) return
FirebaseCrashlytics.getInstance().setUserId(userId.md5()) // 脱敏
}
inline fun setCustomKeys(block: FirebaseCrashlytics.() -> Unit) {
if (!PrivacyManager.userAcceptedPrivacyPolicy()) return
FirebaseCrashlytics.getInstance().apply(block)
}
}
步骤三:在关键业务节点埋点
// 登录成功
CrashlyticsLogger.setUserId(userId)
CrashlyticsLogger.setCustomKeys {
setCustomKey("channel", BuildConfig.CHANNEL)
setCustomKey("api_level", Build.VERSION.SDK_INT)
}
// 支付异常
try {
payCore.charge(order)
} catch (e: PayException) {
CrashlyticsLogger.log("Pay failed, order=$order, code=${e.code}")
FirebaseCrashlytics.getInstance().recordException(e)
}
步骤四:退出登录或撤回隐私授权时立即清理
fun logout() {
PrivacyManager.revokeConsent()
FirebaseCrashlytics.getInstance().setUserId("")
FirebaseCrashlytics.getInstance().setCrashlyticsCollectionEnabled(false)
}
通过以上四步,既满足 Crashlytics 官方最佳实践,也符合国内隐私合规要求,同时把日志量、线程安全、多进程、用户撤回等细节全部收口。
拓展思考
- 日志分级与采样:
线上高并发接口可能每秒上万次调用,全量写入会冲掉关键日志。可引入“令牌桶”限流,仅 1% 采样,但异常 case 强制全量。 - 与APM融合:
把 Crashlytics 日志同时写入字节跳动 RangersAPM 或腾讯 Matrix,做“崩溃+性能”联动,比如检测到卡顿后 3s 内发生崩溃,自动把最近 100 条日志上传到自建 SLS,方便后台还原用户路径。 - 端到端加密:
若日志含订单号、手机号,可在 Java 层做 AES-GCM 加密,密钥放在 TEE,通过 HAL 接口获取;Crashlytics 仅存储密文,后台解密,防止 Google 服务器或第三方 CDN 泄露数据。 - 灰度回捞:
国内厂商 ROM 会杀后台,导致 Crashlytics 上传失败。可在下次冷启时通过 WorkManager 触发“回捞任务”,把.gz压缩包上传至国内 CDN,再转发到 Firebase,提升 15% 上报率。