|
|
@@ -5,6 +5,7 @@ import android.util.Log
|
|
|
import java.io.File
|
|
|
import java.io.FileWriter
|
|
|
import java.io.PrintWriter
|
|
|
+import java.io.RandomAccessFile
|
|
|
import java.text.SimpleDateFormat
|
|
|
import java.util.Date
|
|
|
import java.util.Locale
|
|
|
@@ -26,10 +27,25 @@ object VLog {
|
|
|
private const val MAX_LOG_SIZE = 10 * 1024 * 1024 // 10MB
|
|
|
private const val MAX_LOG_FILES = 1 // 每种类型就保留一个日志文件
|
|
|
|
|
|
+ // 当前日志类型,用于同步写入到 PersistentLog
|
|
|
+ private var currentLogType: PersistentLog.LogType? = null
|
|
|
+
|
|
|
/**
|
|
|
* 初始化日志系统
|
|
|
*/
|
|
|
fun init(context: Context, name: String = "service") {
|
|
|
+ // 同时初始化 PersistentLog
|
|
|
+ currentLogType = when (name.lowercase()) {
|
|
|
+ "client" -> {
|
|
|
+ PersistentLog.initClient(context)
|
|
|
+ PersistentLog.LogType.CLIENT
|
|
|
+ }
|
|
|
+ else -> {
|
|
|
+ PersistentLog.initService(context)
|
|
|
+ PersistentLog.LogType.SERVICE
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
executor.execute {
|
|
|
try {
|
|
|
val logDir = File(context.filesDir, "logs")
|
|
|
@@ -92,6 +108,11 @@ object VLog {
|
|
|
* 写入日志
|
|
|
*/
|
|
|
private fun writeLog(level: String, tag: String, message: String, throwable: Throwable? = null) {
|
|
|
+ // 同时写入到 PersistentLog(不输出到 logcat,因为调用方已经输出)
|
|
|
+ currentLogType?.let { logType ->
|
|
|
+ PersistentLog.writeLogOnly(logType, level, tag, message, throwable)
|
|
|
+ }
|
|
|
+
|
|
|
executor.execute {
|
|
|
lock.withLock {
|
|
|
try {
|
|
|
@@ -166,3 +187,332 @@ object VLog {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+/**
|
|
|
+ * 持久化日志管理器
|
|
|
+ * 支持两种独立的日志类型:
|
|
|
+ * - Client: 客户端日志,最大10MB
|
|
|
+ * - Service: 服务日志,最大30MB
|
|
|
+ *
|
|
|
+ * 特点:
|
|
|
+ * - 使用固定文件名,重启后继续追加,不会重置
|
|
|
+ * - 超过大小限制时自动截断旧日志,保留最近内容
|
|
|
+ */
|
|
|
+object PersistentLog {
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 日志类型枚举
|
|
|
+ */
|
|
|
+ enum class LogType(val fileName: String, val maxSize: Long) {
|
|
|
+ CLIENT("client.log", 10 * 1024 * 1024L), // 10MB
|
|
|
+ SERVICE("service.log", 30 * 1024 * 1024L) // 30MB
|
|
|
+ }
|
|
|
+
|
|
|
+ private data class LogWriter(
|
|
|
+ var file: File,
|
|
|
+ var writer: PrintWriter?,
|
|
|
+ val maxSize: Long
|
|
|
+ )
|
|
|
+
|
|
|
+ private val logWriters = mutableMapOf<LogType, LogWriter>()
|
|
|
+ private val lock = ReentrantLock()
|
|
|
+ private val executor = Executors.newSingleThreadExecutor()
|
|
|
+ private val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.getDefault())
|
|
|
+ private var logDir: File? = null
|
|
|
+
|
|
|
+ // 当文件超过最大大小时,保留最近的这个比例的内容
|
|
|
+ private const val TRUNCATE_KEEP_RATIO = 0.7
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 初始化客户端日志系统
|
|
|
+ */
|
|
|
+ fun initClient(context: Context) {
|
|
|
+ init(context, LogType.CLIENT)
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 初始化Service日志系统
|
|
|
+ */
|
|
|
+ fun initService(context: Context) {
|
|
|
+ init(context, LogType.SERVICE)
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 初始化指定类型的日志系统
|
|
|
+ */
|
|
|
+ private fun init(context: Context, logType: LogType) {
|
|
|
+ executor.execute {
|
|
|
+ lock.withLock {
|
|
|
+ try {
|
|
|
+ val dir = File(context.filesDir, "logs")
|
|
|
+ if (!dir.exists()) {
|
|
|
+ dir.mkdirs()
|
|
|
+ }
|
|
|
+ logDir = dir
|
|
|
+
|
|
|
+ // 使用固定文件名,支持追加
|
|
|
+ val logFile = File(dir, logType.fileName)
|
|
|
+
|
|
|
+ // 检查文件大小,如果超过限制则截断
|
|
|
+ if (logFile.exists() && logFile.length() > logType.maxSize) {
|
|
|
+ truncateLogFile(logFile, logType.maxSize)
|
|
|
+ }
|
|
|
+
|
|
|
+ // 创建或追加到日志文件
|
|
|
+ val writer = PrintWriter(FileWriter(logFile, true), true)
|
|
|
+ logWriters[logType] = LogWriter(logFile, writer, logType.maxSize)
|
|
|
+
|
|
|
+ val timestamp = dateFormat.format(Date())
|
|
|
+ writer.println("\n[$timestamp] [INFO] [PersistentLog] ========== 日志系统启动 (${logType.name}) ==========")
|
|
|
+ writer.flush()
|
|
|
+
|
|
|
+ Log.i("PersistentLog", "${logType.name} 日志系统初始化成功: ${logFile.absolutePath}, 当前大小: ${logFile.length() / 1024}KB")
|
|
|
+ } catch (e: Exception) {
|
|
|
+ Log.e("PersistentLog", "初始化 ${logType.name} 日志系统失败", e)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 截断日志文件,保留最近的内容
|
|
|
+ */
|
|
|
+ private fun truncateLogFile(file: File, maxSize: Long) {
|
|
|
+ try {
|
|
|
+ val keepSize = (maxSize * TRUNCATE_KEEP_RATIO).toLong()
|
|
|
+ val fileSize = file.length()
|
|
|
+
|
|
|
+ if (fileSize <= maxSize) return
|
|
|
+
|
|
|
+ Log.i("PersistentLog", "日志文件超过限制,开始截断: ${file.name}, 当前大小: ${fileSize / 1024 / 1024}MB")
|
|
|
+
|
|
|
+ // 读取文件末尾的内容
|
|
|
+ RandomAccessFile(file, "r").use { raf ->
|
|
|
+ val skipBytes = fileSize - keepSize
|
|
|
+ raf.seek(skipBytes)
|
|
|
+
|
|
|
+ // 跳过可能的不完整行
|
|
|
+ raf.readLine()
|
|
|
+
|
|
|
+ // 读取剩余内容
|
|
|
+ val remainingBytes = ByteArray((fileSize - raf.filePointer).toInt())
|
|
|
+ raf.readFully(remainingBytes)
|
|
|
+
|
|
|
+ // 写入新文件
|
|
|
+ file.writeBytes(remainingBytes)
|
|
|
+ }
|
|
|
+
|
|
|
+ // 在文件开头添加截断标记
|
|
|
+ val tempFile = File(file.parent, "${file.name}.tmp")
|
|
|
+ val timestamp = dateFormat.format(Date())
|
|
|
+ tempFile.writeText("[$timestamp] [INFO] [PersistentLog] ========== 日志已截断,保留最近内容 ==========\n")
|
|
|
+ tempFile.appendBytes(file.readBytes())
|
|
|
+ tempFile.renameTo(file)
|
|
|
+
|
|
|
+ Log.i("PersistentLog", "日志文件截断完成: ${file.name}, 新大小: ${file.length() / 1024 / 1024}MB")
|
|
|
+ } catch (e: Exception) {
|
|
|
+ Log.e("PersistentLog", "截断日志文件失败", e)
|
|
|
+ // 如果截断失败,尝试直接清空文件
|
|
|
+ try {
|
|
|
+ file.writeText("")
|
|
|
+ Log.w("PersistentLog", "截断失败,已清空日志文件: ${file.name}")
|
|
|
+ } catch (e2: Exception) {
|
|
|
+ Log.e("PersistentLog", "清空日志文件也失败", e2)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 检查并轮转日志文件
|
|
|
+ */
|
|
|
+ private fun checkAndRotateIfNeeded(logType: LogType) {
|
|
|
+ val logWriter = logWriters[logType] ?: return
|
|
|
+ try {
|
|
|
+ if (logWriter.file.length() > logWriter.maxSize) {
|
|
|
+ logWriter.writer?.close()
|
|
|
+ truncateLogFile(logWriter.file, logWriter.maxSize)
|
|
|
+ logWriter.writer = PrintWriter(FileWriter(logWriter.file, true), true)
|
|
|
+ }
|
|
|
+ } catch (e: Exception) {
|
|
|
+ Log.e("PersistentLog", "轮转日志文件失败", e)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 写入日志到指定类型(内部使用,同时输出到 logcat)
|
|
|
+ */
|
|
|
+ private fun writeLog(logType: LogType, level: String, tag: String, message: String, throwable: Throwable? = null) {
|
|
|
+ writeLogInternal(logType, level, tag, message, throwable)
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 只写入日志到文件,不输出到 logcat(供 VLog 调用,避免重复输出)
|
|
|
+ */
|
|
|
+ fun writeLogOnly(logType: LogType, level: String, tag: String, message: String, throwable: Throwable? = null) {
|
|
|
+ writeLogInternal(logType, level, tag, message, throwable)
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 内部写入日志方法
|
|
|
+ */
|
|
|
+ private fun writeLogInternal(logType: LogType, level: String, tag: String, message: String, throwable: Throwable? = null) {
|
|
|
+ executor.execute {
|
|
|
+ lock.withLock {
|
|
|
+ try {
|
|
|
+ val logWriter = logWriters[logType] ?: return@withLock
|
|
|
+
|
|
|
+ val timestamp = dateFormat.format(Date())
|
|
|
+ val logMessage = "[$timestamp] [$level] [$tag] $message"
|
|
|
+
|
|
|
+ logWriter.writer?.println(logMessage)
|
|
|
+ throwable?.let {
|
|
|
+ logWriter.writer?.println(it.stackTraceToString())
|
|
|
+ }
|
|
|
+ logWriter.writer?.flush()
|
|
|
+
|
|
|
+ // 检查是否需要轮转
|
|
|
+ checkAndRotateIfNeeded(logType)
|
|
|
+ } catch (e: Exception) {
|
|
|
+ Log.e("PersistentLog", "写入日志失败", e)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 客户端日志方法 ====================
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 客户端 Info 级别日志
|
|
|
+ */
|
|
|
+ fun ci(tag: String, message: String) {
|
|
|
+ Log.i(tag, message)
|
|
|
+ writeLog(LogType.CLIENT, "INFO", tag, message)
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 客户端 Debug 级别日志
|
|
|
+ */
|
|
|
+ fun cd(tag: String, message: String) {
|
|
|
+ Log.d(tag, message)
|
|
|
+ writeLog(LogType.CLIENT, "DEBUG", tag, message)
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 客户端 Warning 级别日志
|
|
|
+ */
|
|
|
+ fun cw(tag: String, message: String, throwable: Throwable? = null) {
|
|
|
+ Log.w(tag, message, throwable)
|
|
|
+ writeLog(LogType.CLIENT, "WARN", tag, message, throwable)
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 客户端 Error 级别日志
|
|
|
+ */
|
|
|
+ fun ce(tag: String, message: String, throwable: Throwable? = null) {
|
|
|
+ Log.e(tag, message, throwable)
|
|
|
+ writeLog(LogType.CLIENT, "ERROR", tag, message, throwable)
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== Service日志方法 ====================
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Service Info 级别日志
|
|
|
+ */
|
|
|
+ fun si(tag: String, message: String) {
|
|
|
+ Log.i(tag, message)
|
|
|
+ writeLog(LogType.SERVICE, "INFO", tag, message)
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Service Debug 级别日志
|
|
|
+ */
|
|
|
+ fun sd(tag: String, message: String) {
|
|
|
+ Log.d(tag, message)
|
|
|
+ writeLog(LogType.SERVICE, "DEBUG", tag, message)
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Service Warning 级别日志
|
|
|
+ */
|
|
|
+ fun sw(tag: String, message: String, throwable: Throwable? = null) {
|
|
|
+ Log.w(tag, message, throwable)
|
|
|
+ writeLog(LogType.SERVICE, "WARN", tag, message, throwable)
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Service Error 级别日志
|
|
|
+ */
|
|
|
+ fun se(tag: String, message: String, throwable: Throwable? = null) {
|
|
|
+ Log.e(tag, message, throwable)
|
|
|
+ writeLog(LogType.SERVICE, "ERROR", tag, message, throwable)
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 工具方法 ====================
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取客户端日志文件路径
|
|
|
+ */
|
|
|
+ fun getClientLogFilePath(): String? {
|
|
|
+ return logWriters[LogType.CLIENT]?.file?.absolutePath
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取Service日志文件路径
|
|
|
+ */
|
|
|
+ fun getServiceLogFilePath(): String? {
|
|
|
+ return logWriters[LogType.SERVICE]?.file?.absolutePath
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取日志目录
|
|
|
+ */
|
|
|
+ fun getLogDir(): File? {
|
|
|
+ return logDir
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 关闭客户端日志系统
|
|
|
+ */
|
|
|
+ fun closeClient() {
|
|
|
+ close(LogType.CLIENT)
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 关闭Service日志系统
|
|
|
+ */
|
|
|
+ fun closeService() {
|
|
|
+ close(LogType.SERVICE)
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 关闭指定类型的日志系统
|
|
|
+ */
|
|
|
+ private fun close(logType: LogType) {
|
|
|
+ executor.execute {
|
|
|
+ lock.withLock {
|
|
|
+ try {
|
|
|
+ logWriters[logType]?.writer?.close()
|
|
|
+ logWriters.remove(logType)
|
|
|
+ } catch (e: Exception) {
|
|
|
+ Log.e("PersistentLog", "关闭 ${logType.name} 日志系统失败", e)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 关闭所有日志系统
|
|
|
+ */
|
|
|
+ fun closeAll() {
|
|
|
+ executor.execute {
|
|
|
+ lock.withLock {
|
|
|
+ try {
|
|
|
+ logWriters.values.forEach { it.writer?.close() }
|
|
|
+ logWriters.clear()
|
|
|
+ } catch (e: Exception) {
|
|
|
+ Log.e("PersistentLog", "关闭日志系统失败", e)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|