目录

1. 📂 简介

2. 🔱 核心架构设计

3. 💠 使用步骤

3.1 添加依赖

3.2 初始化

3.3 打印日志

4. ⚛️ 工程化封装

4.1 第一层:优雅的调用入口

4.1.1 快捷函数 (log_utils.dart)

4.1,2 日志管理器 (log_manager.dart)

4.2 第二层:配置与中控逻辑

4.2.1 灵活的配置 (log_config.dart)

4.2.2 逻辑中控 (log_core.dart)

4.3 第三层:性能优化与实现

4.3.1 异步缓冲区机制 (file_logger.dart)

4.3.2 控制台美化 (console_logger.dart)

5. ✅ 小结


1. 📂 简介

        在 Flutter 项目开发中,日志系统是调试和线上排查的“眼睛”。很多开发者习惯直接使用 print 或 debugPrint,但在中大型项目中,这显然不够:

  1. 无法持久化:线上用户报错,没日志等于盲飞。

  2. 性能隐患:高频的 IO 操作会阻塞 UI 线程。

  3. 职责混乱:打印逻辑散落在业务代码各处,难以统一开关。

        今天分享一套三层架构的日志模块设计方案,支持异步缓冲区写入、多处理器分发以及自动清理机制

2. 🔱 核心架构设计

        为了降低维护成本,我们将日志系统分为三层,各司其职:

  • 第一层:门面层 (Facade):提供最简单的全局函数和单例入口,让开发者“无感”调用。

  • 第二层:核心层 (Core):负责配置解析、级别过滤、逻辑分发。

  • 第三层:实现层 (Handler):具体的落地执行者,如控制台着色打印、文件分片存储。

3. 💠 使用步骤

3.1 添加依赖

# pubspec.yaml
dependencies:
  #  用于国际化(i18n)和本地化(l10n)操作,如日期、时间、数字格式化等。
  intl: ^0.20.2
  #  用于在 Flutter 应用中进行日志记录。它支持多种输出形式,如控制台、文件、以及彩色日志。
  logger: ^1.1.0
  #  提供了在设备上查找常用文件目录的方法,如获取应用的文档目录、临时目录等。
  path_provider: ^2.1.3

3.2 初始化

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  
  // 根据编译模式选择配置
  await LogManager().initialize(
    config: kReleaseMode ? LogConfig.production() : LogConfig.defaultConfig()
  );
  
  runApp(const MyApp());
}

3.3 打印日志

logI("App Running...");

4. ⚛️ 工程化封装

4.1 第一层:优雅的调用入口

        我们不希望在业务代码里看到复杂的初始化逻辑,因此通过 log_utils.dart 提供一套快捷函数。

4.1.1 快捷函数 (log_utils.dart)

///
/// Description:    日志工具类
/// CreateDate:     2025/12/17 11:41
/// Author:         agg
///
void logD(String message, {String? tag = "MoJieGlass"}) {
  LogManager.debug(tag != null ? '[$tag] $message' : message);
}

void logI(String message, {String? tag = "MoJieGlass"}) {
  LogManager.info(tag != null ? '[$tag] $message' : message);
}

void logE(String message, {String? tag = "MoJieGlass"}) {
  LogManager.error(tag != null ? '[$tag] $message' : message);
}

4.1,2 日志管理器 (log_manager.dart)

        LogManager 采用单例模式,作为整个系统的“总线”,负责生命周期管理。

///
/// Description:    日志管理器(单例)
/// CreateDate:     2025/12/17 11:40
/// Author:         agg
///
class LogManager {
  // 声明静态变量
  static final LogManager _instance = LogManager._internal();

  // 工厂构造函数
  factory LogManager() => _instance;

  // 私有命名构造函数
  LogManager._internal();

  LogCore? _logCore;
  LogConfig? _config;

  /// 初始化日志系统
  Future<void> initialize({LogConfig? config}) async {
    if (_logCore != null) {
      await _logCore!.dispose();
    }

    _config = config ?? LogConfig.defaultConfig();
    _logCore = LogCore(config: _config!);
    await _logCore!.initialize();
  }

  /// 获取日志核心实例
  LogCore get logger {
    if (_logCore == null) {
      throw StateError('LogManager not initialized. Call initialize() first.');
    }
    return _logCore!;
  }

