第18章 - 调试与性能优化
嗨,朋友!我是长安。
开发过程中,你一定会遇到各种 Bug 和性能问题。这一章,我们来学习如何使用 Flutter DevTools 调试应用,以及如何优化应用性能。
🔧 调试基础
打印日志
// 基本打印
print('Hello, World!');
// 打印对象
print('User: $user');
print('List: ${list.length} items');
// 调试模式打印(release 模式不会执行)
debugPrint('Debug info');
// 条件打印
assert(() {
print('只在 debug 模式打印');
return true;
}());
// 使用 log(推荐,支持更长的输出)
import 'dart:developer';
log('Long message...', name: 'MyApp');
断言调试
// 断言:条件为 false 时抛出异常(仅 debug 模式有效)
assert(user != null, 'User cannot be null');
assert(age >= 0 && age <= 150, 'Invalid age');
// 条件断点(在 IDE 中设置)
// 右键点击断点 → 设置条件
异常处理
try {
// 可能出错的代码
final result = int.parse(input);
} on FormatException catch (e) {
// 处理特定异常
print('格式错误: $e');
} catch (e, stackTrace) {
// 处理所有异常
print('错误: $e');
print('堆栈: $stackTrace');
} finally {
// 无论如何都会执行
print('清理资源');
}
// 全局异常处理
void main() {
// Flutter 框架异常
FlutterError.onError = (details) {
print('Flutter 错误: ${details.exception}');
print('堆栈: ${details.stack}');
// 可以上报到错误监控平台
};
// 异步异常
PlatformDispatcher.instance.onError = (error, stack) {
print('未捕获的异常: $error');
print('堆栈: $stack');
return true;
};
runApp(const MyApp());
}
🛠️ Flutter DevTools
Flutter DevTools 是官方提供的调试工具套件,功能强大。
启动 DevTools
# 方式1:命令行启动
flutter pub global activate devtools
flutter pub global run devtools
# 方式2:VS Code
# 运行应用后,点击状态栏的 "Open DevTools"
# 方式3:Android Studio
# 运行应用后,点击 "Open Flutter DevTools"
Widget Inspector(界面检查器)
用于检查 Widget 树结构和布局。
功能:
- 查看 Widget 树结构
- 检查 Widget 属性
- 查看布局约束(Constraints)
- 高亮选中的 Widget
- 显示布局边界
// 在代码中开启布局边界显示
void main() {
debugPaintSizeEnabled = true; // 显示尺寸
debugPaintBaselinesEnabled = true; // 显示基线
debugPaintLayerBordersEnabled = true; // 显示图层边界
runApp(const MyApp());
}
Performance(性能面板)
用于分析应用性能和帧率。
关键指标:
- UI 线程:处理 Dart 代码,构建 Widget 树
- Raster 线程:将 Widget 渲染为像素
- 帧率:60fps 为流畅,低于 30fps 会卡顿
常见性能问题:
- UI 线程耗时 > 16ms → 优化 build 方法
- Raster 线程耗时长 → 减少图层、避免 saveLayer
Memory(内存面板)
用于分析内存使用和检测内存泄漏。
功能:
- 查看内存使用趋势
- 分析对象分配
- 检测内存泄漏
- 手动触发 GC
Network(网络面板)
用于监控网络请求。
功能:
- 查看所有 HTTP 请求
- 查看请求/响应详情
- 分析请求耗时
🐛 常见错误排查
1. RenderFlex overflowed(布局溢出)
错误信息:
A RenderFlex overflowed by 42 pixels on the right.
原因: 子 Widget 超出父容器范围
解决方案:
// 错误写法
Row(
children: [
Text('很长很长很长很长很长的文本...'),
Icon(Icons.star),
],
)
// 正确写法1:使用 Expanded
Row(
children: [
Expanded(
child: Text(
'很长很长很长很长很长的文本...',
overflow: TextOverflow.ellipsis, // 超出显示省略号
),
),
Icon(Icons.star),
],
)
// 正确写法2:使用 Flexible
Row(
children: [
Flexible(
child: Text(
'很长很长的文本...',
overflow: TextOverflow.ellipsis,
),
),
Icon(Icons.star),
],
)
// 正确写法3:使用 SingleChildScrollView
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: [
Text('很长很长的文本...'),
Icon(Icons.star),
],
),
)
2. setState() called after dispose()
错误信息:
setState() called after dispose(): _MyWidgetState#12345
原因: Widget 已销毁后还在调用 setState
解决方案:
class _MyWidgetState extends State<MyWidget> {
void initState() {
super.initState();
// 模拟异步操作
_loadData();
}
Future<void> _loadData() async {
final data = await fetchData();
// 错误:没有检查是否已销毁
// setState(() {
// _data = data;
// });
// 正确:检查 mounted
if (mounted) {
setState(() {
_data = data;
});
}
}
void dispose() {
// 取消订阅、计时器等
_subscription?.cancel();
_timer?.cancel();
super.dispose();
}
}
3. Null check operator used on a null value
错误信息:
Null check operator used on a null value
原因: 对 null 值使用了 ! 操作符
解决方案:
// 错误写法
final name = user!.name; // 如果 user 是 null 就会报错
// 正确写法1:使用空安全操作符
final name = user?.name ?? '未知';
// 正确写法2:先判断
if (user != null) {
final name = user.name;
}
// 正确写法3:使用 late(确保使用前已赋值)
late User user;
void initState() {
super.initState();
user = User(name: '长安'); // 必须在使用前赋值
}
4. Looking up a deactivated widget's ancestor
错误信息:
Looking up a deactivated widget's ancestor is unsafe.
原因: 在 Widget 销毁后还在使用 context
解决方案:
// 错误写法
Future<void> _submit() async {
await saveData();
// Widget 可能已经销毁
Navigator.pop(context); // 报错
}
// 正确写法
Future<void> _submit() async {
await saveData();
if (mounted) {
Navigator.pop(context);
}
}
// 或者先保存 Navigator
Future<void> _submit() async {
final navigator = Navigator.of(context);
await saveData();
navigator.pop();
}
5. The method 'xxx' was called on null
错误信息:
The method 'xxx' was called on null.
原因: 调用方法的对象是 null
解决方案:
// 使用空安全调用
user?.login();
// 提供默认值
(user ?? defaultUser).login();
// 先判断
if (user != null) {
user.login();
}
⚡ 性能优化技巧
1. 使用 const 构造函数
// 不好:每次 build 都会创建新对象
Container(
padding: EdgeInsets.all(16),
child: Text('Hello'),
)
// 好:const 对象只创建一次
Container(
padding: const EdgeInsets.all(16),
child: const Text('Hello'),
)
// 自定义 Widget 也要支持 const
class MyWidget extends StatelessWidget {
const MyWidget({super.key}); // 加 const
Widget build(BuildContext context) {
return const Text('Hello'); // 返回值也用 const
}
}
2. 使用 ListView.builder
// 不好:一次性创建所有子 Widget
ListView(
children: items.map((item) => ListTile(title: Text(item))).toList(),
)
// 好:按需创建,只创建可见的 Widget
ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
return ListTile(title: Text(items[index]));
},
)
// 更好:使用 itemExtent 提升滚动性能
ListView.builder(
itemCount: items.length,
itemExtent: 60, // 固定高度
itemBuilder: (context, index) {
return ListTile(title: Text(items[index]));
},
)
3. 减少不必要的 rebuild
// 不好:整个页面重建
class MyPage extends StatefulWidget {
State<MyPage> createState() => _MyPageState();
}
class _MyPageState extends State<MyPage> {
int _count = 0;
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
Text('Count: $_count'), // 只有这个需要更新
ExpensiveWidget(), // 但这个也会重建
AnotherWidget(), // 这个也会重建
],
),
);
}
}
// 好:只重建需要更新的部分
class _MyPageState extends State<MyPage> {
int _count = 0;
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
// 使用 ValueListenableBuilder 局部更新
ValueListenableBuilder<int>(
valueListenable: _countNotifier,
builder: (context, count, child) {
return Text('Count: $count');
},
),
const ExpensiveWidget(), // 不会重建
const AnotherWidget(), // 不会重建
],
),
);
}
}
// 或者把需要更新的部分提取成独立 Widget
class CounterText extends StatefulWidget {
const CounterText({super.key});
State<CounterText> createState() => _CounterTextState();
}
class _CounterTextState extends State<CounterText> {
int _count = 0;
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => setState(() => _count++),
child: Text('Count: $_count'),
);
}
}
4. 图片优化
// 指定图片尺寸,避免加载过大的图片
Image.network(
imageUrl,
width: 200,
height: 200,
cacheWidth: 400, // 缓存宽度(2x 以适应高分屏)
cacheHeight: 400,
)
// 使用缓存
CachedNetworkImage(
imageUrl: imageUrl,
placeholder: (context, url) => const CircularProgressIndicator(),
errorWidget: (context, url, error) => const Icon(Icons.error),
memCacheWidth: 400,
memCacheHeight: 400,
)
// 预加载图片
void initState() {
super.initState();
// 预加载
precacheImage(NetworkImage(imageUrl), context);
}
5. 避免在 build 中做耗时操作
// 不好:每次 build 都会执行
Widget build(BuildContext context) {
final formattedDate = DateFormat('yyyy-MM-dd').format(date); // 每次都创建
final filteredList = list.where((item) => item.isActive).toList(); // 每次都过滤
return ...;
}
// 好:缓存计算结果
class _MyWidgetState extends State<MyWidget> {
late String _formattedDate;
late List<Item> _filteredList;
void initState() {
super.initState();
_updateCache();
}
void _updateCache() {
_formattedDate = DateFormat('yyyy-MM-dd').format(date);
_filteredList = list.where((item) => item.isActive).toList();
}
Widget build(BuildContext context) {
return ...; // 使用缓存的值
}
}
6. 使用 RepaintBoundary
将频繁重绘的部分隔离,避免影响其他区域。
// 动画部分单独隔离
RepaintBoundary(
child: AnimatedWidget(...), // 这部分的重绘不会影响外部
)
// 复杂列表项
ListView.builder(
itemBuilder: (context, index) {
return RepaintBoundary(
child: ComplexListItem(item: items[index]),
);
},
)
7. 懒加载
// 图片懒加载
class LazyImage extends StatefulWidget {
final String url;
const LazyImage({super.key, required this.url});
State<LazyImage> createState() => _LazyImageState();
}
class _LazyImageState extends State<LazyImage> {
bool _isVisible = false;
Widget build(BuildContext context) {
return VisibilityDetector(
key: Key(widget.url),
onVisibilityChanged: (info) {
if (info.visibleFraction > 0 && !_isVisible) {
setState(() => _isVisible = true);
}
},
child: _isVisible
? Image.network(widget.url)
: const SizedBox(height: 200), // 占位
);
}
}
// 数据懒加载
class LazyDataPage extends StatefulWidget {
State<LazyDataPage> createState() => _LazyDataPageState();
}
class _LazyDataPageState extends State<LazyDataPage> {
final List<Item> _items = [];
bool _isLoading = false;
bool _hasMore = true;
final _scrollController = ScrollController();
void initState() {
super.initState();
_loadMore();
_scrollController.addListener(_onScroll);
}
void _onScroll() {
if (_scrollController.position.pixels >=
_scrollController.position.maxScrollExtent - 200) {
_loadMore();
}
}
Future<void> _loadMore() async {
if (_isLoading || !_hasMore) return;
setState(() => _isLoading = true);
final newItems = await fetchItems(page: _items.length ~/ 20 + 1);
setState(() {
_items.addAll(newItems);
_isLoading = false;
_hasMore = newItems.isNotEmpty;
});
}
Widget build(BuildContext context) {
return ListView.builder(
controller: _scrollController,
itemCount: _items.length + (_hasMore ? 1 : 0),
itemBuilder: (context, index) {
if (index >= _items.length) {
return const Center(child: CircularProgressIndicator());
}
return ListTile(title: Text(_items[index].title));
},
);
}
}
📊 性能检测工具
Profile 模式运行
# Profile 模式:接近 release 性能,但可以调试
flutter run --profile
性能覆盖层
MaterialApp(
showPerformanceOverlay: true, // 显示性能覆盖层
// ...
)
Timeline 分析
import 'dart:developer';
// 标记时间线
Timeline.startSync('MyOperation');
// 执行操作...
Timeline.finishSync();
// 或使用 timeSync
Timeline.timeSync('MyOperation', () {
// 执行操作...
});
📝 小结
这一章我们学习了调试和性能优化:
调试技巧
- 使用
print/debugPrint/log打印日志 - 使用 Flutter DevTools 分析应用
- 掌握常见错误的解决方案
性能优化
- 使用
const构造函数 - 使用
ListView.builder按需加载 - 减少不必要的 rebuild
- 图片优化和懒加载
- 使用
RepaintBoundary隔离重绘
性能优化原则
- 先测量,后优化 - 用 DevTools 找到瓶颈
- 不要过早优化 - 先保证功能正确
- 关注用户体验 - 60fps 是目标
- 渐进式优化 - 每次优化一个点
💪 练习题
- 使用 DevTools 分析一个列表页面的性能
- 优化一个有 1000 条数据的列表
- 找出并修复一个内存泄漏问题
🚀 下一步
学会了调试和优化后,下一章我们来学习 打包与发布,把应用发布到应用商店!
由 编程指南 提供
