1115 字
6 分钟
Dart 在 Release 中移除日志代码
Dart 的注解(annotation/metadata)不参与编译期的代码消除(DCE, tree-shaking),不能标注一行并在 release 自动删掉。
- 想要在发布版移除调试代码,应该用常量条件 + tree-shaking 或 assert 等语言内置机制;
- 如果想在打包前物理删掉调用,可以写一个基于 AST 的小型 codemod 脚本离线改写源码。
可行做法
1. assert
assert 在 profile/release 下整个语句会被编译器移除(表达式也不会计算),是”只在调试做点事”的最简洁方案。
// 仅在 debug 执行,不会污染 release 体积与性能assert(() { Loggers.debug('something: $expensive'); // 这里的 closure 在 release 不会被创建/执行 return true;}());
适合:临时日志、检查、探针。
注意:assert 在 profile 模式也不执行。
2. 常量条件 + tree-shaking
让分支在编译时就是常量,从而让编译器把 if (false) { ... }
分支整个剪掉。
a. Flutter 场景
import 'package:flutter/foundation.dart';
if (kDebugMode) { Loggers.debug(() => 'val: ${computeSomething()}'); // 使用懒计算,避免实参副作用}
kDebugMode
/kReleaseMode
是 const,分支会在 AOT/JS 编译期被移除- 实参用
() => ...
懒传,防止在 release 也计算字符串/副作用
b. 自定义开关(—dart-define)
// dart run / flutter build 时通过 --dart-define=LOG_DEBUG=true/false 传入const bool kLogDebug = bool.fromEnvironment('LOG_DEBUG', defaultValue: !bool.fromEnvironment('dart.vm.product'));
if (kLogDebug) { Loggers.debug(() => 'val: ${computeSomething()}');}
3. 零成本 Logger 模板
避免每处都写 if (kDebugMode)
,同时保证 release 下零开销(不创建字符串、不执行函数)。
import 'package:flutter/foundation.dart';
abstract class Log { void d(Object? Function() msg); void i(Object? Function() msg); void e(Object? Function() msg);}
class _NoopLog implements Log { const _NoopLog(); @override void d(Object? Function() msg) {} @override void i(Object? Function() msg) {} @override void e(Object? Function() msg) {}}
class _StdLog implements Log { const _StdLog(); @override void d(Object? Function() msg) { debugPrint('[D] ${msg()}'); } @override void i(Object? Function() msg) { debugPrint('[I] ${msg()}'); } @override void e(Object? Function() msg) { debugPrint('[E] ${msg()}'); }}
// 编译期常量决定实现,分支会被树摇掉const bool _isRelease = kReleaseMode;const Log log = _isRelease ? _NoopLog() : _StdLog();
// 使用:// 在 release 下,closure 不会被创建(配合外层 if 更彻底),即使调用也因 Noop 方法体为空,JIT/AOT 会消除开销void example(int x) { if (!kReleaseMode) { // 避免在 release 创建 closure log.d(() => 'x squared = ${x * x}'); }}
说明:
- 仅 const 条件才能让编译器可靠剪枝
- 防止”实参副作用”的关键是懒执行(
Object? Function()
),或者在调用处加一层if (!kReleaseMode)
在打包前批量删掉 Loggers.debug(…)”
运行期方案之外,你也可以在 CI 的”预发布”步骤做一次源码改写。下面是一个最小可用的 Dart AST codemod(使用 package:analyzer
)——它会删除独立一行的 Loggers.debug(...)
语句(仅限形如一条完整 ExpressionStatement 的调用;嵌在表达式里的不处理,避免破坏语义)。
import 'dart:io';import 'package:analyzer/dart/analysis/utilities.dart';import 'package:analyzer/dart/ast/ast.dart';import 'package:analyzer/dart/ast/visitor.dart';
class _FindDebugInvocations extends RecursiveAstVisitor<void> { final List<SourceRange> toDelete = []; @override void visitMethodInvocation(MethodInvocation node) { final target = node.target; if (target is SimpleIdentifier && target.name == 'Loggers' && node.methodName.name == 'debug') { // 仅删除形如:Loggers.debug(...); 的整行语句 final parent = node.parent; if (parent is ExpressionStatement) { toDelete.add(SourceRange(parent.offset, parent.length)); } } super.visitMethodInvocation(node); }}
class SourceRange { final int offset; final int length; SourceRange(this.offset, this.length);}
void processFile(File file) { final src = file.readAsStringSync(); final parsed = parseString(content: src, path: file.path, throwIfDiagnostics: false); final visitor = _FindDebugInvocations()..visitCompilationUnit(parsed.unit); if (visitor.toDelete.isEmpty) return;
// 逆序删除,避免位移 visitor.toDelete.sort((a, b) => b.offset.compareTo(a.offset)); final buf = StringBuffer(); int cursor = 0; for (final r in visitor.toDelete) { buf.write(src.substring(cursor, r.offset)); cursor = r.offset + r.length; } buf.write(src.substring(cursor)); file.writeAsStringSync(buf.toString()); // 可选:调用 `dart format` 统一格式(此处略)}
void main(List<String> args) { final root = args.isNotEmpty ? Directory(args.first) : Directory('lib'); for (final entry in root.listSync(recursive: true)) { if (entry is File && entry.path.endsWith('.dart')) { processFile(entry); } }}
限制与建议:
- 仅删除独立语句;若你有
someList..add(Loggers.debug(...))
等嵌入式写法,此脚本不会处理。 - 更复杂的模式(多方法名、不同接收者、注解筛选等)可以扩展匹配条件
- 这是一条离线预处理,不依赖编译器;适合强约束团队风格(可配合 pre-commit/CI)
易踩坑与要点
- 实参副作用:
Loggers.debug("${foo()}")
即使在 release 被移除了调用,foo()
可能仍被求值(取决于写法)。因此推荐懒实参或外层if (kDebugMode)
- profile 模式:既不是 debug 也不是 release。若你需要 profile 仍打印,使用
kProfileMode
或自定义bool.fromEnvironment
开关 - 注解/宏:截至目前,Dart 的注解与(已落地的)宏能力侧重生成/扩展声明,并不支持在编译管线里随意删除任意语句;不要指望”贴个注解就删代码”
- LSP/lint:可以用
custom_lint
写一条规则,禁止Loggers.debug
出现在非 assert/非kDebugMode
守护区块内,用于”防漏网”,但它不会在 release 自动删代码
Dart 在 Release 中移除日志代码
https://blog.lpkt.cn/posts/dart-rm-log-in-release/