  /// 获取配置
  LogConfig? get config => _config;

  /// 是否已初始化
  bool get isInitialized => _logCore != null;

  /// 快捷访问方法
  static void verbose(String message) {
    _instance.logger.verbose(message);
  }

  static void debug(String message) {
    _instance.logger.debug(message);
  }

  static void info(String message) {
    _instance.logger.info(message);
  }

  static void warning(String message) {
    _instance.logger.warning(message);
  }

  static void error(String message) {
    _instance.logger.error(message);
  }

  static void wtf(String message) {
    _instance.logger.wtf(message);
  }

  /// 网络日志
  static void network(
    String method,
    String url, {
    dynamic data,
    Map<String, dynamic>? headers,
    int? statusCode,
    String? response,
    int duration = 0,
  }) {
    _instance.logger.network(
      method,
      url,
      data: data,
      headers: headers,
      statusCode: statusCode,
      response: response,
      duration: duration,
    );
  }

  /// 销毁资源
  Future<void> dispose() async {
    await _logCore?.dispose();
    _logCore = null;
    _config = null;
  }
}

4.2 第二层:配置与中控逻辑

        这一层是整个模块的大脑,决定了日志“去哪里”和“存多久”。

4.2.1 灵活的配置 (log_config.dart)

        通过 LogConfig,我们可以轻松实现:开发环境看控制台,生产环境关掉控制台只存文件。

///
/// Description:    日志配置类
/// CreateDate:     2025/12/17 11:19
/// Author:         agg
///
class LogConfig {
  /// 是否启用文件日志
  final bool enableFileLog;

  /// 是否启用控制台日志
  final bool enableConsoleLog;

  /// 最小日志级别(低于此级别的日志不会被记录)
  final Level minLevel;

  /// 日志目录路径,为空则使用默认目录
  final String? logDirectory;

  /// 单个日志文件最大大小(字节),默认 5MB
  final int maxFileSize;

  /// 内存缓冲区最大日志条数
  final int maxBufferSize;

  /// 日志文件名前缀
  final String filePrefix;

  /// 日志文件扩展名
  final String fileExtension;

  /// 保留日志文件的天数
  final int daysToKeep;

  /// 默认配置
  factory LogConfig.defaultConfig() => LogConfig(
    enableFileLog: true,
    enableConsoleLog: true,
    minLevel: Level.verbose,
    maxFileSize: 5 * 1024 * 1024,
    maxBufferSize: 100,
    daysToKeep: 7,
  );

  /// 生产环境配置
  factory LogConfig.production() => LogConfig(
    enableFileLog: true,
    enableConsoleLog: true,
    // 生产环境关闭控制台日志
    minLevel: Level.info,
    // 生产环境只记录info及以上级别,10MB文件大小
    maxFileSize: 10 * 1024 * 1024,
    maxBufferSize: 200,
    // 生产环境保留更久
    daysToKeep: 30,
  );

  const LogConfig({
    required this.enableFileLog,
    required this.enableConsoleLog,
    this.minLevel = Level.verbose,
    this.logDirectory,
    this.maxFileSize = 5 * 1024 * 1024,
    this.maxBufferSize = 100,
    this.filePrefix = 'app',
    this.fileExtension = 'log',
    this.daysToKeep = 7,
  });

  /// 验证配置
  void validate() {
    if (maxFileSize <= 0) {
      throw ArgumentError('maxFileSize must be greater than 0');
    }
    if (maxBufferSize <= 0) {
      throw ArgumentError('maxBufferSize must be greater than 0');
    }
    if (daysToKeep <= 0) {
      throw ArgumentError('daysToKeep must be greater than 0');
    }
  }
}

4.2.2 逻辑中控 (log_core.dart)

        LogCore 持有两个处理器:ConsoleLogger 和 FileLogger。

///
/// Description:    核心日志类
/// CreateDate:     2025/12/17 11:39
/// Author:         agg
///
class LogCore {
  final LogConfig config;
  late ConsoleLogger _consoleLogger;
  late FileLogger _fileLogger;
  bool _isInitialized = false;
  bool _isDisposed = false;

  LogCore({required this.config}) {
    config.validate();
    _initLoggers();
  }

