本篇介绍Log的实现,实际上在Rust中已经有很成熟的log库,同时也有支持Android log的crate:android_log,但是使用下来发现两个问题:

  1. rust的JNI库也使用了log库,如果log级别设置在verbose,在发生JNI调用时会产生大量JNI的log,将库本身的日志淹掉;
  2. 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