Flutter 日志模块之参考设计
目录
4.1,2 日志管理器 (log_manager.dart)
4.3.1 异步缓冲区机制 (file_logger.dart)
4.3.2 控制台美化 (console_logger.dart)
1. 📂 简介
在 Flutter 项目开发中,日志系统是调试和线上排查的“眼睛”。很多开发者习惯直接使用 print 或 debugPrint,但在中大型项目中,这显然不够:
-
无法持久化:线上用户报错,没日志等于盲飞。
-
性能隐患:高频的 IO 操作会阻塞 UI 线程。
-
职责混乱:打印逻辑散落在业务代码各处,难以统一开关。
今天分享一套三层架构的日志模块设计方案,支持异步缓冲区写入、多处理器分发以及自动清理机制。
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 适配了多环境需求。
另外,由于本人能力有限,如有错误,敬请批评指正,谢谢。
更多推荐
所有评论(0)