  void _initLoggers() {
    if (config.enableConsoleLog) {
      _consoleLogger = ConsoleLogger();
    }

    if (config.enableFileLog) {
      _fileLogger = FileLogger(
        customDirectory: config.logDirectory,
        maxFileSize: config.maxFileSize,
        maxBufferSize: config.maxBufferSize,
        filePrefix: config.filePrefix,
        fileExtension: config.fileExtension,
        daysToKeep: config.daysToKeep,
      );
    }
  }

  /// 初始化日志系统
  Future<void> initialize() async {
    if (_isDisposed) {
      throw StateError('LogCore has been disposed');
    }

    try {
      if (config.enableFileLog) {
        await _fileLogger.initialize();
      }
      _isInitialized = true;
    } catch (e) {
      _isInitialized = false;
      // 文件日志初始化失败不影响控制台日志
      if (config.enableConsoleLog) {
        _consoleLogger.log(
          level: Level.error,
          message: 'Failed to initialize file logger: $e',
        );
      }
    }
  }

  void log({required Level level, required String message}) {
    if (!_isInitialized || _isDisposed) return;
    if (level.index < config.minLevel.index) return;

    try {
      // 控制台日志
      if (config.enableConsoleLog) {
        _consoleLogger.log(level: level, message: message);
      }

      // 文件日志
      if (config.enableFileLog) {
        _fileLogger.log(level: level, message: message);
      }
    } catch (e) {
      // 日志记录失败不应影响应用运行
      print('Logging failed: $e');
    }
  }

  void verbose(String message) {
    log(level: Level.verbose, message: message);
  }

  void debug(String message) {
    log(level: Level.debug, message: message);
  }

  void info(String message) {
    log(level: Level.info, message: message);
  }

  void warning(String message) {
    log(level: Level.warning, message: message);
  }

  void error(String message) {
    log(level: Level.error, message: message);
  }

  void wtf(String message) {
    log(level: Level.wtf, message: message);
  }

  /// 网络请求日志
  void network(
    String method,
    String url, {
    dynamic data,
    Map<String, dynamic>? headers,
    int? statusCode,
    String? response,
    int duration = 0,
  }) {
    final message =
        '''
[NETWORK] $method $url
Duration: ${duration}ms
Status: $statusCode
${headers != null ? 'Headers: $headers' : ''}
${data != null ? 'Request: $data' : ''}
${response != null ? 'Response: $response' : ''}
    '''
            .trim();

    info(message);
  }

  /// 获取日志文件
  Future<List<File>> getLogFiles() async {
    if (!_isInitialized || _isDisposed || !config.enableFileLog) {
      return [];
    }
    return await _fileLogger.getLogFiles();
  }

  /// 获取最近日志
  Future<String> getRecentLogs({int lines = 100, String? filePath}) async {
    if (!_isInitialized || _isDisposed || !config.enableFileLog) {
      return '';
    }
    return await _fileLogger.getRecentLogs(lines: lines, filePath: filePath);
  }

  /// 强制刷新缓冲区
  Future<void> flush() async {
    if (!_isInitialized || _isDisposed || !config.enableFileLog) {
      return;
    }
    await _fileLogger.flush();
  }

  /// 销毁资源
  Future<void> dispose() async {
    if (_isDisposed) return;

    _isDisposed = true;

    try {
      await _fileLogger.dispose();
    } catch (e) {
      // 忽略清理错误
    }

    try {
      _consoleLogger.dispose();
    } catch (e) {
      // 忽略清理错误
    }
  }
}

4.3 第三层:性能优化与实现

        最考验功力的地方在于 FileLogger 的实现。频繁打开/关闭文件是非常耗电且影响性能的。

4.3.1 异步缓冲区机制 (file_logger.dart)

        为了极致性能,我们引入了 Buffer (缓冲区)。日志产生后不立即写磁盘,而是先存在内存里。

  • 触发条件1:缓冲区达到 100 条日志。

  • 触发条件2:定时器每隔 1s 强制刷新。

///
/// Description:    文件日志处理器
/// CreateDate:     2025/12/17 11:37
/// Author:         agg
///
class FileLogger {
  final String? customDirectory;
  final int maxFileSize;
  final int maxBufferSize;
  final String filePrefix;
  final String fileExtension;
  final int daysToKeep;

