本篇介绍Log的实现,实际上在Rust中已经有很成熟的log
库,同时也有支持Android log的crate:android_log
,但是使用下来发现两个问题:
- rust的JNI库也使用了log库,如果log级别设置在verbose,在发生JNI调用时会产生大量JNI的log,将库本身的日志淹掉;
android_log
是使用NDK提供的API将rust的log输出到了Android的logcat,应用程序没办法重新定向,不够灵活。
综合以上两个原因,本文决定重新设计一套轻量级的log系统。
先说思路,参考log库,我们的log系统也提供类似 debug!
,verbose!
的宏调用,要提供重定向的能力,同时拥有一个简单的默认实现。
先是抽象trait:
pub trait Logger: Debug + Send {
fn verbose(&self, log_str: String);
fn info(&self, log_str: String);
fn debug(&self, log_str: String);
fn warn(&self, log_str: String);
fn error(&self, log_str: String);
}
我们需要一个统一的log输出方法,以方便后续宏定义,同时需要全局单例的默认实现,也要允许替换默认实现,同时log的实现也不应该阻塞调用者,所以log的实际调用需要发送到io线程执行。
struct LogWrapper {
io_looper: IOLooper,
}
impl LogWrapper {
fn new() -> Self {
// LogWriter是实际写log能力的封装,下文将介绍其实现
let log_writer = LogWriter { inner_logger: None };
let io_looper = IOLooper::new(log_writer);
LogWrapper { io_looper }
}
fn format_log(&self, level: LogLevel, tag: &str, content: Arguments) {
let pid = process::id();
let tid = thread::current().id();
let time = local_time();
let log_str = format!("[{}] {}", tag, content);
// 将写log的调用发送到io线程执行
self.io_looper
.post(move |callback| {
let writer = callback.downcast_ref::<LogWriter>().unwrap();
// 在io线程调用writer的写操作
writer.write(level, time, pid, tid, log_str);
})
.unwrap();
}
}
// 全局单例
static LOG_WRAPPER: Lazy<LogWrapper> = Lazy::new(LogWrapper::new);
// 全局唯一log入口,按level过滤,然后输出到内部log实现
pub fn log(level: LogLevel, tag: &str, args: Arguments) {
if get_log_level() < level as i32 {
return;
}
LOG_WRAPPER.format_log(level, tag, args);
}
static LOG_LEVEL: AtomicI32 = AtomicI32::new(5);
// 设置log级别
pub fn set_log_level(level: LogLevel) {
LOG_LEVEL.swap(level as i32, Ordering::Release);
}
// 重定向log输出,传入一个实现了Logger的类型
pub fn set_logger(log_impl: Option<Box<dyn Logger>>) {
// 这里将log_impl的所有权转移到了子线程,所以需要Logger实现Send
LOG_WRAPPER
.io_looper
.post(|callback| {
let writer = callback.downcast_mut::<LogWriter>().unwrap();
// 释放旧的logger实例,这里其实不用手动释放,
// 重新给writer.inner_logger赋值时旧值会被drop,
// 但是手动drop代码可读性更好
drop(writer.inner_logger.take());
// 替换writer内部的logger实现
writer.inner_logger = log_impl;
})
.unwrap();
}
LogWriter
的结构如下:
struct LogWriter {
// 内部装了一个Logger的实现,上层可以替换这个实现以重定向log
inner_logger: Option<Box<dyn Logger>>,
}
impl LogWriter {
fn write(&self, level: LogLevel, time: String, pid: u32, tid: ThreadId, log_str: String) {
// 取内部实现,如果有则重定向log到此实现,如果没有则打印到标准输出
match &self.inner_logger {
Some(_) => self.redirect(level, log_str),
None => {
println!("{} {}-{:?} {} {}", time, pid, tid, level, log_str)
}
}
}
fn redirect(&self, level: LogLevel, log_str: String) {
let logger = self.inner_logger.as_ref().unwrap();
match level {
LogLevel::Error => logger.error(log_str),
LogLevel::Warn => logger.warn(log_str),
LogLevel::Info => logger.info(log_str),
LogLevel::Debug => logger.debug(log_str),
LogLevel::Verbose => logger.verbose(log_str),
_ => {}
}
}
}
// 实现IOLooper的Callback
impl Callback for LogWriter {}
至此已经可以使用 crate::log::logger::log
调用来打日志了。
接下来是宏定义,为了crate内全局使用,这部分写在lib.rs之中:
macro_rules! log {
($level:expr, $tag:expr, $($arg:tt)+) => {{
// 调用log mod内部的方法
crate::log::logger::log($level, $tag, format_args!($($arg)+))
}}
}
macro_rules! error {
($tag:expr, $($arg:tt)+) => {{
// 调用log宏
log!(crate::LogLevel::Error, $tag, $($arg)+)
}}
}
#[allow(unused_macros)]
macro_rules! warn {
($tag:expr, $($arg:tt)+) => {{
log!(crate::LogLevel::Warn, $tag, $($arg)+)
}}
}
// info,debug,verbose类似
......
接下来就能在crate内随意使用了:
info!("MMKV", "hello world, number: {}", 1)
默认输出如下:
2024-01-21T10:07:37.629+00:00 77399-ThreadId(9) I [MMKV] hello world, number: 1
通过
MMKV::set_logger(log_impl: Box<dyn crate::Logger>)
也能将log重定向到自己的实现。
下篇将介绍消息队列IOLooper
。