测试
This commit is contained in:
134
app/src/main/java/com/hikoncont/crash/CrashHandler.kt
Normal file
134
app/src/main/java/com/hikoncont/crash/CrashHandler.kt
Normal file
@@ -0,0 +1,134 @@
|
||||
package com.hikoncont.crash
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import java.io.File
|
||||
import java.io.PrintWriter
|
||||
import java.io.StringWriter
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
* 全局未捕获异常处理器
|
||||
* 捕获崩溃信息写入本地文件,重启后由 CrashLogUploader 上传服务器
|
||||
*/
|
||||
class CrashHandler private constructor() : Thread.UncaughtExceptionHandler {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "CrashHandler"
|
||||
private const val CRASH_DIR = "crash_logs"
|
||||
private const val MAX_LOG_FILES = 20 // 最多保留20个崩溃日志
|
||||
|
||||
@Volatile
|
||||
private var instance: CrashHandler? = null
|
||||
|
||||
fun getInstance(): CrashHandler {
|
||||
return instance ?: synchronized(this) {
|
||||
instance ?: CrashHandler().also { instance = it }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var defaultHandler: Thread.UncaughtExceptionHandler? = null
|
||||
private lateinit var appContext: Context
|
||||
|
||||
fun init(context: Context) {
|
||||
appContext = context.applicationContext
|
||||
defaultHandler = Thread.getDefaultUncaughtExceptionHandler()
|
||||
Thread.setDefaultUncaughtExceptionHandler(this)
|
||||
Log.i(TAG, "✅ 崩溃处理器已初始化")
|
||||
}
|
||||
|
||||
override fun uncaughtException(thread: Thread, throwable: Throwable) {
|
||||
try {
|
||||
saveCrashLog(thread, throwable)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "❌ 保存崩溃日志失败", e)
|
||||
}
|
||||
// 交给系统默认处理器(终止进程)
|
||||
defaultHandler?.uncaughtException(thread, throwable)
|
||||
}
|
||||
|
||||
private fun saveCrashLog(thread: Thread, throwable: Throwable) {
|
||||
val timestamp = System.currentTimeMillis()
|
||||
val dateFormat = SimpleDateFormat("yyyy-MM-dd_HH-mm-ss", Locale.getDefault())
|
||||
val fileName = "crash_${dateFormat.format(Date(timestamp))}_$timestamp.log"
|
||||
|
||||
val sb = StringBuilder()
|
||||
// 设备信息
|
||||
sb.appendLine("=== 崩溃报告 ===")
|
||||
sb.appendLine("时间: ${SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.getDefault()).format(Date(timestamp))}")
|
||||
sb.appendLine("时间戳: $timestamp")
|
||||
sb.appendLine("线程: ${thread.name} (id=${thread.id})")
|
||||
sb.appendLine()
|
||||
sb.appendLine("=== 设备信息 ===")
|
||||
sb.appendLine("品牌: ${Build.BRAND}")
|
||||
sb.appendLine("型号: ${Build.MODEL}")
|
||||
sb.appendLine("制造商: ${Build.MANUFACTURER}")
|
||||
sb.appendLine("Android版本: ${Build.VERSION.RELEASE} (API ${Build.VERSION.SDK_INT})")
|
||||
sb.appendLine("指纹: ${Build.FINGERPRINT}")
|
||||
try {
|
||||
val pm = appContext.packageManager
|
||||
val pi = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
pm.getPackageInfo(appContext.packageName, android.content.pm.PackageManager.PackageInfoFlags.of(0))
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
pm.getPackageInfo(appContext.packageName, 0)
|
||||
}
|
||||
sb.appendLine("应用版本: ${pi.versionName} (${pi.longVersionCode})")
|
||||
} catch (_: Exception) {
|
||||
sb.appendLine("应用版本: unknown")
|
||||
}
|
||||
sb.appendLine("包名: ${appContext.packageName}")
|
||||
sb.appendLine()
|
||||
|
||||
// 堆栈信息
|
||||
sb.appendLine("=== 异常堆栈 ===")
|
||||
val sw = StringWriter()
|
||||
throwable.printStackTrace(PrintWriter(sw))
|
||||
sb.append(sw.toString())
|
||||
|
||||
// 写入文件
|
||||
val crashDir = File(appContext.filesDir, CRASH_DIR)
|
||||
if (!crashDir.exists()) crashDir.mkdirs()
|
||||
|
||||
val crashFile = File(crashDir, fileName)
|
||||
crashFile.writeText(sb.toString())
|
||||
Log.e(TAG, "💾 崩溃日志已保存: ${crashFile.absolutePath}")
|
||||
|
||||
// 清理旧日志
|
||||
cleanOldLogs(crashDir)
|
||||
}
|
||||
|
||||
private fun cleanOldLogs(dir: File) {
|
||||
val files = dir.listFiles { f -> f.name.startsWith("crash_") && f.name.endsWith(".log") }
|
||||
?: return
|
||||
if (files.size > MAX_LOG_FILES) {
|
||||
files.sortBy { it.lastModified() }
|
||||
for (i in 0 until files.size - MAX_LOG_FILES) {
|
||||
files[i].delete()
|
||||
Log.d(TAG, "🗑️ 清理旧崩溃日志: ${files[i].name}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取崩溃日志目录
|
||||
*/
|
||||
fun getCrashLogDir(): File {
|
||||
return File(appContext.filesDir, CRASH_DIR)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有待上传的崩溃日志文件
|
||||
*/
|
||||
fun getPendingCrashLogs(): List<File> {
|
||||
val dir = getCrashLogDir()
|
||||
if (!dir.exists()) return emptyList()
|
||||
return dir.listFiles { f -> f.name.startsWith("crash_") && f.name.endsWith(".log") }
|
||||
?.sortedByDescending { it.lastModified() }
|
||||
?: emptyList()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user