2 Commits
Author SHA1 Message Date
mazengfei 9db8996d44 chore(app): 添加日志定期清理机制,优化日志文件管理
- 应用启动时立即清理过期日志文件和崩溃日志
- 新增每日24小时定时任务,持续清理过期日志
- 在 CrashHandler 初始化时清理旧的崩溃日志,默认保留7天
- FileLogger新增日志文件大小限制,防止异常刷屏占满存储
- 新增 FileLogger 清理过期日志的方法,按文件修改时间删除过旧日志
- 修改全局API地址为UAT环境域名,更新PROD_BASE_URL常量
2026-08-31 15:09:16 +08:00
mazengfeiandClaude Fable 5 5ffdb3ea64 feat(log): 增加日志定期清理机制,防止长期运行日志无限累加
- FileLogger 新增过期清理(保留7天)及单文件100MB写入上限(适配异步写入版)
- CrashHandler 激活崩溃日志清理并补充异常保护
- App 启动清理 + 运行中每24小时定时清理,覆盖设备长期不断电场景

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-31 15:08:22 +08:00
4 changed files with 80 additions and 8 deletions
@@ -5,8 +5,11 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.IntentFilter import android.content.IntentFilter
import android.content.SharedPreferences import android.content.SharedPreferences
import android.os.Handler
import android.os.Looper
import com.shuwei.intelligent.shelves.utils.BootReceiver import com.shuwei.intelligent.shelves.utils.BootReceiver
import com.shuwei.intelligent.shelves.utils.CrashHandler import com.shuwei.intelligent.shelves.utils.CrashHandler
import com.shuwei.intelligent.shelves.utils.FileLogger
/** /**
* @author: star * @author: star
@@ -19,10 +22,22 @@ class App : Application() {
super.onCreate() super.onCreate()
app = this app = this
CrashHandler.init(this) CrashHandler.init(this)
// 启动时立即清理一次过期日志
cleanupLogs()
// 设备长期不断电、进程不会重新启动,运行中每24小时定期清理一次
logCleanupHandler.postDelayed(logCleanupRunnable, LOG_CLEANUP_INTERVAL)
val filter = IntentFilter(Intent.ACTION_BOOT_COMPLETED) val filter = IntentFilter(Intent.ACTION_BOOT_COMPLETED)
registerReceiver(BootReceiver(), filter) registerReceiver(BootReceiver(), filter)
} }
/**
* 清理过期的日志文件:文件日志与崩溃日志均保留最近7天
*/
private fun cleanupLogs() {
FileLogger.cleanupExpiredLogs(this)
CrashHandler.getInstance()?.cleanupOldCrashReports()
}
companion object { companion object {
private lateinit var app: App private lateinit var app: App
var canteenId = "0" var canteenId = "0"
@@ -36,6 +51,19 @@ class App : Application() {
@Volatile @Volatile
private var sharedPref: SharedPreferences? = null private var sharedPref: SharedPreferences? = null
/** 日志定期清理间隔:24小时,与日志文件按天滚动对齐 */
private const val LOG_CLEANUP_INTERVAL = 24L * 60 * 60 * 1000
private val logCleanupHandler = Handler(Looper.getMainLooper())
/** 日志定期清理任务:执行后重新调度自身,形成循环 */
private val logCleanupRunnable = object : Runnable {
override fun run() {
getInstance().cleanupLogs()
logCleanupHandler.postDelayed(this, LOG_CLEANUP_INTERVAL)
}
}
fun getInstance(): App { fun getInstance(): App {
return app return app
} }
@@ -20,7 +20,7 @@ object GlobalData {
// const val LOCAL_BASE_URL = "http://192.168.1.201:14801" // const val LOCAL_BASE_URL = "http://192.168.1.201:14801"
const val LOCAL_BASE_URL = "http://192.168.10.101:24801" const val LOCAL_BASE_URL = "http://192.168.10.101:24801"
const val TEST_BASE_URL = "https://dev.yixiong-tech.com:8081" const val TEST_BASE_URL = "https://dev.yixiong-tech.com:8081"
const val PROD_BASE_URL = "https://api.dm.yixiong-tech.com:8443" const val PROD_BASE_URL = "https://platform-api.uat.shuziweidao.com"
} }
@@ -39,11 +39,19 @@ class CrashHandler private constructor(private val context: Context) :
synchronized(CrashHandler::class.java) { synchronized(CrashHandler::class.java) {
if (instance == null) { if (instance == null) {
instance = CrashHandler(context.applicationContext) instance = CrashHandler(context.applicationContext)
// 初始化时清理过期的崩溃日志(默认保留7天)
instance?.cleanupOldCrashReports()
} }
} }
} }
} }
/**
* 获取单例实例
* @return 已通过 init() 初始化时返回实例,否则返回 null
*/
fun getInstance(): CrashHandler? = instance
//fun getCrashReportFiles(context: Context): Array<File> { //fun getCrashReportFiles(context: Context): Array<File> {
// val crashDir = getCrashDir() // val crashDir = getCrashDir()
// return if (crashDir.exists() && crashDir.isDirectory) { // return if (crashDir.exists() && crashDir.isDirectory) {
@@ -222,16 +230,21 @@ class CrashHandler private constructor(private val context: Context) :
* 清理旧的崩溃日志 * 清理旧的崩溃日志
*/ */
fun cleanupOldCrashReports(maxAgeDays: Int = 7) { fun cleanupOldCrashReports(maxAgeDays: Int = 7) {
val crashDir = getCrashDir() // 该方法会在 Application 启动及定时任务中调用,异常必须内部消化,避免影响主流程
if (!crashDir.exists() || !crashDir.isDirectory) return try {
val crashDir = getCrashDir()
if (!crashDir.exists() || !crashDir.isDirectory) return
val now = System.currentTimeMillis() val now = System.currentTimeMillis()
val maxAgeMillis = maxAgeDays * 24 * 60 * 60 * 1000L val maxAgeMillis = maxAgeDays * 24 * 60 * 60 * 1000L
crashDir.listFiles()?.forEach { file -> crashDir.listFiles()?.forEach { file ->
if (file.lastModified() < now - maxAgeMillis) { if (file.lastModified() < now - maxAgeMillis) {
file.delete() file.delete()
}
} }
} catch (e: Exception) {
Log.e(TAG, "清理崩溃日志失败", e)
} }
} }
@@ -19,6 +19,12 @@ class FileLogger(context: Context) {
private const val FILE_PREFIX = "log_" private const val FILE_PREFIX = "log_"
private const val FILE_EXTENSION = ".txt" private const val FILE_EXTENSION = ".txt"
/** 日志保留天数,超过该天数的日志文件在应用启动时清理 */
private const val RETENTION_DAYS = 7L
/** 单个日志文件大小上限(100MB),超过后当天不再写入,防止异常刷屏撑爆存储 */
private const val MAX_LOG_FILE_SIZE = 100L * 1024 * 1024
/** 全局单线程日志写入 */ /** 全局单线程日志写入 */
private val logChannel = Channel<RawEntry>(capacity = Channel.UNLIMITED) private val logChannel = Channel<RawEntry>(capacity = Channel.UNLIMITED)
private val writerScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) private val writerScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
@@ -31,6 +37,9 @@ class FileLogger(context: Context) {
for (entry in logChannel) { for (entry in logChannel) {
try { try {
val fileName = "${FILE_PREFIX}${dateFormat.format(Date())}${FILE_EXTENSION}" val fileName = "${FILE_PREFIX}${dateFormat.format(Date())}${FILE_EXTENSION}"
// 单文件超过大小上限时丢弃本条日志,防止异常刷屏撑爆存储(单协程检查,无并发问题)
val file = File(entry.appContext.filesDir, fileName)
if (file.exists() && file.length() > MAX_LOG_FILE_SIZE) continue
val timestamp = timeFormat.format(Date()) val timestamp = timeFormat.format(Date())
val logLine = "[$timestamp] [${entry.level}] ${entry.message}\n" val logLine = "[$timestamp] [${entry.level}] ${entry.message}\n"
val output = entry.appContext.openFileOutput( val output = entry.appContext.openFileOutput(
@@ -49,6 +58,28 @@ class FileLogger(context: Context) {
val message: String, val message: String,
val level: LogLevel val level: LogLevel
) )
/**
* 清理过期的日志文件,保留最近 retentionDays 天
* 按文件最后修改时间判断,最近仍在写入的文件不会被误删
* 建议在 Application 启动时调用一次
* @param context 上下文,用于定位 filesDir
* @param retentionDays 日志保留天数
*/
fun cleanupExpiredLogs(context: Context, retentionDays: Long = RETENTION_DAYS) {
try {
val cutoff = System.currentTimeMillis() - retentionDays * 24 * 60 * 60 * 1000L
context.filesDir.listFiles { _, name ->
name.startsWith(FILE_PREFIX) && name.endsWith(FILE_EXTENSION)
}?.forEach { file ->
if (file.lastModified() < cutoff) {
file.delete()
}
}
} catch (e: Exception) {
e.printStackTrace()
}
}
} }
/** 持有 ApplicationContext 避免 Activity 泄漏 */ /** 持有 ApplicationContext 避免 Activity 泄漏 */