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

19 KiB
Raw Blame History

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
  • 实现下载进度显示和本地缓存管理
// 改为网络播放
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 格式

# 安装 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 压缩

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

android {
    buildTypes {
        release {
            signingConfig = signingConfigs.getByName("release") // 换正式签名

            // 启用代码压缩和资源裁剪
            isMinifyEnabled = true
            isShrinkResources = true

            proguardFiles(
                getDefaultProguardFile("proguard-android-optimize.txt"),
                "proguard-rules.pro"
            )
        }
    }
}

构建命令使用 --split-per-abi

# 分架构打包,每个 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)

构建命令:

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 是多余的:

# 移除这个依赖
# google_fonts: ^6.1.0

# 已有本地字体
fonts:
  - family: Inter
    fonts:
      - asset: assets/fonts/Inter-Regular.ttf
      # ...

代码中改用 TextStyle(fontFamily: 'Inter') 替代 GoogleFonts.inter()

3.3 移除重复的 http

# 移除 - 已有 dio 做网络请求
# http: ^1.2.0

3.4 字体子集化

当前 4 个 Inter 字体文件共 1.6MB。如果只需要中英文:

# 安装 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 构造函数

// ❌ 每次 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 隔离重绘区域

// 对频繁更新的局部 Widget 包裹 RepaintBoundary
RepaintBoundary(
  child: AnimatedWidget(...),  // 只重绘这部分,不影响外部
)

适用场景:

  • 动画区域
  • 频繁刷新的倒计时/进度条
  • BLE 数据实时显示

拆分 Widget避免大范围重建

// ❌ 整个页面一个大 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 精准订阅

// ❌ 监听整个 state任何字段变化都重建
final state = ref.watch(deviceProvider);

// ✅ 只监听需要的字段
final deviceName = ref.watch(deviceProvider.select((s) => s.name));
final isConnected = ref.watch(deviceProvider.select((s) => s.isConnected));

区分 watchlisten

// 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),这是发热的主要来源之一。

// ❌ 持续高频扫描
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 图片与内存管理

网络图片缓存

# 添加依赖
dependencies:
  cached_network_image: ^3.3.0
// 使用缓存图片组件
CachedNetworkImage(
  imageUrl: imageUrl,
  memCacheWidth: 300,  // 限制内存中图片尺寸
  memCacheHeight: 300,
  placeholder: (_, __) => const CircularProgressIndicator(),
  errorWidget: (_, __, ___) => const Icon(Icons.error),
)

大图降采样

// ❌ 直接加载 4.7MB 的 PNG 到内存
Image.asset('assets/www/首页背景2.png')

// ✅ 指定缓存尺寸,降低内存占用
Image.asset(
  'assets/www/首页背景2.webp',
  cacheWidth: 750,   // 按屏幕宽度限制
  cacheHeight: 1334,
)

页面销毁时释放资源

class _MyPageState extends State<MyPage> {
  VideoPlayerController? _videoController;

  @override
  void dispose() {
    _videoController?.dispose();  // 必须释放!
    super.dispose();
  }
}

3.5 Isolate 处理耗时任务

// 图片处理、数据解析等放到 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

// ❌ 一次性创建所有子 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]),
)

图片列表优化

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 是否启用:

# iOS 默认启用Android 3.22+ 默认启用
flutter run --profile  # 然后查看 DevTools

如果遇到兼容性问题可临时回退:

flutter run --no-enable-impeller

4.3 Shader 预热(如仍用 Skia

// 在 main.dart 中
void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  // 预编译 shader 缓存
  await ShaderWarmUp().execute();

  runApp(const MyApp());
}

SkSL 收集命令:

# 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 页面切换优化

// 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 优化

// 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 包大小分析

# 分析 APK 大小组成
flutter build apk --analyze-size

# 分析 iOS 包大小
flutter build ipa --analyze-size

5.2 性能分析

# Profile 模式运行(有性能数据但接近 release 性能)
flutter run --profile

# 打开 DevTools
flutter pub global activate devtools
flutter pub global run devtools

5.3 检测无用代码和依赖

# 检测未使用的依赖
dart pub deps --no-dev

# 分析代码问题
dart analyze

5.4 GlanceAPM 监控库)

# 生产环境卡顿检测
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_fontshttp 包大小 -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 状态:

flutter run --profile  # 然后在 DevTools 中查看渲染后端

Android 持久化配置(AndroidManifest.xml

<meta-data
  android:name="io.flutter.embedding.android.EnableImpeller"
  android:value="true" />

九、低端设备适配策略

针对不同档次手机动态降级:

// 设备分级
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 开源 APM 采集卡顿时的堆栈,定位具体函数
flutter_smooth 开源库 即使 Widget 树极重也能达到 ~60fps
Firebase Performance 云端 APM 启动时间、网络延迟、自定义追踪
Sentry 错误+性能 崩溃报告+性能事务

自动化性能测试CI 集成)

// 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');
  });
}

十一、参考资源

官方文档

优秀开源项目参考

工具