  File? _currentFile;
  final List<String> _buffer = [];
  Timer? _periodicTimer;
  late String _logDirectory;
  final DateFormat _dateFormat = DateFormat('yyyy-MM-dd');
  final DateFormat _timeFormat = DateFormat('HH:mm:ss.SSS');
  bool _isInitialized = false;
  bool _isDisposed = false;

  FileLogger({
    this.customDirectory,
    this.maxFileSize = 5 * 1024 * 1024,
    this.maxBufferSize = 100,
    this.filePrefix = 'app',
    this.fileExtension = 'log',
    this.daysToKeep = 7,
  });

  /// 初始化文件日志
  Future<void> initialize() async {
    if (_isDisposed) {
      throw StateError('FileLogger has been disposed');
    }

    try {
      Directory directory;
      if (customDirectory != null && customDirectory!.isNotEmpty) {
        directory = Directory(customDirectory!);
      } else {
        Directory? appDir;
        if (Platform.isAndroid) {
          appDir = await getExternalStorageDirectory();
        } else {
          appDir = await getApplicationDocumentsDirectory();
        }
        directory = Directory('${appDir?.path}/logs');
        // Android 日志路径: /sdcard/Android/data/<package_name>/files/logs/
        // iOS 日志路径: <AppHome>/Documents/logs
      }

      _logDirectory = directory.path;

      if (!await directory.exists()) {
        await directory.create(recursive: true);
      }

      await _setupLogFile();
      await _cleanOldLogs();

      _isInitialized = true;

      // 写入初始化日志
      await _writeToBuffer(
        '========== Log System Initialized ==========',
        skipBuffer: true,
      );

      // 启动定时器1s定期刷新缓冲区
      _periodicTimer = Timer.periodic(Duration(seconds: 1), (_) {
        if (_buffer.isNotEmpty) {
          _flushBuffer();
        }
      });
    } catch (e) {
      _isInitialized = false;
      rethrow;
    }
  }

  /// 设置日志文件
  Future<void> _setupLogFile() async {
    final now = DateTime.now();
    final dateStr = _dateFormat.format(now);
    final baseName = '${_logDirectory}/$filePrefix\_$dateStr';

    // 检查是否存在今天的日志文件
    String filePath = '$baseName.$fileExtension';
    File file = File(filePath);

    // 如果文件存在且超过大小限制,创建带时间戳的新文件
    if (await file.exists()) {
      final length = await file.length();
      if (length >= maxFileSize) {
        final timeStr = DateFormat('HH-mm-ss').format(now);
        filePath = '${baseName}_$timeStr.$fileExtension';
        file = File(filePath);
      }
    }

    _currentFile = file;
  }

  /// 写入日志
  Future<void> log({required Level level, required String message}) async {
    if (!_isInitialized || _isDisposed) return;

    try {
      final timestamp = _timeFormat.format(DateTime.now());
      var levelStr = "";
      if (level == Level.info) {
        levelStr = "INFOS";
      } else {
        levelStr = level.name.toUpperCase();
      }

      await _writeToBuffer('[$timestamp][$levelStr] $message');
    } catch (e) {
      // 文件日志失败时不应影响应用运行
      print('File logger error: $e');
    }
  }

  /// 写入缓冲区
  Future<void> _writeToBuffer(
    String logEntry, {
    bool skipBuffer = false,
  }) async {
    _buffer.add('$logEntry\n');

    // 如果缓冲区满了或者需要立即写入,则刷新到文件
    if (_buffer.length >= maxBufferSize || skipBuffer) {
      await _flushBuffer();
    }
  }

  /// 刷新缓冲区到文件
  Future<void> _flushBuffer() async {
    if (_buffer.isEmpty || _currentFile == null || _isDisposed) return;

    try {
      await _currentFile!.writeAsString(
        _buffer.join(),
        mode: FileMode.append,
        flush: true,
      );
      _buffer.clear();
    } catch (e) {
      // 如果写入失败,保留缓冲区内容下次再试
      print('Failed to flush log buffer: $e');
    }
  }

