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/     
  