# 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 Bundle(Google 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 { 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 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 Glance(APM 监控库) ```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 ``` --- ## 九、低端设备适配策略 针对不同档次手机动态降级: ```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/) — 字体子集化