背景
前几天,我需要开发手机App,AI推荐了Flutter,并快速生成了代码,连接FastAPI接口,获取数据,绘制结果,花了200左右的积分,迅速完成。AI还贴心地生成了手机拍照功能,拍照照片上传到FastAPI服务器上。现在普遍对AI生成代码不太放心,这里强调两点FastAPI的安装部署和运行,Flutter的内存泄漏问题,不是说Flutter会轻易内存泄漏,Flutter是成熟的平台,我只是说从某个点来讲内存泄漏,让普通程序员和新手怎么查泄漏,有谨慎心态能落地实践。
Dart是Google开发的⼀种面向对象的计算机编程语言,和Java类似。Flutter是Google开源的UI⼯具包,帮助开发者通过⼀套代码库高效构建多平台精美应用,支持移动、Web、桌面和嵌入式平台。 Dart是flutter的程序开发语言。Flutter能被AI推荐,有大量的QQ群,群里有很多爱好者,都证明Flutter的成功。Flutter多端就是Flutter多平台的意思,移动、Web、桌面和嵌入式平台都可以用Flutter开发,所以叫Flutter多端。这里面最重要的是苹果的iOS开发,xcode需要运行在iOS下,一般是用虚拟机来做,环境不太有好。Flutter的一次开发,多端(多平台)兼容,会节省很多时间。如果不用Flutter,需要掌握多门编程语言和多个开发环境,工作量大。
Flutter项目的具体架构
# 牛身检测 Flutter App
一个简单的Flutter应用,用于上传牛只图片并显示检测到的牛身轮廓。
## 项目结构
```
cowapp/
├── lib/
│ ├── main.dart # 应用入口
│ ├── home_page.dart # 主页面(按钮+图片控件)
│ ├── services/
│ │ └── api_service.dart # API服务(与后端通信)
│ └── widgets/
│ └── contour_painter.dart # 轮廓绘制组件
├── pubspec.yaml # 项目配置
└── README.md # 本文件
```
## 功能
1. 点击按钮从图库选择牛只图片
2. 自动上传到后端服务器进行检测
3. 显示检测到的牛身轮廓(绿色线条)
4. 在原图上叠加显示轮廓结果
## 技术栈
- **Flutter**: 前端框架
- **image_picker**: 图片选择
- **http**: 网络请求
- **CustomPainter**: 轮廓绘制
## 运行步骤
### 1. 启动后端服务
```bash
cd d:\cow
python server.py
```
服务将在 `http://localhost:8000` 启动。
### 2. 安装Flutter依赖
```bash
cd d:\cowapp
flutter pub get
```
### 3. 运行Flutter应用
```bash
flutter run
```
## API接口
### POST /view_body_new_service
**请求**:
- Content-Type: multipart/form-data
- Body: 图片文件(字段名: file)
**响应**:
```json
{
"success": true,
"message": "检测成功",
"contour_points": [[x1,y1], [x2,y2], ...],
"image_width": 4096,
"image_height": 3072
}
```
## 注意事项
1. **后端服务地址**: 默认连接到 `http://localhost:8000`,可在 `api_service.dart` 中修改
2. **图片大小**: 支持最大4096x3072分辨率,会自动缩放显示
3. **轮廓绘制**: 使用绿色线条绘制,红色圆点标记起点
4. **缩放处理**: CustomPainter 会根据原图尺寸和显示尺寸自动计算缩放比例,确保轮廓点正确对齐
## 待优化功能
- [ ] 添加进度显示
- [ ] 支持相机拍照
- [ ] 显示更多检测信息(如牛身特征)
- [ ] 支持多张图片批量处理
- [ ] 离线模式缓存
Flutter多端开发能力在这个牛身App里有体现,在Windows下可以运行,编译成apk就可以在安卓手机上运行,在Windows下功能测试完成,则安卓手机上的测试就相对简单多了。
contour_painter.dart轮廓绘制组件代码解析和内存泄漏分析
import 'package:flutter/material.dart';//导入库 import 'package:flutter/foundation.dart'; class ContourPainter extends CustomPainter { final List<List<int>> contourPoints;//坐标数组 final int imageWidth; final int imageHeight; final Color contourColor; final double strokeWidth; ContourPainter({ required this.contourPoints, required this.imageWidth, required this.imageHeight, this.contourColor = Colors.green, this.strokeWidth = 3.0, }); @override void paint(Canvas canvas, Size size) {//重写的绘制函数 if (kDebugMode) { print('[ContourPainter] paint 被调用'); print('[ContourPainter] 画布尺寸: ${size.width}x${size.height}'); print('[ContourPainter] 原图尺寸: ${imageWidth}x$imageHeight'); print('[ContourPainter] 轮廓点数: ${contourPoints.length}'); if (contourPoints.isNotEmpty) { print('[ContourPainter] 第一个点: ${contourPoints.first}'); print('[ContourPainter] 最后一个点: ${contourPoints.last}'); } } if (contourPoints.isEmpty || imageWidth == 0 || imageHeight == 0) { if (kDebugMode) { print('[ContourPainter] 提前返回(数据为空)'); } return; } // 现在外层 FittedBox 统一缩放,画布 size 应等于原图像素尺寸 // 这里仍然计算一个 scale 以防御不一致情况 final scaleX = size.width / imageWidth; final scaleY = size.height / imageHeight; final scale = (scaleX > 0 && scaleY > 0) ? scaleX // 正常情况下 scaleX == scaleY == 1.0(因为 FittedBox) : 1.0; if (kDebugMode) { print('[ContourPainter] scaleX=$scaleX, scaleY=$scaleY, scale=$scale'); } // 创建画笔 final paint = Paint() ..color = contourColor ..strokeWidth = strokeWidth // 直接使用像素线宽,FittedBox会整体缩放 ..style = PaintingStyle.stroke ..strokeCap = StrokeCap.round ..strokeJoin = StrokeJoin.round; // 绘制轮廓路径 if (contourPoints.length > 1) { final path = Path(); // 移动到第一个点 final startX = contourPoints[0][0] * scale; final startY = contourPoints[0][1] * scale; path.moveTo(startX, startY); if (kDebugMode) { print('[ContourPainter] 路径起点: ($startX, $startY)'); } // 连接所有轮廓点 for (int i = 1; i < contourPoints.length; i++) { final x = contourPoints[i][0] * scale; final y = contourPoints[i][1] * scale; path.lineTo(x, y); } // 闭合路径 path.close(); // 绘制路径 canvas.drawPath(path, paint); if (kDebugMode) { print('[ContourPainter] 路径已绘制'); } } // 绘制起点标记(红色圆点) if (contourPoints.isNotEmpty) { final startX = contourPoints[0][0] * scale; final startY = contourPoints[0][1] * scale; final startPaint = Paint() ..color = Colors.red ..style = PaintingStyle.fill; canvas.drawCircle( Offset(startX, startY), 6.0, // 固定像素大小,外层会缩放 startPaint, ); } // 绘制所有轮廓点(小圆点),间隔绘制避免太密集 final pointPaint = Paint() ..color = contourColor.withValues(alpha: 0.6) ..style = PaintingStyle.fill; for (int i = 0; i < contourPoints.length; i++) { if (i % 5 == 0) { final x = contourPoints[i][0] * scale; final y = contourPoints[i][1] * scale; canvas.drawCircle( Offset(x, y), 2.0, pointPaint, ); } } } @override bool shouldRepaint(covariant ContourPainter oldDelegate) {//重写的触发绘制函数 // 当轮廓点、颜色、线宽、图片尺寸变化时重绘 return oldDelegate.contourPoints != contourPoints || oldDelegate.contourColor != contourColor || oldDelegate.strokeWidth != strokeWidth || oldDelegate.imageWidth != imageWidth || oldDelegate.imageHeight != imageHeight; } }
dart语言类似Java,当然也像C#,这里面没有指针,使用还是很方便。这里面Path()是常用的资源对象,资源对象用不好会导致内存泄漏。Flutter在频繁分配Path()时会出问题,但是这些代码明显是不频繁的,是可以接受的。为什么不频繁?paint绘制的前提是数据对象改变让shouldRepaint返回真,才绘制,不频繁绘制就不会分配Path()对象,所以代码是正确的。其它的对象不涉及内存泄漏。