  /// 清理旧日志文件
  Future<void> _cleanOldLogs() async {
    try {
      final directory = Directory(_logDirectory);
      if (!await directory.exists()) return;

      final files = await directory
          .list()
          .where((entity) => entity is File)
          .cast<File>()
          .where((file) => file.path.endsWith('.$fileExtension'))
          .toList();

      final cutoffDate = DateTime.now().subtract(Duration(days: daysToKeep));

      for (final file in files) {
        try {
          final stat = await file.stat();
          if (stat.modified.isBefore(cutoffDate)) {
            await file.delete();
          }
        } catch (e) {
          // 忽略单个文件删除失败
        }
      }
    } catch (e) {
      // 清理失败不应影响主要功能
    }
  }

  /// 获取所有日志文件
  Future<List<File>> getLogFiles() async {
    if (!_isInitialized || _isDisposed) return [];

    final directory = Directory(_logDirectory);
    if (!await directory.exists()) return [];

    try {
      final files = await directory
          .list()
          .where((entity) => entity is File)
          .cast<File>()
          .where((file) => file.path.endsWith('.$fileExtension'))
          .toList();

      // 按修改时间排序(最新的在前)
      files.sort((a, b) {
        try {
          final statA = a.statSync();
          final statB = b.statSync();
          return statB.modified.compareTo(statA.modified);
        } catch (e) {
          return 0;
        }
      });

      return files;
    } catch (e) {
      return [];
    }
  }

  /// 获取最近日志
  Future<String> getRecentLogs({int lines = 100, String? filePath}) async {
    if (!_isInitialized || _isDisposed) return '';

    try {
      File file;
      if (filePath != null) {
        file = File(filePath);
      } else {
        final files = await getLogFiles();
        if (files.isEmpty) return 'No log files found';
        file = files.first;
      }

      if (!await file.exists()) return 'Log file not found';

      final content = await file.readAsString();
      final linesList = content.split('\n');

      final start = linesList.length > lines ? linesList.length - lines : 0;
      return linesList.sublist(start).join('\n');
    } catch (e) {
      return 'Error reading logs: $e';
    }
  }

  /// 强制刷新缓冲区
  Future<void> flush() => _flushBuffer();

  /// 销毁资源
  Future<void> dispose() async {
    if (_isDisposed) return;

    _isDisposed = true;
    _periodicTimer?.cancel();
    await _flushBuffer();
    _buffer.clear();
    _currentFile = null;
  }
}

4.3.2 控制台美化 (console_logger.dart)

        通过集成 logger 库,我们可以让输出带上边框、时间戳和颜色,方便快速定位问题。

///
/// Description:    控制台日志处理器
/// CreateDate:     2025/12/17 11:34
/// Author:         agg
///
class ConsoleLogger {
  late Logger _logger;

  ConsoleLogger() {
    _initLogger();
  }

  void _initLogger() {
    _logger = Logger(
      printer: PrettyPrinter(
        methodCount: 0,
        errorMethodCount: 8,
        lineLength: 120,
        colors: false,
        printEmojis: false,
        printTime: false,
        noBoxingByDefault: true,
      ),
      filter: DevelopmentFilter(),
      level: Level.verbose,
    );
  }

  /// 记录日志到控制台
  void log({
    required Level level,
    required String message,
    dynamic error,
    StackTrace? stackTrace,
  }) {
    try {
      final fullMessage = error != null ? '$message\nError: $error' : message;

      switch (level) {
        case Level.verbose:
          _logger.v(fullMessage);
          break;
        case Level.debug:
          _logger.d(fullMessage);
          break;
        case Level.info:
          _logger.i(fullMessage);
          break;
        case Level.warning:
          _logger.w(fullMessage);
          break;
        case Level.error:
          _logger.e(fullMessage);
          break;
        case Level.wtf:
          _logger.wtf(fullMessage);
          break;
        case Level.nothing:
          break;
      }
    } catch (e) {
      // 防止日志记录本身导致应用崩溃
      print('Console logger error: $e');
    }
  }

  /// 销毁资源
  void dispose() {
    // Logger库没有dispose方法,这里预留接口
  }
}

5. ✅ 小结

        这套方案通过三层架构实现了职责分离,通过异步缓冲区解决了性能瓶颈,通过 LogConfig 适配了多环境需求。

        另外,由于本人能力有限,如有错误,敬请批评指正,谢谢。

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