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 下零开销(不创建字符串、不执行函数)。

log.dart
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 的调用;嵌在表达式里的不处理,避免破坏语义)。

strip_debug.dart
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/
作者
lollipopkit
发布于
2025-08-21
许可协议
CC BY-NC-SA 4.0