本篇用于记录日志框架的使用,原地址:https://juejin.cn/post/7470521949626613771
一、为什么我们需要专门的日志框架?
1.1 原生Log的缺点
- 😱 性能杀手:同步写入日志导致主线程卡顿(线上环境每秒数百条日志时尤为明显)
- 📉 日志丢失:系统日志缓冲区溢出时直接丢弃旧日志(关键崩溃现场说没就没)
- 🔒 安全隐患:测试代码忘记删除,敏感数据(用户ID、地址)裸奔在Logcat
- 🌪 可读性灾难:不同模块的日志混杂,定位问题像大海捞针
- 🚫 无法动态控制:线上环境无法按需调整日志级别,被迫重新打包发布
- 📁 文件管理缺失:无法自动分片、压缩、加密存储日志文件
- 📶 网络同步困难:关键日志无法实时上报到服务端
1.2 理想日志框架的六大特征
- 异步写入:绝不阻塞主线程
- 分级控制:动态调整日志级别
- 安全保障:自动脱敏+加密存储
- 智能管理:日志分片/压缩/自动清理
- 崩溃现场保留:确保关键日志不丢失
- 可扩展性:支持自定义输出和网络上报
二、主流日志框架横向评测
2.1 候选名单
框架 | 核心优势 | 潜在短板 |
---|---|---|
Logger | 漂亮的格式化输出 | 功能较基础 |
Timber | 灵活的树形结构扩展 | 需要自行实现文件存储 |
XLog(微信Mars框架的一部分) | 微信出品,全链路解决方案 | 接入成本略高 |
com.elvishew:xlog | 轻量级、灵活配置、兼容性好 | 社区活跃度较低 |
三、为什么选择xlog?
-
性能:com.elvishew:xlog是一个轻量级的日志库,对应用性能的影响较小。它提供了灵活的日志配置和多种输出目标,可以满足大多数应用的日志记录需求。
-
易用性:该方案易于集成和使用,提供了丰富的API和配置选项,方便开发者根据需求进行定制。
-
功能需求:支持多种日志级别、格式化输出、日志文件管理等功能,可以满足一般应用的日志记录和管理需求。
-
日志管理便捷性:日志文件可以灵活配置保存路径和自动备份策略,方便开发者进行日志管理和分析。
另外,如何选择合适的框架,需要根据项目情况去判定,并非一成不变的。即使手写管理也是可以的。
四、使用案例
- github网址:xLog/README_ZH.md at master · elvishew/xLog · GitHub
- 为了保持项目的组件化,这里创建新的模块,专门进行log的管理。
- 日志模块,加密存储日志文件到本地。
- 解密工具:https://github.com/leavesCZY/compose-multiplatform-xlog-decode
- 日志文件存储位置 /sdcard/Android/data/包名/files/logs/
4.1 引入依赖
xlog = "1.11.1"
xlogLibcat = "1.0.0"
xlog-libcat = { module = "com.elvishew:xlog-libcat", version.ref = "xlogLibcat" }
xlog = { module = "com.elvishew:xlog", version.ref = "xlog" }
dependencies {
implementation(libs.xlog)
implementation(libs.xlog.libcat
}
4.2 配置xlog
- 用于生成日志文件名
/**
* FramesFileNameGenerator 类实现了 FileNameGenerator 接口,用于生成日志文件的文件名。
* 它根据应用程序的版本名称、日志级别和时间戳来创建唯一的文件名。
*
* @param appVersionName 应用程序的版本名称,用于包含在生成的文件名中。
*/
class FramesFileNameGenerator(private val appVersionName: String) : FileNameGenerator {
/**
* 指示文件名是否可以更改。
*
* @return 始终返回 true,表示文件名是可更改的。
*/
override fun isFileNameChangeable(): Boolean {
return true
}
/**
* 根据日志级别和时间戳生成日志文件的文件名。
* 文件名格式为:.kLog-日期-v版本号.log,例如:.kLog-2023-10-04-v1.0.log
*
* @param logLevel 日志级别,未在此方法中使用,但可能在将来用于更细化的日志文件名生成。
* @param timestamp 时间戳,用于生成文件名中的日期部分。
* @return 生成的日志文件名。
*/
override fun generateFileName(logLevel: Int, timestamp: Long): String {
// 使用默认区域设置创建 SimpleDateFormat 实例,用于格式化日期。
val sdf = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
// 拼接文件名并返回。
return ".kLog" + "-" + sdf.format(Date(timestamp)).plus("-v")
.plus(appVersionName) + ".log"
}
}
- 用于日志信息格式化
/**
* FramesFlattener 类实现了 Flattener2 接口,用于将日志信息格式化为扁平化的字符串格式。
* 该类负责将日志事件信息转换为可读的字符串格式。
*/
class FramesFlattener : Flattener2 {
/**
* 将日志信息格式化为扁平化的字符串格式。
*
* @param timeMillis 日志事件的时间戳,自 Unix 纪元以来的毫秒数。
* @param logLevel 日志事件的日志级别。
* @param tag 日志事件的标签,可能为 null。
* @param message 日志事件的消息内容,可能为 null。
* @return 返回格式化后的日志信息字符串,包含日期、日志级别、标签和消息,各部分以 '|' 分隔。
*/
override fun flatten(
timeMillis: Long,
logLevel: Int,
tag: String?,
message: String?
): CharSequence {
// 拼接当前日期、日志级别名称、标签和消息,使用 '|' 作为分隔符
return (getCurrDDate()
+ '|' + LogLevel.getLevelName(logLevel)
+ '|' + tag
+ '|' + message)
}
/**
* 获取当前日期和时间的字符串表示。
*
* @return 返回当前日期和时间的字符串,格式为 "yyyy-MM-dd HH:mm:ss.SSS"。
*/
private fun getCurrDDate(): String {
// 使用 SimpleDateFormat 格式化当前日期和时间
return SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.getDefault()).format(Date())
}
}
- 用于捕捉崩溃异常等
/**
* 自定义崩溃处理类,用于捕获未处理的异常和崩溃信息
*/
class FramesCrashHandler private constructor() : UncaughtExceptionHandler {
private val tag = "FramesCrashHandler"
private val infoMap = mutableMapOf<String, String?>()
private var mDefaultHandler: UncaughtExceptionHandler? = null
private var mContext: Context? = null
private val sDateFormat = SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.getDefault())
companion object {
// 单例实例
val instance: FramesCrashHandler by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) {
FramesCrashHandler()
}
}
/**
* 初始化崩溃处理器
* @param context 应用上下文
*/
fun init(context: Context?) {
mContext = context
mDefaultHandler = Thread.getDefaultUncaughtExceptionHandler()
Thread.setDefaultUncaughtExceptionHandler(this)
}
/**
* 处理未捕获的异常
* @param thread 发生异常的线程
* @param ex 异常信息
*/
override fun uncaughtException(thread: Thread, ex: Throwable) {
if (!handleException(ex) && mDefaultHandler != null) {
mDefaultHandler!!.uncaughtException(thread, ex)
} else {
KLog.e("uncaughtException", ex)
SystemClock.sleep(3000L)
Process.killProcess(Process.myPid())
exitProcess(1)
}
}
/**
* 自定义异常处理逻辑
* @param ex 异常信息
* @return 是否处理了该异常
*/
private fun handleException(ex: Throwable?): Boolean {
return if (ex == null) {
false
} else {
try {
object : Thread() {
override fun run() {
Looper.prepare()
// Toast.makeText(mContext, "很抱歉,程序出现异常", Toast.LENGTH_LONG).show()
Looper.loop()
}
}.start()
KLog.e(tag, "crash!!!", ex)
collectDeviceInfo(mContext)
saveCrashInfoFile(ex)
SystemClock.sleep(3000L)
} catch (var3: Exception) {
var3.printStackTrace()
}
true
}
}
/**
* 收集设备信息
* @param ctx 应用上下文
*/
private fun collectDeviceInfo(ctx: Context?) {
try {
val pm = ctx!!.packageManager
val pi = pm.getPackageInfo(ctx.packageName, PackageManager.GET_ACTIVITIES)
if (pi != null) {
val versionName = pi.versionName + ""
val versionCode = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
pi.longVersionCode.toString() + ""
} else {
pi.versionCode.toString()
}
infoMap["versionName"] = versionName
infoMap["versionCode"] = versionCode
}
} catch (var9: PackageManager.NameNotFoundException) {
KLog.e(tag, "an error occured when collect package info", var9)
}
val fields = Build::class.java.declaredFields
val var12 = fields.size
for (var13 in 0 until var12) {
val field = fields[var13]
try {
field.isAccessible = true
infoMap[field.name] = field?.get(null).toString()
} catch (var8: Exception) {
KLog.e(tag, "an error occur when collect crash info", var8)
}
}
}
/**
* 保存崩溃信息到文件
* @param ex 异常信息
* @return 文件名
*/
@Throws(Exception::class)
private fun saveCrashInfoFile(ex: Throwable): String? {
val sb = StringBuffer()
return try {
val date = sDateFormat.format(Date())
sb.append("\r\n" + date + "\n")
val var5: Iterator<*> = infoMap.entries.iterator()
var result: String
while (var5.hasNext()) {
val (key1, value) = var5.next() as Map.Entry<*, *>
val key = key1 as String
result = value as String
sb.append("$key=$result\n")
}
val writer: Writer = StringWriter()
val printWriter = PrintWriter(writer)
ex.printStackTrace(printWriter)
var cause = ex.cause
while (cause != null) {
cause.printStackTrace(printWriter)
cause = cause.cause
}
printWriter.flush()
printWriter.close()
result = writer.toString()
sb.append(result)
writeFile(sb.toString())
} catch (var10: Exception) {
KLog.e(tag, "an error occured while writing file...", var10)
sb.append("an error occured while writing file...\r\n")
writeFile(sb.toString())
null
}
}
/**
* 将崩溃信息写入文件
* @param sb 崩溃信息
* @return 文件名
*/
@Throws(Exception::class)
private fun writeFile(sb: String): String {
val time = sDateFormat.format(Date())
val fileName = "crash-$time.log"
val path = getPrivateErrDir()
val fos = FileOutputStream(path + fileName, true)
fos.write(sb.toByteArray())
fos.flush()
fos.close()
return fileName
}
/**
* 获取崩溃日志保存目录
* @return 目录路径
*/
private fun getPrivateErrDir(): String? {
val str = KLog.LOG_DIR
val filePath: String =
if ("mounted" != Environment.getExternalStorageState() && Environment.isExternalStorageRemovable()) {
mContext!!.filesDir.path + str + "/"
} else {
mContext!!.getExternalFilesDir(null as String?)!!.path + str + "/"
}
val file = File(filePath)
return if (!file.exists() && !file.mkdirs()) null else filePath
}
}
4.3 xlog简单封装
- xlog工具类简单封装管理
/**
* 日志工具类
* @see https://github.com/elvishew/xLog/blob/master/README_ZH.md
*/
object KLog {
private const val isEncryptLog = false
private const val DEFAULT_TAG = "K_LOG"
const val LOG_DIR = "/kLogs/"
private const val ZIP_FILE = "androidLogs.zip"
private lateinit var fullLogDir: String
private lateinit var fullLogZip: String
fun init(context: Context) {
val fileDir = context.getExternalFilesDir(null)?.path + ""
fullLogDir = fileDir + LOG_DIR
fullLogZip = fileDir + "/${System.currentTimeMillis()}-" + ZIP_FILE
val config = LogConfiguration.Builder()
.logLevel(LogLevel.ALL)
.tag(DEFAULT_TAG).build()
val filePrinter = FilePrinter.Builder(fullLogDir)
.fileNameGenerator(FramesFileNameGenerator(AppUtils.getAppVersionName()))
//缓存10天日志
.cleanStrategy(FileLastModifiedCleanStrategy(10L * 24L * 60L * 60L * 1000L))
.flattener(FramesFlattener())
.build()
if (true) {
XLog.init(config, filePrinter, AndroidPrinter(true, 1500))
} else {
//正式环境不打印console.log
XLog.init(config, filePrinter)
}
}
/**
* Log an object with level [LogLevel.VERBOSE].
*
* @param object the object to log
* @see LogConfiguration.Builder.addObjectFormatter
* @since 1.1.0
*/
fun v(`object`: Any?) {
if (isEncryptLog) {
XLog.v(DEFAULT_TAG, `object`)
} else {
XLog.v(`object`)
}
}
/**
* Log an array with level [LogLevel.VERBOSE].
*
* @param array the array to log
*/
fun v(array: Array<Any?>?) {
if (isEncryptLog) {
XLog.v(DEFAULT_TAG, array)
} else {
XLog.v(array)
}
}
/**
* Log a message with level [LogLevel.VERBOSE].
*
* @param format the format of the message to log
* @param args the arguments of the message to log
*/
fun v(format: String?, vararg args: Any?) {
if (isEncryptLog) {
XLog.v(DEFAULT_TAG, args)
} else {
XLog.v(format, args)
}
}
/**
* Log a message with level [LogLevel.VERBOSE].
*
* @param msg the message to log
*/
fun v(msg: String?) {
if (isEncryptLog) {
XLog.v(DEFAULT_TAG, msg)
} else {
XLog.v(msg)
}
}
/**
* Log a message and a throwable with level [LogLevel.VERBOSE].
*
* @param msg the message to log
* @param tr the throwable to be log
*/
fun v(msg: String?, tr: Throwable?) {
if (isEncryptLog) {
XLog.v(DEFAULT_TAG, tr)
} else {
XLog.v(msg, tr)
}
}
/**
* Log an object with level [LogLevel.DEBUG].
*
* @param object the object to log
* @see LogConfiguration.Builder.addObjectFormatter
* @since 1.1.0
*/
fun d(`object`: Any?) {
if (isEncryptLog) {
XLog.d(DEFAULT_TAG, `object`)
} else {
XLog.d(`object`)
}
}
/**
* Log an array with level [LogLevel.DEBUG].
*
* @param array the array to log
*/
fun d(array: Array<Any?>?) {
if (isEncryptLog) {
XLog.d(DEFAULT_TAG, array)
} else {
XLog.d(array)
}
}
/**
* Log a message with level [LogLevel.DEBUG].
*
* @param msg the message to log
*/
@JvmStatic
fun d(msg: String?) {
if (isEncryptLog) {
XLog.d(DEFAULT_TAG, msg)
} else {
XLog.d(msg)
}
}
/**
* Log a message and a throwable with level [LogLevel.DEBUG].
*
* @param msg the message to log
* @param tr the throwable to be log
*/
fun d(msg: String?, tr: Throwable?) {
if (isEncryptLog) {
XLog.d(DEFAULT_TAG, tr)
} else {
XLog.d(msg, tr)
}
}
/**
* Log a message with level [LogLevel.DEBUG].
*
* @param tag custom tag
* @param msg the message to log
*/
fun d(tag: String, msg: String?) {
if (isEncryptLog) {
XLog.d(DEFAULT_TAG, msg)
} else {
XLog.tag(tag).d(msg)
}
}
/**
* Log an object with level [LogLevel.INFO].
*
* @param object the object to log
* @see LogConfiguration.Builder.addObjectFormatter
* @since 1.1.0
*/
fun i(`object`: Any?) {
if (isEncryptLog) {
XLog.i(DEFAULT_TAG, `object`)
} else {
XLog.i(`object`)
}
}
/**
* Log an array with level [LogLevel.INFO].
*
* @param array the array to log
*/
fun i(array: Array<Any?>?) {
if (isEncryptLog) {
XLog.i(DEFAULT_TAG, array)
} else {
XLog.i(array)
}
}
/**
* Log a message with level [LogLevel.INFO].
*
* @param format the format of the message to log
* @param args the arguments of the message to log
*/
@JvmStatic
fun i(format: String?, vararg args: Any?) {
if (isEncryptLog) {
XLog.i(DEFAULT_TAG, args)
} else {
XLog.i(format, args)
}
}
/**
* Log a message with level [LogLevel.INFO].
*
* @param msg the message to log
*/
fun i(msg: String?) {
if (isEncryptLog) {
XLog.i(DEFAULT_TAG, msg)
} else {
XLog.i(msg)
}
}
/**
* Log a message with level [LogLevel.INFO].
*
* @param tag custom tag
* @param msg the message to log
*/
fun i(tag: String, msg: String?) {
if (isEncryptLog) {
XLog.i(DEFAULT_TAG, msg)
} else {
XLog.tag(tag).i(msg)
}
}
/**
* Log a message and a throwable with level [LogLevel.INFO].
*
* @param msg the message to log
* @param tr the throwable to be log
*/
fun i(msg: String?, tr: Throwable?) {
if (isEncryptLog) {
XLog.i(DEFAULT_TAG, tr)
} else {
XLog.i(msg, tr)
}
}
/**
* Log an object with level [LogLevel.WARN].
*
* @param object the object to log
* @see LogConfiguration.Builder.addObjectFormatter
* @since 1.1.0
*/
@JvmStatic
fun w(`object`: Any?) {
if (isEncryptLog) {
XLog.w(DEFAULT_TAG, `object`)
} else {
XLog.w(`object`)
}
}
/**
* Log an array with level [LogLevel.WARN].
*
* @param array the array to log
*/
@JvmStatic
fun w(array: Array<Any?>?) {
if (isEncryptLog) {
XLog.w(DEFAULT_TAG, array)
} else {
XLog.w(array)
}
}
/**
* Log a message with level [LogLevel.WARN].
*
* @param format the format of the message to log
* @param args the arguments of the message to log
*/
@JvmStatic
fun w(format: String?, vararg args: Any?) {
if (isEncryptLog) {
XLog.w(DEFAULT_TAG, args)
} else {
XLog.w(format, args)
}
}
/**
* Log a message with level [LogLevel.WARN].
*
* @param msg the message to log
*/
@JvmStatic
fun w(msg: String?) {
if (isEncryptLog) {
XLog.w(DEFAULT_TAG, msg)
} else {
XLog.w(msg)
}
}
/**
* Log a message and a throwable with level [LogLevel.WARN].
*
* @param msg the message to log
* @param tr the throwable to be log
*/
@JvmStatic
fun w(msg: String?, tr: Throwable?) {
if (isEncryptLog) {
XLog.w(DEFAULT_TAG, tr)
} else {
XLog.w(msg, tr)
}
}
/**
* Log an object with level [LogLevel.ERROR].
*
* @param object the object to log
* @see LogConfiguration.Builder.addObjectFormatter
* @since 1.1.0
*/
@JvmStatic
fun e(`object`: Any?) {
if (isEncryptLog) {
XLog.e(DEFAULT_TAG, `object`)
} else {
XLog.e(`object`)
}
}
/**
* Log an array with level [LogLevel.ERROR].
*
* @param array the array to log
*/
@JvmStatic
fun e(array: Array<Any?>?) {
if (isEncryptLog) {
XLog.e(DEFAULT_TAG, array)
} else {
XLog.e(array)
}
}
/**
* Log a message with level [LogLevel.ERROR].
*
* @param format the format of the message to log
* @param args the arguments of the message to log
*/
@JvmStatic
fun e(format: String?, vararg args: Any?) {
if (isEncryptLog) {
XLog.e(DEFAULT_TAG, args)
} else {
XLog.e(format, args)
}
}
/**
* Log a message with level [LogLevel.ERROR].
*
* @param msg the message to log
*/
@JvmStatic
fun e(msg: String?) {
if (isEncryptLog) {
XLog.e(DEFAULT_TAG, msg)
} else {
XLog.e(msg)
}
}
/**
* Log a message and a throwable with level [LogLevel.ERROR].
*
* @param msg the message to log
* @param tr the throwable to be log
*/
@JvmStatic
fun e(msg: String?, tr: Throwable?) {
if (isEncryptLog) {
XLog.e(DEFAULT_TAG, tr)
} else {
XLog.e(msg, tr)
}
}
/**
* Log a message with level [LogLevel.ERROR].
*
* @param tag custom tag
* @param msg the message to log
*/
fun e(tag: String, msg: String?) {
if (isEncryptLog) {
XLog.e(DEFAULT_TAG, msg)
} else {
XLog.tag(tag).e(msg)
}
}
/**
* Log an object with specific log level.
*
* @param logLevel the specific log level
* @param object the object to log
* @see LogConfiguration.Builder.addObjectFormatter
* @since 1.4.0
*/
@JvmStatic
fun log(logLevel: Int, `object`: Any?) {
if (isEncryptLog) {
XLog.d(DEFAULT_TAG, `object`)
} else {
XLog.log(logLevel, `object`)
}
}
/**
* Log an array with specific log level.
*
* @param logLevel the specific log level
* @param array the array to log
* @since 1.4.0
*/
fun log(logLevel: Int, array: Array<Any?>?) {
if (isEncryptLog) {
XLog.d(DEFAULT_TAG, array)
} else {
XLog.log(logLevel, array)
}
}
/**
* Log a message with specific log level.
*
* @param logLevel the specific log level
* @param format the format of the message to log
* @param args the arguments of the message to log
* @since 1.4.0
*/
fun log(logLevel: Int, format: String?, vararg args: Any?) {
if (isEncryptLog) {
XLog.d(DEFAULT_TAG, args)
} else {
XLog.log(logLevel, format, args)
}
}
/**
* Log a message with specific log level.
*
* @param logLevel the specific log level
* @param msg the message to log
* @since 1.4.0
*/
fun log(logLevel: Int, msg: String?) {
if (isEncryptLog) {
XLog.d(DEFAULT_TAG, msg)
} else {
XLog.log(logLevel, msg)
}
}
/**
* Log a message and a throwable with specific log level.
*
* @param logLevel the specific log level
* @param msg the message to log
* @param tr the throwable to be log
* @since 1.4.0
*/
fun log(logLevel: Int, msg: String?, tr: Throwable?) {
if (isEncryptLog) {
XLog.d(DEFAULT_TAG, tr)
} else {
XLog.log(logLevel, msg, tr)
}
}
/**
* Log a JSON string, with level [LogLevel.DEBUG] by default.
*
* @param json the JSON string to log
*/
fun json(json: String?) {
if (isEncryptLog) {
XLog.d(DEFAULT_TAG, json)
} else {
XLog.json(json)
}
}
/**
* Log a XML string, with level [LogLevel.DEBUG] by default.
*
* @param xml the XML string to log
*/
fun xml(xml: String?) {
if (isEncryptLog) {
XLog.d(DEFAULT_TAG, xml)
} else {
XLog.xml(xml)
}
}
/**
* compress
*/
fun compress(folderPath: String, zipFilePath: String) {
try {
LogUtils.compress(folderPath, zipFilePath)
} catch (e: Exception) {
e.printStackTrace()
}
}
fun compressLog() {
try {
LogUtils.compress(fullLogDir, fullLogZip)
} catch (e: Exception) {
e.printStackTrace()
}
}
fun getLogZipFile(): MutableList<File> {
val file = File(fullLogZip)
if (file.exists()) {
return mutableListOf<File>().also {
it.add(file)
}
}
return mutableListOf()
}
fun hasCrashLog(): Boolean {
val dir = File(fullLogDir)
return if (dir.exists() && dir.isDirectory) {
dir.listFiles()?.any {
it.name.startsWith("crash-")
} == true
} else {
false
}
}
fun deleteLogFile() {
try {
File(fullLogZip).delete()
val dir = File(fullLogDir)
dir.listFiles()?.filter {
it.name.startsWith("crash-")
}?.forEach {
it.delete()
}
} catch (e: Exception) {
e(e.toString())
}
}
}
4.4 本地初始化
- 需要将app模块下的AndroidManifest.xml中属性修改为 android:name=“.InitApplication”
class InitApplication : Application() {
override fun onCreate() {
super.onCreate()
appInit()
}
private fun appInit() {
// 本地日志初始化
KLog.init(this)
FramesCrashHandler.instance.init(this)
}
到此,xlog的日志就可以使用了,只需要在其他模块引入一下log模块就可以调用。如:KLog.d("hello,xlog")