rtc_prd/airhub_app/FLUTTER_OPTIMIZATION_GUIDE.md
2026-03-26 10:39:54 +08:00

704 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Flutter App 全面优化方案
> 针对 airhub_app 项目的从头到尾优化整改方案
> 目标:减小包大小、降低发热、消除卡顿
---
## 一、项目现状分析
| 指标 | 当前值 | 问题 |
|------|--------|------|
| Flutter 版本 | 3.41.2 (Dart 3.11.0) | 较新,支持 Impeller |
| 代码规模 | 132 个 Dart 文件 / 28k 行 | 中等规模 |
| assets 总大小 | **~135MB** | **严重超标** |
| storybook_videos/ | **92MB** (11个mp4最大48MB) | **主要元凶** |
| music/ | **15MB** (10个mp3) | 应该CDN下载 |
| story_covers/ | **7.4MB** (PNG图片) | 需压缩或远程加载 |
| 背景图 PNG | 单张 4.5-4.7MB | **未压缩的巨型PNG** |
| 字体文件 | 1.6MB (4个Inter字重) | 可裁剪子集 |
| google_fonts 依赖 | 存在 | 会下载额外字体 |
| Android release 配置 | 未启用 minifyEnabled | 缺少代码压缩 |
| ProGuard | 已引用但未启用 shrinkResources | 资源未裁剪 |
| iOS 构建 | 默认配置 | 未做符号裁剪优化 |
---
## 二、包大小优化(预估可减少 60-70%
### 阶段 1资源外迁预估减少 110MB+ → 约 80% 的包大小)
这是**最关键也是收益最大**的一步。
#### 1.1 视频资源迁移到 CDN / 服务器
```
当前: assets/www/storybook_videos/ → 92MB 全部打进包
目标: 视频从服务端按需下载,本地缓存
```
**实施方案:**
- 将所有 mp4 文件上传到后端服务器或 OSS/CDN
- App 端改为流式播放或首次使用时下载到本地缓存
- 使用 `video_player` 的网络播放功能直接播放远程URL
- 实现下载进度显示和本地缓存管理
```dart
// 改为网络播放
VideoPlayerController.networkUrl(Uri.parse('https://cdn.example.com/videos/magic_broom.mp4'))
// 或按需下载到缓存
final cacheDir = await getTemporaryDirectory();
final file = File('${cacheDir.path}/magic_broom.mp4');
if (!file.existsSync()) {
await dio.download(url, file.path, onReceiveProgress: (received, total) {
// 显示下载进度
});
}
```
#### 1.2 音频资源迁移
```
当前: assets/www/music/ → 15MB
目标: 音频按需下载或流式播放
```
- `just_audio` 本身支持网络URL播放
- 改为 `AudioSource.uri(Uri.parse('https://cdn.example.com/music/xxx.mp3'))`
#### 1.3 故事封面图迁移
```
当前: assets/www/story_covers/ → 7.4MB
目标: 网络图片 + 本地缓存
```
- 添加 `cached_network_image` 依赖
- 封面图改为网络加载,自动缓存
#### 1.4 背景图压缩
```
当前: 首页背景2.png (4.7MB) + 首页底图.png (4.5MB)
目标: 压缩到 200-500KB
```
**方案A转 WebP 格式**
```bash
# 安装 cwebp
brew install webp
# 转换 (质量80通常可减少 85%+)
cwebp -q 80 "assets/www/首页背景2.png" -o "assets/www/首页背景2.webp"
cwebp -q 80 "assets/www/首页底图.png" -o "assets/www/首页底图.webp"
```
**方案B用 pngquant 压缩**
```bash
pngquant --quality=65-80 "assets/www/首页背景2.png" --output "assets/www/首页背景2.png"
```
预期效果4.7MB → 300-500KB减少 **90%**
### 阶段 2构建配置优化预估再减 30-40%
#### 2.1 Android 构建优化
**修改 `android/app/build.gradle.kts`**
```kotlin
android {
buildTypes {
release {
signingConfig = signingConfigs.getByName("release") // 换正式签名
// 启用代码压缩和资源裁剪
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
}
```
**构建命令使用 `--split-per-abi`**
```bash
# 分架构打包,每个 APK 减少 30-40%
flutter build apk --release --split-per-abi --obfuscate --split-debug-info=./debug-info
# 或 App BundleGoogle Play 自动按设备裁剪)
flutter build appbundle --release --obfuscate --split-debug-info=./debug-info
```
| 参数 | 作用 | 预估收益 |
|------|------|----------|
| `--split-per-abi` | 按 CPU 架构分包 | 减少 30-40% |
| `--obfuscate` | 代码混淆 | 减少 5-10% |
| `--split-debug-info` | 分离调试符号 | 减少 5-10% |
| `minifyEnabled=true` | R8 代码压缩 | 减少 15-25% |
| `shrinkResources=true` | 移除未使用资源 | 减少 5-10% |
#### 2.2 iOS 构建优化
**Xcode Build Settings 调整:**
- Strip Debug Symbols During Copy → YES
- Strip Linked Product → YES
- Dead Code Stripping → YES
- Optimization Level → -Oz (Smallest)
**构建命令:**
```bash
flutter build ipa --release --obfuscate --split-debug-info=./debug-info
```
### 阶段 3依赖瘦身
#### 3.1 审计当前依赖
| 依赖 | 大小影响 | 建议 |
|------|----------|------|
| `google_fonts` | 较大,会下载字体 | 已内嵌 Inter 字体,考虑移除 |
| `webview_flutter` | ~4MB 原生库 | 仅在需要时加载(deferred) |
| `video_player` | ~3-4MB | 考虑 deferred import |
| `image` (dart包) | 纯 Dart 图像处理 | 检查是否真的需要 |
| `http` | 与 `dio` 重复 | 统一用 dio移除 http |
| `flutter_svg` | 中等 | 保留SVG 比 PNG 小得多) |
#### 3.2 移除 `google_fonts`
你已经在 `pubspec.yaml` 中内嵌了 Inter 字体,`google_fonts` 是多余的:
```yaml
# 移除这个依赖
# google_fonts: ^6.1.0
# 已有本地字体
fonts:
- family: Inter
fonts:
- asset: assets/fonts/Inter-Regular.ttf
# ...
```
代码中改用 `TextStyle(fontFamily: 'Inter')` 替代 `GoogleFonts.inter()`
#### 3.3 移除重复的 `http` 包
```yaml
# 移除 - 已有 dio 做网络请求
# http: ^1.2.0
```
#### 3.4 字体子集化
当前 4 个 Inter 字体文件共 1.6MB。如果只需要中英文:
```bash
# 安装 fonttools
pip install fonttools brotli
# 裁剪只保留需要的字符集(中文 + 英文 + 数字 + 标点)
pyftsubset Inter-Regular.ttf \
--text-file=used_chars.txt \
--output-file=Inter-Regular-subset.ttf
# 通常可从 400KB → 50-80KB
```
---
## 三、性能优化:降低发热与 CPU 占用
### 3.1 Widget 重建优化(最重要)
#### 使用 `const` 构造函数
```dart
// ❌ 每次 build 都创建新实例
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(16), // 每次创建新对象
child: Text('Hello'),
);
}
// ✅ 编译期常量,不会触发重建
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(16),
child: const Text('Hello'),
);
}
```
**全项目检查:** 运行 `dart analyze` 查看 `prefer_const_constructors` 警告。
#### 使用 `RepaintBoundary` 隔离重绘区域
```dart
// 对频繁更新的局部 Widget 包裹 RepaintBoundary
RepaintBoundary(
child: AnimatedWidget(...), // 只重绘这部分,不影响外部
)
```
适用场景:
- 动画区域
- 频繁刷新的倒计时/进度条
- BLE 数据实时显示
#### 拆分 Widget避免大范围重建
```dart
// ❌ 整个页面一个大 build 方法
class MyPage extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final state = ref.watch(someProvider); // 任何变化都重建整个页面
return Scaffold(
body: Column(children: [Header(), Content(state), Footer()]),
);
}
}
// ✅ 拆成独立 Widget只监听需要的部分
class _Content extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final data = ref.watch(someProvider.select((s) => s.data)); // 精准监听
return ...;
}
}
```
### 3.2 Riverpod 状态管理优化
#### 使用 `select` 精准订阅
```dart
// ❌ 监听整个 state任何字段变化都重建
final state = ref.watch(deviceProvider);
// ✅ 只监听需要的字段
final deviceName = ref.watch(deviceProvider.select((s) => s.name));
final isConnected = ref.watch(deviceProvider.select((s) => s.isConnected));
```
#### 区分 `watch` 和 `listen`
```dart
// watch: 用于 UI 显示的数据(需要重建)
final name = ref.watch(provider.select((s) => s.name));
// listen: 用于副作用(弹窗、导航、日志),不触发重建
ref.listen(provider, (prev, next) {
if (next.hasError) showErrorDialog(context, next.error);
});
```
### 3.3 BLE 通信优化(降低发热的关键)
你的 app 大量使用蓝牙(`flutter_blue_plus`),这是发热的主要来源之一。
```dart
// ❌ 持续高频扫描
FlutterBluePlus.startScan(timeout: Duration(seconds: 30));
// ✅ 扫到目标设备后立即停止
FlutterBluePlus.startScan(timeout: Duration(seconds: 10));
// 找到目标后
FlutterBluePlus.stopScan();
// ❌ 不必要的频繁读取特征值
Timer.periodic(Duration(milliseconds: 100), (_) => characteristic.read());
// ✅ 使用 notify 替代轮询
await characteristic.setNotifyValue(true);
characteristic.onValueReceived.listen((value) {
// 只在有新数据时处理
});
```
**BLE 省电策略:**
- 连接成功后立即停止扫描
- 使用 notification 而非 polling
- 非活跃页面暂停 BLE 数据监听
- 合理设置连接间隔参数
### 3.4 图片与内存管理
#### 网络图片缓存
```yaml
# 添加依赖
dependencies:
cached_network_image: ^3.3.0
```
```dart
// 使用缓存图片组件
CachedNetworkImage(
imageUrl: imageUrl,
memCacheWidth: 300, // 限制内存中图片尺寸
memCacheHeight: 300,
placeholder: (_, __) => const CircularProgressIndicator(),
errorWidget: (_, __, ___) => const Icon(Icons.error),
)
```
#### 大图降采样
```dart
// ❌ 直接加载 4.7MB 的 PNG 到内存
Image.asset('assets/www/首页背景2.png')
// ✅ 指定缓存尺寸,降低内存占用
Image.asset(
'assets/www/首页背景2.webp',
cacheWidth: 750, // 按屏幕宽度限制
cacheHeight: 1334,
)
```
#### 页面销毁时释放资源
```dart
class _MyPageState extends State<MyPage> {
VideoPlayerController? _videoController;
@override
void dispose() {
_videoController?.dispose(); // 必须释放!
super.dispose();
}
}
```
### 3.5 Isolate 处理耗时任务
```dart
// 图片处理、数据解析等放到 Isolate
import 'package:flutter/foundation.dart';
// 简单任务用 compute
final result = await compute(processImage, imageData);
// 复杂任务用 Isolate
Future<Uint8List> processImageInIsolate(Uint8List rawData) async {
return await Isolate.run(() {
// 在独立线程处理图片
return heavyImageProcessing(rawData);
});
}
```
适用场景:
- BLE 接收的大量数据解析
- 图片编解码badge 图片处理)
- JSON 大数据解析
---
## 四、UI 流畅度优化(消除卡顿)
### 4.1 列表优化
#### 使用 `ListView.builder` 而非 `ListView`
```dart
// ❌ 一次性创建所有子 Widget
ListView(children: items.map((e) => ItemWidget(e)).toList())
// ✅ 按需创建可见项
ListView.builder(
itemCount: items.length,
itemExtent: 80, // 固定高度可提升滚动性能
addAutomaticKeepAlives: false, // 不需要保活的列表关闭此项
addRepaintBoundaries: true,
itemBuilder: (context, index) => ItemWidget(items[index]),
)
```
#### 图片列表优化
```dart
ListView.builder(
cacheExtent: 500, // 预缓存区域
itemBuilder: (context, index) {
return RepaintBoundary(
child: CachedNetworkImage(
imageUrl: items[index].imageUrl,
memCacheWidth: 200, // 限制内存图片大小
),
);
},
)
```
### 4.2 Impeller 渲染引擎Flutter 3.41.2 默认启用)
你的 Flutter 版本已默认使用 Impeller 渲染引擎,优势:
- **消除 Shader 编译卡顿**Skia 的最大痛点)
- 更稳定的帧率
- 更低的 GPU 内存占用
验证 Impeller 是否启用:
```bash
# iOS 默认启用Android 3.22+ 默认启用
flutter run --profile # 然后查看 DevTools
```
如果遇到兼容性问题可临时回退:
```bash
flutter run --no-enable-impeller
```
### 4.3 Shader 预热(如仍用 Skia
```dart
// 在 main.dart 中
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// 预编译 shader 缓存
await ShaderWarmUp().execute();
runApp(const MyApp());
}
```
SkSL 收集命令:
```bash
# 1. 收集 shader
flutter run --profile --cache-sksl --purge-persistent-cache
# 2. 操作 app 中所有页面和动画
# 3. 按 M 导出 flutter_01.sksl.json
# 4. 使用缓存构建
flutter build apk --bundle-sksl-path flutter_01.sksl.json
```
### 4.4 页面切换优化
```dart
// go_router 中预加载关键页面
GoRoute(
path: '/device-control',
pageBuilder: (context, state) => CustomTransitionPage(
transitionDuration: const Duration(milliseconds: 200), // 缩短动画时间
child: const DeviceControlPage(),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return FadeTransition(opacity: animation, child: child); // 用 Fade 替代 Slide
},
),
)
```
### 4.5 WebView 优化
```dart
// WebView 是重量级组件
// 1. 延迟初始化
late final WebViewController _controller;
@override
void initState() {
super.initState();
// 延迟到页面完全展示后再初始化 WebView
WidgetsBinding.instance.addPostFrameCallback((_) {
_initWebView();
});
}
// 2. 页面销毁时清理
@override
void dispose() {
_controller.clearCache();
_controller.clearLocalStorage();
super.dispose();
}
```
---
## 五、工具与监控
### 5.1 包大小分析
```bash
# 分析 APK 大小组成
flutter build apk --analyze-size
# 分析 iOS 包大小
flutter build ipa --analyze-size
```
### 5.2 性能分析
```bash
# Profile 模式运行(有性能数据但接近 release 性能)
flutter run --profile
# 打开 DevTools
flutter pub global activate devtools
flutter pub global run devtools
```
### 5.3 检测无用代码和依赖
```bash
# 检测未使用的依赖
dart pub deps --no-dev
# 分析代码问题
dart analyze
```
### 5.4 GlanceAPM 监控库)
```yaml
# 生产环境卡顿检测
dependencies:
glance_flutter: ^0.x.x # 实验性
```
---
## 六、实施优先级与预期效果
### P0 紧急1-2天收益巨大
| 任务 | 预期收益 |
|------|----------|
| 视频/音频迁移到 CDN | 包大小 -107MB (80%) |
| 背景图转 WebP 压缩 | 包大小 -8MB |
| 启用 `minifyEnabled` + `shrinkResources` | 包大小 -15-25% |
| `--split-per-abi` 分架构打包 | APK -30-40% |
### P1 重要3-5天
| 任务 | 预期收益 |
|------|----------|
| 移除 `google_fonts``http` 包 | 包大小 -2-4MB |
| 字体子集化 | 包大小 -1.2MB |
| `--obfuscate --split-debug-info` | 包大小 -5-10% |
| BLE 扫描/通信优化 | 降低发热 30-50% |
| 图片内存管理 (cacheWidth/Height) | 降低内存 40% |
### P2 优化1-2周
| 任务 | 预期收益 |
|------|----------|
| 全项目 const 构造函数审查 | 减少 Widget 重建 |
| Riverpod select 精准订阅 | 减少不必要重建 |
| 列表 ListView.builder 优化 | 滚动更流畅 |
| 耗时任务移入 Isolate | 消除主线程卡顿 |
| RepaintBoundary 隔离动画区域 | 减少重绘范围 |
| Deferred import (webview/video) | 减小初始加载 |
### P3 持续(长期)
| 任务 | 预期收益 |
|------|----------|
| 接入性能监控 (Glance/Sentry) | 线上问题感知 |
| 定期 `flutter build --analyze-size` | 包大小回归检测 |
| CI 集成包大小检查 | 防止包大小退化 |
---
## 七、预期总体效果
| 指标 | 优化前 | 优化后(预估) | 改善 |
|------|--------|----------------|------|
| APK 大小 | ~150-180MB | ~25-35MB | **80%+** |
| IPA 大小 | ~200MB+ | ~40-60MB | **70%+** |
| 冷启动时间 | 3-5s | 1-2s | **60%** |
| 内存占用 | 较高 | 降低 40%+ | **40%+** |
| 手机发热 | 明显 | 温热 | **显著改善** |
| 滚动帧率 | 有掉帧 | 稳定 60fps | **流畅** |
---
## 八、Impeller 渲染引擎性能数据
Flutter 3.41.2 已默认启用 Impeller替代 Skia关键性能数据
| 维度 | Skia | Impeller | 改善 |
|------|------|---------|------|
| Shader 编译 | 运行时 JIT首次卡顿 20-200ms | 构建时 AOT 预编译 | **消除 Shader 卡顿** |
| GPU 光栅化耗时 | 4.05ms | 2.81ms | **-30%** |
| 120Hz 帧率达标率 | 67.1% | 91.6% | **+36%** |
| 复杂场景掉帧率 | ~12% | ~1.5% | **-87%** |
确认 Impeller 状态:
```bash
flutter run --profile # 然后在 DevTools 中查看渲染后端
```
Android 持久化配置(`AndroidManifest.xml`
```xml
<meta-data
android:name="io.flutter.embedding.android.EnableImpeller"
android:value="true" />
```
---
## 九、低端设备适配策略
针对不同档次手机动态降级:
```dart
// 设备分级
enum DeviceTier { low, mid, high }
DeviceTier getDeviceTier() {
final memory = SysInfo.getTotalPhysicalMemory(); // 需原生获取
if (memory < 3 * 1024 * 1024 * 1024) return DeviceTier.low;
if (memory < 6 * 1024 * 1024 * 1024) return DeviceTier.mid;
return DeviceTier.high;
}
```
| 策略 | 低端 | 中端 | 高端 |
|------|------|------|------|
| 动画 | 关闭粒子/复杂动画 | 简化动画 | 全特效 |
| 图片 | WebP 降分辨率 30% | 标准分辨率 | 高清 |
| BackdropFilter | 纯色替代 | 简化模糊 | 全高斯模糊 |
| 并发网络请求 | 限 2 个 | 限 4 个 | 不限制 |
---
## 十、生产环境性能监控
| 工具 | 类型 | 用途 |
|------|------|------|
| [Glance](https://github.com/littleGnAl/glance) | 开源 APM | 采集卡顿时的堆栈,定位具体函数 |
| [flutter_smooth](https://github.com/fzyzcjy/flutter_smooth) | 开源库 | 即使 Widget 树极重也能达到 ~60fps |
| Firebase Performance | 云端 APM | 启动时间、网络延迟、自定义追踪 |
| Sentry | 错误+性能 | 崩溃报告+性能事务 |
### 自动化性能测试CI 集成)
```dart
// integration_test/performance_test.dart
import 'package:integration_test/integration_test.dart';
void main() {
final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized();
testWidgets('scrolling performance', (tester) async {
await tester.pumpWidget(const MyApp());
await binding.traceAction(() async {
await tester.fling(find.byType(ListView), const Offset(0, -500), 10000);
await tester.pumpAndSettle();
}, reportKey: 'scrolling_timeline');
});
}
```
---
## 十一、参考资源
### 官方文档
- [Flutter Performance Best Practices](https://docs.flutter.dev/perf/best-practices)
- [App Size Optimization](https://docs.flutter.dev/perf/app-size)
- [Impeller Rendering Engine](https://docs.flutter.dev/perf/impeller)
- [Deferred Components](https://docs.flutter.dev/perf/deferred-components)
- [Shader Compilation Jank](https://docs.flutter.dev/perf/shader)
### 优秀开源项目参考
- [flutter_deer](https://github.com/simplezhli/flutter_deer) — 完善的 Flutter 实践项目,代码规范
- [Best-Flutter-UI-Templates](https://github.com/mitesh77/Best-Flutter-UI-Templates) — UI 模板合集
- [Solido/awesome-flutter](https://github.com/Solido/awesome-flutter) — Flutter 最全资源汇总
### 工具
- [Glance](https://pub.dev/packages/glance_flutter) — APM 卡顿检测
- [TinyPNG](https://tinypng.com/) — PNG/WebP 压缩
- [pngquant](https://pngquant.org/) — 批量 PNG 压缩
- [pyftsubset](https://fonttools.readthedocs.io/) — 字体子集化