Skip to Content

33e - 移动端 Steering 规则与反模式

本文是《AI Agent 实战手册》第 33 章第 5 节。 上一节:AI辅助移动端UI | 下一节:AI辅助游戏开发概览

概述

移动端开发是 AI 代码生成最容易”翻车”的领域之一——AI 生成的代码在模拟器上看起来完美,但部署到真机后可能出现内存泄漏导致闪退、后台任务疯狂耗电、权限请求缺失导致崩溃、离线场景完全不可用、iOS 和 Android 行为不一致、以及包体积膨胀到用户拒绝下载等严重问题。2025-2026 年的实践表明,移动端 AI 生成代码的质量问题率高达 45-55%,远高于 Web 前端的 30-40%,根本原因在于移动端有更多平台特定约束、资源限制和系统级交互。本节提供完整的移动端 Steering 规则模板(覆盖 CLAUDE.md、Kiro Steering、Cursor Rules)、八大反模式的深度剖析(含问题代码 vs 正确代码对比),以及针对 iOS(SwiftUI)、Android(Jetpack Compose)、React Native 和 Flutter 四大平台的专用规则。


1. 移动端 Steering 规则概述

1.1 为什么移动端需要专用 Steering 规则

移动端开发与 Web 开发有本质区别,AI 在以下方面特别容易犯错:

维度Web 开发移动端开发AI 常见失误
内存管理浏览器自动 GC,页面刷新即释放受限内存,后台可被系统杀死不释放监听器、不取消订阅、循环引用
电池不关心核心用户体验指标频繁网络请求、持续定位、不必要的后台任务
权限浏览器统一处理运行时权限,平台差异大缺少权限检查、不处理拒绝、不解释用途
网络通常稳定弱网/离线是常态无离线策略、无重试、无缓存
平台差异浏览器差异较小iOS/Android 行为差异巨大只测一个平台、忽略平台规范
包体积可懒加载安装前必须下载完整包引入过多依赖、未压缩资源、未分包
生命周期页面加载/卸载复杂的前后台切换不处理生命周期、后台状态丢失
安全HTTPS + CSP本地存储、证书固定、代码混淆明文存储敏感数据、缺少证书固定

1.2 Steering 规则工具对比

工具规则文件位置价格移动端适用性适用场景
Claude CodeCLAUDE.md(项目根目录)Max $100/月起 / API 按量★★★★★大型移动项目,多文件重构
Kiro.kiro/steering/*.md免费(预览期)★★★★★分层规则,按平台/模块分区
Cursor.cursor/rules/*.mdc免费 / Pro $20/月★★★★☆日常移动端编码
GitHub Copilot.github/copilot-instructions.md$10/月(Individual)★★★☆☆代码补全级辅助
Windsurf.windsurfrules免费 / Pro $15/月★★★☆☆轻量级移动端开发
Xcode 26 AIXcode 内置上下文免费(Xcode 内置)★★★★★iOS 原生开发专用
Android Studio GeminiStudio 内置上下文免费(内置 Gemini)★★★★★Android 原生开发专用

1.3 移动端 Steering 规则架构

项目根目录/ ├── CLAUDE.md # 全局规则(通用约束) ├── .kiro/steering/ │ ├── mobile-general.md # 移动端通用规则(always) │ ├── ios-swiftui.md # iOS 专用规则(auto: *.swift) │ ├── android-compose.md # Android 专用规则(auto: *.kt) │ ├── react-native.md # RN 专用规则(auto: *.tsx in src/) │ └── flutter.md # Flutter 专用规则(auto: *.dart) ├── .cursor/rules/ │ ├── mobile-rules.mdc # Cursor 移动端规则 │ └── platform-specific.mdc # 平台特定规则 └── .github/ └── copilot-instructions.md # Copilot 移动端指令

2. 完整 Steering 规则模板

2.1 CLAUDE.md 移动端通用规则模板

以下是适用于跨平台移动端项目(React Native + Expo)的完整 CLAUDE.md 模板:

# CLAUDE.md — 移动端项目规则 ## 项目概述 - 框架:React Native 0.79+ / Expo SDK 53 - 语言:TypeScript 5.x(strict mode) - 导航:React Navigation v7 / Expo Router v4 - 状态管理:Zustand(本地)+ TanStack Query(服务端) - 样式:StyleSheet.create(禁止内联样式对象) - 动画:react-native-reanimated v3(原生线程动画) - 手势:react-native-gesture-handler v2 - 存储:expo-secure-store(敏感数据)+ MMKV(一般数据) - 测试:Jest + React Native Testing Library + Detox(E2E) - 最低支持:iOS 16+ / Android API 26+(Android 8.0) ## 内存管理规则(最高优先级) - 所有 useEffect 必须返回清理函数,取消订阅/移除监听器/清除定时器 - 禁止在组件外部持有组件引用(会导致组件无法被 GC) - 图片必须使用 expo-image 或 react-native-fast-image,禁止原生 Image 加载大图 - FlatList/FlashList 必须设置 removeClippedSubviews={true} - 长列表必须使用 FlashList(替代 FlatList),设置 estimatedItemSize - 禁止在 renderItem 中创建新的函数/对象引用(使用 useCallback/useMemo) ## 电池优化规则 - 定位:仅在必要时请求,使用 foregroundService 而非持续后台定位 - 网络:批量请求,使用 NetInfo 检测网络状态后再发起请求 - 动画:使用 useAnimatedStyle(原生线程),禁止 JS 线程驱动动画 - 后台任务:使用 expo-background-fetch,设置合理的 minimumInterval - 传感器:使用后立即停止监听,禁止持续监听加速度计/陀螺仪 ## 权限处理规则 - 所有权限请求前必须显示自定义说明弹窗(解释为什么需要) - 必须处理三种状态:granted / denied / blocked - 权限被拒绝时必须提供降级方案(不能崩溃或白屏) - 权限被永久拒绝时引导用户到系统设置页 - iOS Info.plist 和 Android AndroidManifest.xml 必须声明所有使用的权限 - 禁止在应用启动时一次性请求所有权限(按需请求) ## 离线支持规则 - 所有 API 请求必须有离线降级策略 - 关键数据必须本地缓存(使用 TanStack Query 的 persistQueryClient) - 网络状态变化时自动重试失败的请求 - 离线时显示明确的离线状态指示器 - 表单数据在提交失败时必须本地保存,网络恢复后自动重试 ## 平台一致性规则 - 使用 Platform.select() 处理平台差异,禁止 Platform.OS === 'ios' 的 if-else - 导航栏样式必须分别适配 iOS(大标题)和 Android(Material) - 触觉反馈:iOS 使用 expo-haptics,Android 使用 Vibration API - 日期选择器:iOS 使用滚轮样式,Android 使用日历样式 - 状态栏:iOS 默认浅色,Android 需要手动设置 translucent ## 包体积控制规则 - 禁止引入 lodash 全量包(使用 lodash-es 或单独导入) - 图片资源必须压缩(PNG 使用 pngquant,JPEG 质量 ≤ 80%) - 使用 expo-asset 延迟加载非首屏资源 - 字体文件仅包含使用的字重(禁止引入完整字体族) - 每次添加新依赖前检查包体积影响(使用 bundlephobia) - 启用 Hermes 引擎(React Native 默认) - 使用 ProGuard/R8(Android)和 Bitcode(iOS)进行代码压缩 ## 安全规则 - 敏感数据(token、密码、API key)必须存储在 expo-secure-store - 禁止在 console.log 中输出敏感信息(生产环境移除所有 console) - API 请求必须使用 HTTPS,生产环境启用证书固定 - 禁止在代码中硬编码 API 密钥(使用环境变量) - 启用代码混淆(Android ProGuard / iOS 编译优化) ## 无障碍规则 - 所有可交互元素必须设置 accessibilityLabel - 图片必须设置 accessibilityRole="image" 和描述性 label - 表单控件必须关联 accessibilityHint - 支持 Dynamic Type(iOS)和系统字体缩放(Android) - 最小触摸目标:44x44pt(iOS)/ 48x48dp(Android)

2.2 Kiro Steering 移动端分层规则

通用移动端规则(.kiro/steering/mobile-general.md

--- inclusion: always --- # 移动端通用 Steering 规则 ## 生命周期管理 - 组件卸载时必须清理所有副作用(定时器、订阅、监听器) - 应用进入后台时暂停非必要操作(动画、轮询、传感器) - 应用恢复前台时刷新过期数据 - 处理低内存警告(iOS didReceiveMemoryWarning / Android onTrimMemory) ## 网络请求 - 所有请求必须设置超时(默认 15 秒) - 必须处理网络错误并显示用户友好的错误信息 - 使用指数退避重试策略(最多 3 次) - 大文件上传/下载必须支持断点续传 ## 导航 - 深层导航必须支持 deep linking - 返回手势不能导致数据丢失(未保存的表单需确认) - 导航栈不能无限增长(使用 replace 替代 push 在适当场景) ## 测试 - 每个屏幕至少一个快照测试 - 关键业务流程必须有 E2E 测试 - 权限相关功能必须有 mock 测试 - 离线场景必须有专门的测试用例

iOS SwiftUI 专用规则(.kiro/steering/ios-swiftui.md

--- inclusion: auto globs: ["**/*.swift"] --- # iOS SwiftUI Steering 规则 ## SwiftUI 最佳实践 - 使用 @Observable 宏(iOS 17+)替代 ObservableObject - 视图体(body)中禁止执行副作用或复杂计算 - 使用 .task {} 替代 .onAppear + async 模式 - 列表使用 List + LazyVStack,禁止在 ScrollView 中嵌套 ForEach 渲染大量视图 - 图片加载使用 AsyncImage 或第三方库(Kingfisher/Nuke),设置缓存策略 ## iOS 26 Liquid Glass 适配 - 使用 .glassEffect() 修饰符实现毛玻璃效果 - 导航栏使用系统默认的 Liquid Glass 样式 - TabBar 使用 .tabBarStyle(.automatic) 自动适配 - 避免自定义导航栏覆盖系统 Liquid Glass 效果 ## 内存管理 - 使用 [weak self] 在闭包中避免循环引用 - Combine 订阅必须存储在 cancellables Set 中并在 deinit 中取消 - 大图片使用 UIImage.preparingThumbnail(of:) 降采样 - Core Data 使用 NSBatchDeleteRequest 批量删除,避免逐条删除 ## 权限(iOS 特定) - Info.plist 必须包含所有权限的 Usage Description - 使用 PHPhotoLibrary.authorizationStatus(for:) 检查照片权限级别 - 定位权限区分 whenInUse 和 always,优先请求 whenInUse - 推送通知使用 UNUserNotificationCenter.requestAuthorization ## App Store 合规 - 禁止使用私有 API - 必须提供 App Privacy 标签所需的隐私信息 - 应用内购买必须使用 StoreKit 2 - 登录功能必须支持 Sign in with Apple

Android Jetpack Compose 专用规则(.kiro/steering/android-compose.md

--- inclusion: auto globs: ["**/*.kt"] --- # Android Jetpack Compose Steering 规则 ## Compose 最佳实践 - Composable 函数必须是无副作用的(副作用使用 LaunchedEffect/SideEffect) - 使用 remember {} 缓存计算结果,避免重组时重复计算 - 列表使用 LazyColumn/LazyRow,设置 key 参数优化重组 - 状态提升:UI 状态由 ViewModel 持有,Composable 只负责渲染 - 使用 derivedStateOf {} 避免不必要的重组 ## Material 3 Expressive 适配 - 使用 MaterialTheme.colorScheme 获取颜色,禁止硬编码颜色值 - 动态颜色:使用 dynamicDarkColorScheme/dynamicLightColorScheme(Android 12+) - 组件使用 Material 3 版本(androidx.compose.material3) - 弹性动画使用 Material Motion 规范 ## 内存管理 - ViewModel 中使用 viewModelScope 管理协程生命周期 - 避免在 Composable 中创建 CoroutineScope(使用 rememberCoroutineScope) - Bitmap 处理使用 BitmapFactory.Options.inSampleSize 降采样 - Room 数据库查询使用 Flow,自动感知生命周期 ## 权限(Android 特定) - 使用 rememberLauncherForActivityResult + RequestPermission 请求权限 - Android 13+ 细分媒体权限(READ_MEDIA_IMAGES/VIDEO/AUDIO) - Android 14+ 照片选择器优先使用 PickVisualMedia(无需权限) - 后台定位需要 ACCESS_BACKGROUND_LOCATION(单独请求) - 通知权限(POST_NOTIFICATIONS)在 Android 13+ 需要运行时请求 ## Google Play 合规 - targetSdkVersion 必须满足 Google Play 最新要求(2025: API 35) - 使用 App Bundle(.aab)而非 APK 发布 - 敏感权限必须在 Play Console 声明用途 - 数据安全部分必须准确填写

2.3 Cursor Rules 移动端模板

React Native 项目(.cursor/rules/react-native.mdc

--- description: React Native 移动端开发规则 globs: ["src/**/*.tsx", "src/**/*.ts", "app/**/*.tsx"] --- # React Native 开发规则 ## 组件规范 - 使用函数组件 + TypeScript,禁止 class 组件 - Props 使用 interface 定义,必须导出类型 - 组件文件结构:类型定义 → 组件 → 样式(StyleSheet.create) - 禁止在组件内部定义子组件(提取到独立文件) ## 性能规则 - FlatList renderItem 使用 useCallback 包裹 - 避免在 render 中创建新对象/数组(使用 useMemo) - 图片使用 expo-image,设置 contentFit 和 placeholder - 动画使用 reanimated worklet,禁止 Animated API 的 JS 驱动动画 - 避免不必要的 re-render:使用 React.memo + 浅比较 ## 导航规则 - 使用 Expo Router 的文件系统路由 - 类型安全导航:使用 useRouter<T>() 泛型 - 深层链接必须在 app.json 中配置 scheme - 模态页面使用 presentation: 'modal' ## 错误处理 - 每个屏幕必须有 ErrorBoundary - API 错误必须显示用户友好的错误信息 - 网络错误必须区分离线和服务器错误 - 崩溃上报使用 Sentry 或 Bugsnag

Flutter 项目(.cursor/rules/flutter.mdc

--- description: Flutter 移动端开发规则 globs: ["lib/**/*.dart"] --- # Flutter 开发规则 ## Widget 规范 - 优先使用 StatelessWidget,仅在需要时使用 StatefulWidget - 状态管理使用 Riverpod 2.x / Bloc,禁止全局变量 - Widget 拆分:单个 build 方法不超过 80 行 - 使用 const 构造函数优化重建性能 ## 性能规则 - ListView.builder 替代 ListView(大列表) - 图片使用 cached_network_image,设置 memCacheWidth/Height - 避免在 build 中执行异步操作(使用 FutureBuilder/StreamBuilder) - 使用 RepaintBoundary 隔离频繁重绘的区域 - 禁止在 build 中创建 TextEditingController(使用 late 或 initState) ## 平台适配 - 使用 Platform.isIOS / Platform.isAndroid 处理平台差异 - Cupertino 组件用于 iOS 风格,Material 组件用于 Android 风格 - 使用 flutter_adaptive_scaffold 处理多屏幕尺寸 - 状态栏样式使用 SystemChrome.setSystemUIOverlayStyle ## 资源管理 - 图片放在 assets/ 目录,使用 1x/2x/3x 多分辨率 - 字体仅包含使用的字重 - 使用 flutter_gen 生成类型安全的资源引用 - 国际化使用 flutter_localizations + arb 文件

2.4 跨平台统一 Steering 规则模板

以下是一个可同时用于 CLAUDE.md 和 Kiro Steering 的跨平台移动端规则核心模板:

# 移动端跨平台核心规则 ## 🚫 绝对禁止(违反即拒绝 PR) 1. 禁止在生产代码中使用 console.log / print / NSLog 输出敏感信息 2. 禁止明文存储用户凭证(密码、token、API key) 3. 禁止在主线程执行耗时操作(网络请求、文件 I/O、复杂计算) 4. 禁止忽略权限请求结果(必须处理 granted/denied/blocked) 5. 禁止无限制的后台任务(必须设置超时和取消机制) 6. 禁止引入未经审查的第三方库(检查维护状态、安全漏洞、包体积) 7. 禁止硬编码 IP 地址、域名或环境配置 8. 禁止在 App Store / Play Store 审核中使用测试数据 ## ⚠️ 必须遵守(代码审查重点) 1. 所有副作用必须有对应的清理逻辑 2. 所有网络请求必须有超时、重试和错误处理 3. 所有用户输入必须验证和清理 4. 所有列表必须使用虚拟化渲染(FlatList/LazyColumn/ListView.builder) 5. 所有图片必须有加载状态、错误状态和缓存策略 6. 所有导航必须支持 deep linking 7. 所有文本必须支持国际化(即使当前只有一种语言) 8. 所有触摸目标最小 44x44pt / 48x48dp ## ✅ 推荐实践(提升代码质量) 1. 使用设计 token 而非硬编码颜色/字体/间距 2. 组件设计遵循单一职责原则 3. 业务逻辑与 UI 分离(MVVM / Clean Architecture) 4. 使用 TypeScript strict / Dart strict / Swift strict concurrency 5. 关键路径有单元测试覆盖 6. 使用 CI/CD 自动化构建和测试

3. 反模式一:内存泄漏(Memory Leaks)

3.1 问题描述

内存泄漏是移动端最隐蔽也最致命的问题——应用不会立即崩溃,而是随着使用时间增长逐渐变慢,最终被系统强制杀死。AI 生成的代码特别容易在以下场景产生内存泄漏:

  • 未清理的事件监听器和订阅:组件卸载后监听器仍在运行
  • 闭包中的强引用:异步回调持有已销毁组件的引用
  • 未释放的定时器:setInterval/setTimeout 未在组件卸载时清除
  • 图片缓存失控:大量高分辨率图片未设置缓存上限
  • 导航栈累积:不断 push 新页面而不 pop,导致页面栈无限增长

3.2 AI 生成的问题代码 vs 正确代码

React Native:未清理的订阅

// ❌ AI 常见错误:未清理 useEffect 中的订阅 import { useEffect, useState } from 'react'; import { AppState, Dimensions } from 'react-native'; import NetInfo from '@react-native-community/netinfo'; function DashboardScreen() { const [isOnline, setIsOnline] = useState(true); const [orientation, setOrientation] = useState('portrait'); useEffect(() => { // 问题 1:NetInfo 订阅未取消 NetInfo.addEventListener(state => { setIsOnline(state.isConnected ?? false); }); // 问题 2:AppState 监听未移除 AppState.addEventListener('change', (nextState) => { if (nextState === 'active') { // 刷新数据... } }); // 问题 3:Dimensions 监听未移除 Dimensions.addEventListener('change', ({ window }) => { setOrientation(window.width > window.height ? 'landscape' : 'portrait'); }); // 问题 4:定时器未清除 setInterval(() => { // 轮询数据... }, 30000); // ❌ 没有返回清理函数! }, []); return (/* ... */); }
// ✅ 正确写法:所有副作用都有对应的清理 import { useEffect, useState, useCallback } from 'react'; import { AppState, useWindowDimensions } from 'react-native'; import NetInfo from '@react-native-community/netinfo'; function DashboardScreen() { const [isOnline, setIsOnline] = useState(true); const { width, height } = useWindowDimensions(); // 自动管理生命周期 const orientation = width > height ? 'landscape' : 'portrait'; // 网络状态监听(自动清理) useEffect(() => { const unsubscribe = NetInfo.addEventListener(state => { setIsOnline(state.isConnected ?? false); }); return () => unsubscribe(); // ✅ 清理订阅 }, []); // AppState 监听(自动清理) useEffect(() => { const subscription = AppState.addEventListener('change', (nextState) => { if (nextState === 'active') { // 刷新数据... } }); return () => subscription.remove(); // ✅ 移除监听 }, []); // 轮询(自动清理) useEffect(() => { const intervalId = setInterval(() => { // 轮询数据... }, 30000); return () => clearInterval(intervalId); // ✅ 清除定时器 }, []); // ✅ 使用 useWindowDimensions 替代手动监听 Dimensions // 它会自动订阅和取消订阅 return (/* ... */); }

SwiftUI:闭包中的循环引用

// ❌ AI 常见错误:闭包中的强引用导致循环引用 class LocationManager: ObservableObject { @Published var currentLocation: CLLocation? private var locationManager = CLLocationManager() private var timer: Timer? init() { // 问题 1:Timer 持有 self 的强引用 timer = Timer.scheduledTimer(withTimeInterval: 60, repeats: true) { _ in self.refreshLocation() // ❌ 强引用 self } // 问题 2:闭包持有 self 的强引用 NotificationCenter.default.addObserver( forName: UIApplication.didBecomeActiveNotification, object: nil, queue: .main ) { _ in self.refreshLocation() // ❌ 强引用 self } } func refreshLocation() { // 刷新位置... } // ❌ 没有 deinit 清理! }
// ✅ 正确写法:使用 [weak self] 和 deinit 清理 class LocationManager: ObservableObject { @Published var currentLocation: CLLocation? private var locationManager = CLLocationManager() private var timer: Timer? private var cancellables = Set<AnyCancellable>() init() { // ✅ Timer 使用 [weak self] timer = Timer.scheduledTimer(withTimeInterval: 60, repeats: true) { [weak self] _ in self?.refreshLocation() } // ✅ 使用 Combine 的 NotificationCenter publisher(自动管理生命周期) NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification) .sink { [weak self] _ in self?.refreshLocation() } .store(in: &cancellables) // ✅ 存储在 cancellables 中 } func refreshLocation() { // 刷新位置... } deinit { timer?.invalidate() // ✅ 清理定时器 cancellables.removeAll() // ✅ 取消所有订阅 } }

Flutter:未 dispose 的 Controller

// ❌ AI 常见错误:Controller 未 dispose class ChatScreen extends StatefulWidget { @override State<ChatScreen> createState() => _ChatScreenState(); } class _ChatScreenState extends State<ChatScreen> { final _scrollController = ScrollController(); final _textController = TextEditingController(); final _focusNode = FocusNode(); late StreamSubscription _messageSubscription; @override void initState() { super.initState(); // 问题:订阅了 Stream 但没有在 dispose 中取消 _messageSubscription = chatService.messageStream.listen((message) { setState(() { // 添加消息... }); _scrollController.animateTo( _scrollController.position.maxScrollExtent, duration: Duration(milliseconds: 300), curve: Curves.easeOut, ); }); } // ❌ 没有 dispose 方法!所有 Controller 和订阅都会泄漏 // ScrollController、TextEditingController、FocusNode、StreamSubscription // 全部没有被释放 @override Widget build(BuildContext context) { return Scaffold(/* ... */); } }
// ✅ 正确写法:所有资源在 dispose 中释放 class ChatScreen extends StatefulWidget { const ChatScreen({super.key}); @override State<ChatScreen> createState() => _ChatScreenState(); } class _ChatScreenState extends State<ChatScreen> { final _scrollController = ScrollController(); final _textController = TextEditingController(); final _focusNode = FocusNode(); late final StreamSubscription _messageSubscription; @override void initState() { super.initState(); _messageSubscription = chatService.messageStream.listen((message) { if (!mounted) return; // ✅ 检查 widget 是否仍然挂载 setState(() { // 添加消息... }); // ✅ 延迟滚动,确保列表已更新 WidgetsBinding.instance.addPostFrameCallback((_) { if (_scrollController.hasClients) { _scrollController.animateTo( _scrollController.position.maxScrollExtent, duration: const Duration(milliseconds: 300), curve: Curves.easeOut, ); } }); }); } @override void dispose() { _messageSubscription.cancel(); // ✅ 取消 Stream 订阅 _scrollController.dispose(); // ✅ 释放 ScrollController _textController.dispose(); // ✅ 释放 TextEditingController _focusNode.dispose(); // ✅ 释放 FocusNode super.dispose(); } @override Widget build(BuildContext context) { return Scaffold(/* ... */); } }

Jetpack Compose:协程泄漏

// ❌ AI 常见错误:在 Composable 中启动不受管理的协程 @Composable fun LiveDataScreen(viewModel: LiveDataViewModel) { val scope = CoroutineScope(Dispatchers.IO) // ❌ 每次重组创建新 scope LaunchedEffect(Unit) { // 问题 1:使用 GlobalScope,永远不会被取消 GlobalScope.launch { while (true) { viewModel.fetchLatestData() delay(30_000) } } } // 问题 2:在 onClick 中使用未管理的 scope Button(onClick = { scope.launch { // ❌ 这个 scope 不会随 Composable 销毁而取消 viewModel.uploadData() } }) { Text("上传") } }
// ✅ 正确写法:使用生命周期感知的协程 scope @Composable fun LiveDataScreen(viewModel: LiveDataViewModel) { val scope = rememberCoroutineScope() // ✅ 随 Composable 生命周期管理 // ✅ LaunchedEffect 自动在 Composable 离开组合时取消 LaunchedEffect(Unit) { while (isActive) { // ✅ 检查协程是否仍然活跃 viewModel.fetchLatestData() delay(30_000) } } Button(onClick = { scope.launch { // ✅ 使用 rememberCoroutineScope viewModel.uploadData() } }) { Text("上传") } } // ✅ ViewModel 中使用 viewModelScope class LiveDataViewModel : ViewModel() { private var pollingJob: Job? = null fun startPolling() { pollingJob?.cancel() // ✅ 取消之前的轮询 pollingJob = viewModelScope.launch { // ✅ 随 ViewModel 销毁自动取消 while (isActive) { fetchLatestData() delay(30_000) } } } suspend fun fetchLatestData() { /* ... */ } suspend fun uploadData() { /* ... */ } override fun onCleared() { super.onCleared() pollingJob?.cancel() // ✅ 显式取消(虽然 viewModelScope 会自动取消) } }

3.3 内存泄漏检测工具

工具平台价格用途
Xcode Instruments (Leaks)iOS免费运行时内存泄漏检测
Xcode Memory Graph DebuggeriOS免费可视化对象引用关系
Android Studio ProfilerAndroid免费内存分配追踪和堆转储
LeakCanaryAndroid免费(开源)自动检测 Activity/Fragment 泄漏
FlipperReact Native免费(开源)内存、网络、布局调试
Flutter DevToolsFlutter免费内存快照对比和泄漏检测
FinotesiOS/Android$99/月起生产环境内存泄漏监控
Sentry Performance全平台免费额度 / $26/月起生产环境性能和内存监控

3.4 提示词模板:内存泄漏检测与修复

请审查以下 [React Native / SwiftUI / Flutter / Compose] 代码的内存管理: [粘贴代码] 检查清单: 1. 所有 useEffect / LaunchedEffect / initState 是否有对应的清理逻辑? 2. 闭包中是否存在对 self/this/组件的强引用? 3. 定时器(Timer/setInterval)是否在组件销毁时清除? 4. Stream/Flow/Combine 订阅是否在组件销毁时取消? 5. 大对象(图片、数据集)是否有缓存上限和释放策略? 6. 导航栈是否可能无限增长? 7. 事件监听器是否在不需要时移除? 对于发现的每个问题,请: - 标注问题位置和严重程度(高/中/低) - 解释为什么会导致内存泄漏 - 提供修复后的代码

4. 反模式二:电池消耗(Battery Drain)

4.1 问题描述

电池消耗是用户卸载应用的首要原因之一。AI 生成的代码往往在功能上正确,但在能耗上极度浪费——频繁的网络请求、持续的 GPS 定位、不必要的后台任务、JS 线程驱动的动画等都会快速耗尽电池。研究表明,批量合并网络请求可减少约 20% 的空闲耗电,而将定位从持续模式改为按需模式可节省高达 40% 的定位相关能耗。

4.2 AI 生成的问题代码 vs 正确代码

React Native:频繁轮询和持续定位

// ❌ AI 常见错误:频繁轮询 + 持续高精度定位 import * as Location from 'expo-location'; function DeliveryTracker() { const [location, setLocation] = useState(null); const [orders, setOrders] = useState([]); useEffect(() => { // 问题 1:每 5 秒轮询一次订单状态(极度耗电) const pollInterval = setInterval(async () => { const response = await fetch('https://api.example.com/orders'); const data = await response.json(); setOrders(data); }, 5000); // ❌ 5 秒太频繁 // 问题 2:持续高精度 GPS 定位 Location.watchPositionAsync( { accuracy: Location.Accuracy.BestForNavigation, // ❌ 最高精度,极度耗电 distanceInterval: 0, // ❌ 任何移动都触发更新 timeInterval: 1000, // ❌ 每秒更新 }, (newLocation) => { setLocation(newLocation); // 问题 3:每次位置更新都发送到服务器 fetch('https://api.example.com/location', { method: 'POST', body: JSON.stringify(newLocation.coords), }); } ); // ❌ 没有清理! }, []); return (/* ... */); }
// ✅ 正确写法:智能轮询 + 自适应定位精度 import * as Location from 'expo-location'; import NetInfo from '@react-native-community/netinfo'; import { AppState } from 'react-native'; function DeliveryTracker() { const [location, setLocation] = useState(null); const [orders, setOrders] = useState([]); const appState = useRef(AppState.currentState); const locationBuffer = useRef<Location.LocationObject[]>([]); useEffect(() => { let pollInterval: NodeJS.Timeout; let locationSubscription: Location.LocationSubscription; let appStateSubscription: any; const startTracking = async () => { // ✅ 根据应用状态调整轮询频率 const getPollInterval = () => { return appState.current === 'active' ? 30000 : 120000; // 前台 30s,后台 2min }; const pollOrders = async () => { const netState = await NetInfo.fetch(); if (!netState.isConnected) return; // ✅ 离线时跳过 try { const response = await fetch('https://api.example.com/orders'); const data = await response.json(); setOrders(data); } catch (error) { // 静默失败,下次轮询重试 } // ✅ 动态调整下次轮询间隔 pollInterval = setTimeout(pollOrders, getPollInterval()); }; pollInterval = setTimeout(pollOrders, getPollInterval()); // ✅ 自适应定位精度 locationSubscription = await Location.watchPositionAsync( { accuracy: Location.Accuracy.Balanced, // ✅ 平衡精度(~100m) distanceInterval: 50, // ✅ 移动 50 米才更新 timeInterval: 10000, // ✅ 最多每 10 秒更新一次 }, (newLocation) => { setLocation(newLocation); // ✅ 缓冲位置数据,批量上传 locationBuffer.current.push(newLocation); if (locationBuffer.current.length >= 5) { uploadLocations(locationBuffer.current); locationBuffer.current = []; } } ); // ✅ 监听应用状态变化 appStateSubscription = AppState.addEventListener('change', (nextState) => { if (nextState === 'background') { // 进入后台:降低定位频率,暂停非必要轮询 uploadLocations(locationBuffer.current); // 上传缓冲数据 locationBuffer.current = []; } appState.current = nextState; }); }; startTracking(); return () => { clearTimeout(pollInterval); // ✅ 清理轮询 locationSubscription?.remove(); // ✅ 停止定位 appStateSubscription?.remove(); // ✅ 移除监听 }; }, []); return (/* ... */); } // ✅ 批量上传位置数据 async function uploadLocations(locations: Location.LocationObject[]) { if (locations.length === 0) return; try { await fetch('https://api.example.com/locations/batch', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(locations.map(l => l.coords)), }); } catch { // 失败时保留数据,下次重试 } }

SwiftUI:不必要的后台刷新

// ❌ AI 常见错误:过度使用后台刷新 class WeatherViewModel: ObservableObject { @Published var weather: Weather? private var timer: Timer? init() { // 问题:即使应用在后台也每分钟刷新天气 timer = Timer.scheduledTimer(withTimeInterval: 60, repeats: true) { [weak self] _ in Task { await self?.fetchWeather() // ❌ 后台也在请求 } } // 问题:启动时立即请求,不检查缓存 Task { await fetchWeather() } } func fetchWeather() async { // 每次都发网络请求,不使用缓存 let url = URL(string: "https://api.weather.com/current")! let (data, _) = try! await URLSession.shared.data(from: url) // ❌ 强制解包 weather = try! JSONDecoder().decode(Weather.self, from: data) } }
// ✅ 正确写法:智能缓存 + 生命周期感知刷新 class WeatherViewModel: ObservableObject { @Published var weather: Weather? @Published var lastUpdated: Date? private var refreshTask: Task<Void, Never>? private let cache = WeatherCache() private let minRefreshInterval: TimeInterval = 600 // 10 分钟 func onAppear() { // ✅ 先加载缓存 if let cached = cache.load() { weather = cached.weather lastUpdated = cached.timestamp } // ✅ 仅在缓存过期时刷新 if shouldRefresh() { startAutoRefresh() } } func onDisappear() { // ✅ 离开页面时停止自动刷新 refreshTask?.cancel() refreshTask = nil } private func shouldRefresh() -> Bool { guard let lastUpdated else { return true } return Date().timeIntervalSince(lastUpdated) > minRefreshInterval } private func startAutoRefresh() { refreshTask?.cancel() refreshTask = Task { [weak self] in while !Task.isCancelled { await self?.fetchWeather() try? await Task.sleep(for: .seconds(600)) // ✅ 10 分钟刷新一次 } } } func fetchWeather() async { do { let url = URL(string: "https://api.weather.com/current")! let (data, _) = try await URLSession.shared.data(from: url) let decoded = try JSONDecoder().decode(Weather.self, from: data) await MainActor.run { weather = decoded lastUpdated = Date() } // ✅ 更新缓存 cache.save(weather: decoded, timestamp: Date()) } catch { // ✅ 网络错误时使用缓存数据,不崩溃 print("Weather fetch failed, using cached data: \(error)") } } }

4.3 电池优化检查清单

检查项严重程度检测方法
GPS 定位精度是否过高🔴 高检查 accuracy 参数
网络轮询间隔是否过短(<30s)🔴 高搜索 setInterval/Timer
后台任务是否有超时限制🔴 高检查 BackgroundFetch 配置
动画是否在原生线程运行🟡 中检查 useNativeDriver/worklet
传感器监听是否及时停止🟡 中检查加速度计/陀螺仪订阅
网络请求是否批量合并🟡 中检查请求频率和合并逻辑
图片是否按需加载🟢 低检查懒加载和缓存策略
日志输出是否在生产环境关闭🟢 低检查 console.log/print

4.4 提示词模板:电池优化审查

请审查以下移动端代码的电池消耗情况: [粘贴代码] 重点检查: 1. 网络请求频率是否合理?是否可以批量合并? 2. GPS 定位精度是否过高?是否可以降低? 3. 后台任务是否有超时和取消机制? 4. 动画是否运行在原生线程? 5. 传感器(加速度计、陀螺仪)是否在不需要时停止? 6. 应用进入后台时是否暂停非必要操作? 对于每个问题,请提供: - 预估的电池影响(高/中/低) - 优化后的代码 - 预期的电池节省百分比

5. 反模式三:缺少权限处理(Missing Permission Handling)

5.1 问题描述

权限处理是 AI 生成移动端代码中最常被忽略的部分。AI 倾向于假设权限已经被授予,直接调用需要权限的 API,导致应用在权限未授予时崩溃或功能失效。研究表明,超过 82% 的用户希望应用在请求权限前提供清晰的理由说明,而提供合理解释可使权限授予率提高 81%。

常见问题包括:

  • 不检查权限状态就直接使用:相机、定位、相册等 API 在无权限时崩溃
  • 不处理权限拒绝:用户拒绝后应用白屏或功能完全不可用
  • 不处理”永久拒绝”:用户选择”不再询问”后无法引导到设置页
  • 启动时请求所有权限:一次性弹出多个权限请求,用户体验极差
  • 不区分 iOS 和 Android 权限模型:两个平台的权限粒度和流程不同

5.2 AI 生成的问题代码 vs 正确代码

React Native:不检查权限直接使用

// ❌ AI 常见错误:不检查权限直接使用相机 import { Camera } from 'expo-camera'; function ScannerScreen() { // 问题 1:没有检查相机权限 // 问题 2:没有处理权限拒绝 // 问题 3:没有处理权限永久拒绝 return ( <Camera style={{ flex: 1 }} onBarCodeScanned={({ data }) => { console.log('Scanned:', data); }} /> // ❌ 如果用户没有授予相机权限,这里会崩溃或显示黑屏 ); }
// ✅ 正确写法:完整的权限处理流程 import { useState, useEffect } from 'react'; import { Camera, CameraView } from 'expo-camera'; import { Alert, Linking, Platform, View, Text, TouchableOpacity, StyleSheet } from 'react-native'; type PermissionState = 'loading' | 'undetermined' | 'granted' | 'denied' | 'blocked'; function ScannerScreen() { const [permissionState, setPermissionState] = useState<PermissionState>('loading'); useEffect(() => { checkPermission(); }, []); const checkPermission = async () => { const { status } = await Camera.getCameraPermissionsAsync(); if (status === 'granted') { setPermissionState('granted'); } else if (status === 'undetermined') { setPermissionState('undetermined'); } else { // iOS: 'denied' 意味着可以再次请求或已永久拒绝 // Android: 需要检查 canAskAgain setPermissionState('denied'); } }; const requestPermission = async () => { // ✅ 先显示自定义说明(在系统弹窗之前) Alert.alert( '需要相机权限', '扫描二维码需要使用您的相机。我们不会录制或存储任何影像。', [ { text: '暂不开启', style: 'cancel' }, { text: '开启相机', onPress: async () => { const { status, canAskAgain } = await Camera.requestCameraPermissionsAsync(); if (status === 'granted') { setPermissionState('granted'); } else if (!canAskAgain) { setPermissionState('blocked'); // ✅ 永久拒绝 } else { setPermissionState('denied'); } }, }, ] ); }; const openSettings = () => { // ✅ 引导用户到系统设置页 if (Platform.OS === 'ios') { Linking.openURL('app-settings:'); } else { Linking.openSettings(); } }; // ✅ 根据权限状态渲染不同 UI switch (permissionState) { case 'loading': return <LoadingView />; case 'undetermined': case 'denied': return ( <View style={styles.permissionContainer}> <Text style={styles.icon}>📷</Text> <Text style={styles.title}>需要相机权限</Text> <Text style={styles.description}> 扫描二维码需要使用相机。{'\n'} 我们不会录制或存储任何影像。 </Text> <TouchableOpacity style={styles.button} onPress={requestPermission}> <Text style={styles.buttonText}>开启相机权限</Text> </TouchableOpacity> {/* ✅ 提供手动输入的降级方案 */} <TouchableOpacity style={styles.linkButton} onPress={() => {/* 导航到手动输入页 */}}> <Text style={styles.linkText}>手动输入编码</Text> </TouchableOpacity> </View> ); case 'blocked': return ( <View style={styles.permissionContainer}> <Text style={styles.icon}>⚙️</Text> <Text style={styles.title}>相机权限已关闭</Text> <Text style={styles.description}> 请在系统设置中开启相机权限,{'\n'} 才能使用扫码功能。 </Text> <TouchableOpacity style={styles.button} onPress={openSettings}> <Text style={styles.buttonText}>前往设置</Text> </TouchableOpacity> <TouchableOpacity style={styles.linkButton} onPress={() => {/* 导航到手动输入页 */}}> <Text style={styles.linkText}>手动输入编码</Text> </TouchableOpacity> </View> ); case 'granted': return ( <CameraView style={{ flex: 1 }} barcodeScannerSettings={{ barcodeTypes: ['qr'] }} onBarcodeScanned={({ data }) => { // 处理扫描结果... }} /> ); } } const styles = StyleSheet.create({ permissionContainer: { flex: 1, justifyContent: 'center', alignItems: 'center', padding: 32, }, icon: { fontSize: 64, marginBottom: 16 }, title: { fontSize: 20, fontWeight: '700', marginBottom: 8, textAlign: 'center' }, description: { fontSize: 15, color: '#666', textAlign: 'center', marginBottom: 24, lineHeight: 22 }, button: { backgroundColor: '#007AFF', paddingHorizontal: 32, paddingVertical: 14, borderRadius: 12, marginBottom: 16, }, buttonText: { color: '#fff', fontSize: 16, fontWeight: '600' }, linkButton: { padding: 8 }, linkText: { color: '#007AFF', fontSize: 15 }, });

Android Compose:权限请求不完整

// ❌ AI 常见错误:权限处理不完整 @Composable fun CameraScreen() { // 问题:只处理了 granted,没有处理 denied 和 permanently denied val permissionState = rememberPermissionState(Manifest.permission.CAMERA) if (permissionState.status.isGranted) { CameraPreview() } else { // ❌ 直接请求,没有解释为什么需要 Button(onClick = { permissionState.launchPermissionRequest() }) { Text("授予相机权限") } // ❌ 如果用户选择"不再询问",这个按钮将永远无效 } }
// ✅ 正确写法:完整的 Android 权限处理 @OptIn(ExperimentalPermissionsApi::class) @Composable fun CameraScreen( onManualInput: () -> Unit // ✅ 降级方案回调 ) { val permissionState = rememberPermissionState(Manifest.permission.CAMERA) var showRationale by remember { mutableStateOf(false) } // ✅ 权限说明对话框 if (showRationale) { AlertDialog( onDismissRequest = { showRationale = false }, title = { Text("需要相机权限") }, text = { Text("扫描二维码需要使用相机。我们不会录制或存储任何影像。") }, confirmButton = { TextButton(onClick = { showRationale = false permissionState.launchPermissionRequest() }) { Text("开启") } }, dismissButton = { TextButton(onClick = { showRationale = false }) { Text("暂不") } } ) } when { // ✅ 已授权:显示相机 permissionState.status.isGranted -> { CameraPreview() } // ✅ 需要解释(用户之前拒绝过,但没有选择"不再询问") permissionState.status.shouldShowRationale -> { PermissionRequestUI( icon = Icons.Default.CameraAlt, title = "需要相机权限", description = "扫描二维码需要使用相机。\n我们不会录制或存储任何影像。", primaryAction = "开启相机权限", onPrimaryClick = { showRationale = true }, secondaryAction = "手动输入编码", onSecondaryClick = onManualInput ) } // ✅ 永久拒绝:引导到设置页 else -> { val context = LocalContext.current PermissionRequestUI( icon = Icons.Default.Settings, title = "相机权限已关闭", description = "请在系统设置中开启相机权限。", primaryAction = "前往设置", onPrimaryClick = { val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply { data = Uri.fromParts("package", context.packageName, null) } context.startActivity(intent) }, secondaryAction = "手动输入编码", onSecondaryClick = onManualInput ) } } } // ✅ 可复用的权限请求 UI 组件 @Composable fun PermissionRequestUI( icon: ImageVector, title: String, description: String, primaryAction: String, onPrimaryClick: () -> Unit, secondaryAction: String, onSecondaryClick: () -> Unit ) { Column( modifier = Modifier.fillMaxSize().padding(32.dp), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center ) { Icon(icon, contentDescription = null, modifier = Modifier.size(64.dp), tint = MaterialTheme.colorScheme.primary) Spacer(Modifier.height(16.dp)) Text(title, style = MaterialTheme.typography.headlineSmall, textAlign = TextAlign.Center) Spacer(Modifier.height(8.dp)) Text(description, style = MaterialTheme.typography.bodyMedium, textAlign = TextAlign.Center, color = MaterialTheme.colorScheme.onSurfaceVariant) Spacer(Modifier.height(24.dp)) Button(onClick = onPrimaryClick, modifier = Modifier.fillMaxWidth()) { Text(primaryAction) } Spacer(Modifier.height(8.dp)) TextButton(onClick = onSecondaryClick) { Text(secondaryAction) } } }

5.3 权限处理最佳实践矩阵

权限类型iOS 特殊处理Android 特殊处理降级方案
相机Info.plist: NSCameraUsageDescriptionCAMERA(运行时)手动输入/从相册选择
照片库PHPhotoLibrary 分级权限(limited/full)Android 14+: PickVisualMedia(无需权限)文件选择器
定位whenInUse vs always 分步请求ACCESS_FINE vs COARSE + BACKGROUND 分步手动输入地址
通知UNUserNotificationCenterAndroid 13+: POST_NOTIFICATIONS应用内消息中心
麦克风NSMicrophoneUsageDescriptionRECORD_AUDIO文字输入替代
联系人NSContactsUsageDescriptionREAD_CONTACTS手动输入联系人
蓝牙NSBluetoothAlwaysUsageDescriptionBLUETOOTH_CONNECT (Android 12+)USB 连接替代

5.4 提示词模板:权限处理生成

为 [应用功能] 生成完整的权限处理代码: 框架:[React Native / SwiftUI / Jetpack Compose / Flutter] 需要的权限:[相机 / 定位 / 照片 / 通知 / 麦克风 / ...] 必须包含: 1. 权限状态检查(granted / denied / undetermined / blocked) 2. 自定义权限说明弹窗(在系统弹窗之前显示,解释为什么需要) 3. 权限拒绝后的降级方案(应用不能崩溃或白屏) 4. 永久拒绝后引导到系统设置页 5. iOS 和 Android 的差异处理 6. 可复用的权限请求 UI 组件 权限请求时机: - 不要在应用启动时请求 - 在用户即将使用相关功能时请求(just-in-time) - 先显示自定义说明,再触发系统权限弹窗

6. 反模式四:离线支持差(Poor Offline Support)

6.1 问题描述

移动端用户经常处于弱网或离线环境——地铁、电梯、偏远地区、飞行模式。AI 生成的代码几乎总是假设网络始终可用,导致离线时应用完全不可用、数据丢失、或显示无意义的错误信息。实践表明,采用本地优先(local-first)的读取路径后,中端 Android 设备上的屏幕加载时间可从约 1.7 秒降至约 300 毫秒。

6.2 AI 生成的问题代码 vs 正确代码

React Native:无离线策略的数据加载

// ❌ AI 常见错误:完全依赖网络,无离线策略 function ProductListScreen() { const [products, setProducts] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { fetchProducts(); }, []); const fetchProducts = async () => { setLoading(true); try { const response = await fetch('https://api.example.com/products'); const data = await response.json(); setProducts(data); } catch (err) { setError('加载失败'); // ❌ 离线时只显示"加载失败",无任何数据 } finally { setLoading(false); } }; if (loading) return <ActivityIndicator />; if (error) return <Text>{error}</Text>; // ❌ 离线时用户看到的就是这个 // ❌ 没有缓存,每次进入页面都要重新加载 // ❌ 没有离线状态指示 // ❌ 没有重试机制 return <FlatList data={products} renderItem={/* ... */} />; }
// ✅ 正确写法:离线优先 + 智能缓存 + 自动重试 import { useQuery, useQueryClient, QueryClient } from '@tanstack/react-query'; import { createAsyncStoragePersister } from '@tanstack/query-async-storage-persister'; import AsyncStorage from '@react-native-async-storage/async-storage'; import NetInfo, { useNetInfo } from '@react-native-community/netinfo'; // ✅ 配置持久化缓存 const queryClient = new QueryClient({ defaultOptions: { queries: { gcTime: 1000 * 60 * 60 * 24, // 缓存 24 小时 staleTime: 1000 * 60 * 5, // 5 分钟内认为数据新鲜 retry: 3, // 自动重试 3 次 retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000), // 指数退避 }, }, }); const asyncStoragePersister = createAsyncStoragePersister({ storage: AsyncStorage, key: 'REACT_QUERY_CACHE', }); // ✅ 离线感知的产品列表 function ProductListScreen() { const netInfo = useNetInfo(); const isOffline = !netInfo.isConnected; const { data: products, isLoading, isError, error, refetch, dataUpdatedAt, } = useQuery({ queryKey: ['products'], queryFn: async () => { const response = await fetch('https://api.example.com/products'); if (!response.ok) throw new Error(`HTTP ${response.status}`); return response.json(); }, // ✅ 离线时使用缓存数据,不发起请求 enabled: !isOffline || !queryClient.getQueryData(['products']), }); return ( <View style={{ flex: 1 }}> {/* ✅ 离线状态指示器 */} {isOffline && ( <View style={styles.offlineBanner}> <Text style={styles.offlineText}>📡 当前处于离线模式,显示的是缓存数据</Text> </View> )} {/* ✅ 数据新鲜度提示 */} {dataUpdatedAt && ( <Text style={styles.freshness}> 最后更新:{new Date(dataUpdatedAt).toLocaleString()} </Text> )} {isLoading && !products ? ( <ActivityIndicator /> ) : isError && !products ? ( // ✅ 错误状态:区分离线和服务器错误 <View style={styles.errorContainer}> <Text style={styles.errorIcon}> {isOffline ? '📡' : '⚠️'} </Text> <Text style={styles.errorTitle}> {isOffline ? '无网络连接' : '加载失败'} </Text> <Text style={styles.errorMessage}> {isOffline ? '请检查网络连接后重试' : '服务器暂时不可用请稍后重试'} </Text> <TouchableOpacity style={styles.retryButton} onPress={() => refetch()}> <Text style={styles.retryText}>重试</Text> </TouchableOpacity> </View> ) : ( <FlatList data={products} renderItem={({ item }) => <ProductCard product={item} />} // ✅ 下拉刷新(仅在线时可用) refreshing={isLoading} onRefresh={isOffline ? undefined : refetch} /> )} </View> ); }

Flutter:离线表单数据丢失

// ❌ AI 常见错误:表单提交失败时数据丢失 class FeedbackForm extends StatefulWidget { @override State<FeedbackForm> createState() => _FeedbackFormState(); } class _FeedbackFormState extends State<FeedbackForm> { final _controller = TextEditingController(); Future<void> _submit() async { try { final response = await http.post( Uri.parse('https://api.example.com/feedback'), body: jsonEncode({'content': _controller.text}), ); if (response.statusCode == 200) { Navigator.pop(context); } } catch (e) { // ❌ 网络错误时只显示 SnackBar,用户的输入可能丢失 ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('提交失败,请重试')), ); // ❌ 如果用户此时退出页面,数据就丢失了 } } @override Widget build(BuildContext context) { return Scaffold( body: Column(children: [ TextField(controller: _controller), ElevatedButton(onPressed: _submit, child: Text('提交')), ]), ); } @override void dispose() { _controller.dispose(); super.dispose(); } }
// ✅ 正确写法:离线队列 + 自动重试 + 数据持久化 import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'dart:convert'; class FeedbackForm extends StatefulWidget { const FeedbackForm({super.key}); @override State<FeedbackForm> createState() => _FeedbackFormState(); } class _FeedbackFormState extends State<FeedbackForm> with WidgetsBindingObserver { final _controller = TextEditingController(); bool _isSubmitting = false; bool _isOffline = false; @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); _checkConnectivity(); _restoreDraft(); // ✅ 恢复草稿 } Future<void> _checkConnectivity() async { final result = await Connectivity().checkConnectivity(); setState(() => _isOffline = result.contains(ConnectivityResult.none)); // ✅ 监听网络变化 Connectivity().onConnectivityChanged.listen((results) { final wasOffline = _isOffline; setState(() => _isOffline = results.contains(ConnectivityResult.none)); // ✅ 网络恢复时自动重试队列中的请求 if (wasOffline && !_isOffline) { _retryPendingSubmissions(); } }); } // ✅ 自动保存草稿 Future<void> _saveDraft() async { final prefs = await SharedPreferences.getInstance(); await prefs.setString('feedback_draft', _controller.text); } // ✅ 恢复草稿 Future<void> _restoreDraft() async { final prefs = await SharedPreferences.getInstance(); final draft = prefs.getString('feedback_draft'); if (draft != null && draft.isNotEmpty) { _controller.text = draft; } } Future<void> _submit() async { if (_controller.text.trim().isEmpty) return; setState(() => _isSubmitting = true); if (_isOffline) { // ✅ 离线时保存到本地队列 await _saveToOfflineQueue(_controller.text); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('已保存,将在网络恢复后自动提交'), backgroundColor: Colors.orange, ), ); _clearDraft(); Navigator.pop(context); } } else { try { final response = await http.post( Uri.parse('https://api.example.com/feedback'), headers: {'Content-Type': 'application/json'}, body: jsonEncode({'content': _controller.text}), ).timeout(const Duration(seconds: 15)); // ✅ 设置超时 if (response.statusCode == 200) { _clearDraft(); if (mounted) Navigator.pop(context); } else { throw Exception('Server error: ${response.statusCode}'); } } catch (e) { // ✅ 提交失败时保存到离线队列 await _saveToOfflineQueue(_controller.text); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('提交失败,已保存到本地,稍后自动重试'), backgroundColor: Colors.orange, ), ); } } } setState(() => _isSubmitting = false); } Future<void> _saveToOfflineQueue(String content) async { final prefs = await SharedPreferences.getInstance(); final queue = prefs.getStringList('offline_queue') ?? []; queue.add(jsonEncode({ 'content': content, 'timestamp': DateTime.now().toIso8601String(), })); await prefs.setStringList('offline_queue', queue); } Future<void> _retryPendingSubmissions() async { final prefs = await SharedPreferences.getInstance(); final queue = prefs.getStringList('offline_queue') ?? []; if (queue.isEmpty) return; final failedItems = <String>[]; for (final item in queue) { try { final data = jsonDecode(item); await http.post( Uri.parse('https://api.example.com/feedback'), headers: {'Content-Type': 'application/json'}, body: jsonEncode(data), ).timeout(const Duration(seconds: 15)); } catch (e) { failedItems.add(item); // 重试失败的保留在队列中 } } await prefs.setStringList('offline_queue', failedItems); } Future<void> _clearDraft() async { final prefs = await SharedPreferences.getInstance(); await prefs.remove('feedback_draft'); } @override void didChangeAppLifecycleState(AppLifecycleState state) { if (state == AppLifecycleState.paused) { _saveDraft(); // ✅ 应用进入后台时保存草稿 } } @override void dispose() { WidgetsBinding.instance.removeObserver(this); _saveDraft(); // ✅ 页面销毁时保存草稿 _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('反馈'), actions: [ if (_isOffline) const Padding( padding: EdgeInsets.only(right: 16), child: Icon(Icons.cloud_off, color: Colors.orange), // ✅ 离线指示 ), ], ), body: Padding( padding: const EdgeInsets.all(16), child: Column(children: [ if (_isOffline) Container( padding: const EdgeInsets.all(12), margin: const EdgeInsets.only(bottom: 16), decoration: BoxDecoration( color: Colors.orange.shade50, borderRadius: BorderRadius.circular(8), ), child: const Row(children: [ Icon(Icons.info_outline, color: Colors.orange, size: 20), SizedBox(width: 8), Expanded(child: Text('当前离线,提交后将在网络恢复时自动发送', style: TextStyle(color: Colors.orange, fontSize: 13))), ]), ), Expanded( child: TextField( controller: _controller, maxLines: null, expands: true, textAlignVertical: TextAlignVertical.top, decoration: const InputDecoration( hintText: '请输入您的反馈...', border: OutlineInputBorder(), ), onChanged: (_) => _saveDraft(), // ✅ 实时保存草稿 ), ), const SizedBox(height: 16), SizedBox( width: double.infinity, child: ElevatedButton( onPressed: _isSubmitting ? null : _submit, child: _isSubmitting ? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2)) : Text(_isOffline ? '保存并稍后发送' : '提交'), ), ), ]), ), ); } }

6.3 离线支持架构模式

┌─────────────────────────────────────────────────────────┐ │ 离线优先架构 │ ├─────────────────────────────────────────────────────────┤ │ │ │ UI 层 │ │ ┌──────────────────────────────────────────────┐ │ │ │ 离线状态指示器 │ 数据新鲜度标签 │ 重试按钮 │ │ │ └──────────────────────────────────────────────┘ │ │ ↕ │ │ Repository 层(数据源协调) │ │ ┌──────────────────────────────────────────────┐ │ │ │ 1. 先读本地缓存(即时响应) │ │ │ │ 2. 后台请求远程数据 │ │ │ │ 3. 远程数据到达后更新本地缓存和 UI │ │ │ │ 4. 写操作:先写本地 → 加入同步队列 │ │ │ └──────────────────────────────────────────────┘ │ │ ↕ ↕ │ │ ┌──────────────┐ ┌──────────────────────┐ │ │ │ 本地存储 │ │ 同步队列 │ │ │ │ SQLite/MMKV │ │ 待上传操作列表 │ │ │ │ Hive/Drift │ │ 冲突解决策略 │ │ │ └──────────────┘ │ 指数退避重试 │ │ │ └──────────────────────┘ │ │ ↕ │ │ ┌──────────────────────────────────────────────┐ │ │ │ 网络层 │ │ │ │ 连接状态监听 → 在线时自动同步队列 │ │ │ └──────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────┘

6.4 离线支持工具推荐

工具平台价格用途
TanStack Query + AsyncStorageReact Native免费(开源)查询缓存持久化
WatermelonDBReact Native免费(开源)高性能离线数据库(SQLite)
MMKVReact Native免费(开源)高性能键值存储(替代 AsyncStorage)
DriftFlutter免费(开源)类型安全的 SQLite ORM
HiveFlutter免费(开源)轻量级 NoSQL 本地存储
PowerSyncFlutter/RN免费额度 / $49/月实时离线同步(基于 SQLite)
Core Data + CloudKitiOS免费Apple 原生离线同步方案
Room + WorkManagerAndroid免费Android 原生离线队列方案

7. 反模式五:平台不一致(Platform Inconsistency)

7.1 问题描述

跨平台开发中,AI 生成的代码往往只在一个平台上测试通过,在另一个平台上表现完全不同。这不仅包括视觉差异,还包括行为差异——同一段代码在 iOS 和 Android 上可能产生截然不同的结果。

常见的平台不一致问题:

  • 导航行为:iOS 支持边缘滑动返回,Android 使用系统返回键
  • 状态栏:iOS 默认浅色内容,Android 需要手动配置
  • 键盘行为:iOS 键盘弹出时自动推高内容,Android 需要手动处理
  • 日期/时间选择器:iOS 使用滚轮,Android 使用日历/时钟
  • 触觉反馈:iOS 有丰富的 Haptic Engine,Android 只有基础振动
  • 字体渲染:iOS 使用 San Francisco,Android 使用 Roboto
  • 阴影效果:iOS 支持 shadowColor/Offset/Radius,Android 使用 elevation
  • 安全区域:iOS 有 Dynamic Island/刘海,Android 有各种异形屏

7.2 AI 生成的问题代码 vs 正确代码

React Native:忽略平台差异

// ❌ AI 常见错误:不处理平台差异 import { View, Text, StyleSheet, TextInput, TouchableOpacity } from 'react-native'; function LoginScreen() { return ( <View style={styles.container}> {/* 问题 1:没有处理状态栏 */} <Text style={styles.title}>登录</Text> {/* 问题 2:阴影在 Android 上不生效 */} <View style={styles.card}> <TextInput style={styles.input} placeholder="邮箱" // 问题 3:键盘类型没有平台适配 keyboardType="email-address" // 问题 4:自动大写在 Android 上行为不同 autoCapitalize="none" /> <TextInput style={styles.input} placeholder="密码" secureTextEntry /> {/* 问题 5:按钮样式在两个平台上看起来不同 */} <TouchableOpacity style={styles.button}> <Text style={styles.buttonText}>登录</Text> </TouchableOpacity> </View> </View> ); } const styles = StyleSheet.create({ container: { flex: 1, padding: 20, backgroundColor: '#f5f5f5' }, title: { fontSize: 28, fontWeight: 'bold', marginBottom: 20 }, card: { backgroundColor: '#fff', borderRadius: 12, padding: 20, // ❌ 这些阴影属性只在 iOS 上生效 shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.1, shadowRadius: 8, // ❌ Android 需要 elevation,但效果和 iOS 阴影不同 }, input: { borderWidth: 1, borderColor: '#ddd', borderRadius: 8, padding: 12, marginBottom: 12, fontSize: 16, // ❌ 没有处理 Android TextInput 的下划线 }, button: { backgroundColor: '#007AFF', // ❌ 这是 iOS 蓝色,Android 应该用 Material 色 borderRadius: 8, padding: 14, alignItems: 'center', }, buttonText: { color: '#fff', fontSize: 16, fontWeight: '600' }, });
// ✅ 正确写法:完整的平台适配 import { View, Text, StyleSheet, TextInput, TouchableOpacity, Platform, StatusBar, KeyboardAvoidingView, } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import * as Haptics from 'expo-haptics'; function LoginScreen() { const insets = useSafeAreaInsets(); const handleLogin = async () => { // ✅ 平台适配的触觉反馈 if (Platform.OS === 'ios') { await Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium); } // Android 触觉反馈通过 TouchableOpacity 的 android_ripple 实现 // ... 登录逻辑 }; return ( <> {/* ✅ 平台适配的状态栏 */} <StatusBar barStyle="dark-content" backgroundColor={Platform.OS === 'android' ? '#f5f5f5' : undefined} translucent={Platform.OS === 'android'} /> {/* ✅ 键盘避让:iOS 用 padding,Android 用 height */} <KeyboardAvoidingView style={{ flex: 1 }} behavior={Platform.OS === 'ios' ? 'padding' : 'height'} keyboardVerticalOffset={Platform.OS === 'ios' ? 0 : StatusBar.currentHeight || 0} > <View style={[ styles.container, { paddingTop: insets.top + 20 } // ✅ 安全区域适配 ]}> <Text style={styles.title}>登录</Text> <View style={styles.card}> <TextInput style={[ styles.input, // ✅ Android 移除默认下划线 Platform.OS === 'android' && { underlineColorAndroid: 'transparent' }, ]} placeholder="邮箱" keyboardType="email-address" autoCapitalize="none" autoCorrect={false} textContentType="emailAddress" // ✅ iOS 自动填充 autoComplete="email" // ✅ Android 自动填充 /> <TextInput style={[ styles.input, Platform.OS === 'android' && { underlineColorAndroid: 'transparent' }, ]} placeholder="密码" secureTextEntry textContentType="password" // ✅ iOS 自动填充 autoComplete="password" // ✅ Android 自动填充 /> <TouchableOpacity style={styles.button} onPress={handleLogin} activeOpacity={0.8} // ✅ Android Material 水波纹效果 {...(Platform.OS === 'android' && { android_ripple: { color: 'rgba(255,255,255,0.3)' }, })} > <Text style={styles.buttonText}>登录</Text> </TouchableOpacity> </View> </View> </KeyboardAvoidingView> </> ); } const styles = StyleSheet.create({ container: { flex: 1, padding: 20, backgroundColor: '#f5f5f5' }, title: { fontSize: 28, // ✅ 平台适配的字重 fontWeight: Platform.select({ ios: '700', android: 'bold' }), marginBottom: 20, color: '#1a1a1a', }, card: { backgroundColor: '#fff', borderRadius: 12, padding: 20, // ✅ 跨平台阴影 ...Platform.select({ ios: { shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.1, shadowRadius: 8, }, android: { elevation: 4, }, }), }, input: { borderWidth: 1, borderColor: '#ddd', borderRadius: 8, padding: Platform.select({ ios: 14, android: 12 }), // ✅ 平台适配的内边距 marginBottom: 12, fontSize: 16, color: '#1a1a1a', }, button: { // ✅ 平台适配的主色 backgroundColor: Platform.select({ ios: '#007AFF', // iOS 系统蓝 android: '#6200EE', // Material 主色 }), borderRadius: Platform.select({ ios: 10, android: 8 }), padding: 14, alignItems: 'center', marginTop: 4, }, buttonText: { color: '#fff', fontSize: 16, fontWeight: '600' }, });

Flutter:忽略 Cupertino vs Material 差异

// ❌ AI 常见错误:在 iOS 上使用 Material 组件 class SettingsScreen extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('设置')), // ❌ iOS 上应该用 CupertinoNavigationBar body: ListView( children: [ // ❌ iOS 上应该用 CupertinoSwitch SwitchListTile( title: Text('推送通知'), value: true, onChanged: (v) {}, ), // ❌ iOS 上日期选择器应该用滚轮样式 ListTile( title: Text('生日'), onTap: () { showDatePicker( // ❌ 这是 Material 日历样式 context: context, initialDate: DateTime.now(), firstDate: DateTime(1900), lastDate: DateTime.now(), ); }, ), ], ), ); } }
// ✅ 正确写法:平台自适应组件 import 'dart:io'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; class SettingsScreen extends StatelessWidget { const SettingsScreen({super.key}); @override Widget build(BuildContext context) { // ✅ 根据平台选择不同的 Scaffold if (Platform.isIOS) { return CupertinoPageScaffold( navigationBar: const CupertinoNavigationBar( middle: Text('设置'), ), child: _buildContent(context), ); } return Scaffold( appBar: AppBar(title: const Text('设置')), body: _buildContent(context), ); } Widget _buildContent(BuildContext context) { return ListView( children: [ // ✅ 平台自适应开关 _AdaptiveSwitch( title: '推送通知', value: true, onChanged: (v) {}, ), // ✅ 平台自适应日期选择 ListTile( title: const Text('生日'), trailing: const Icon(Icons.chevron_right), onTap: () => _showAdaptiveDatePicker(context), ), ], ); } void _showAdaptiveDatePicker(BuildContext context) { if (Platform.isIOS) { // ✅ iOS:底部弹出滚轮选择器 showCupertinoModalPopup( context: context, builder: (context) => Container( height: 300, color: CupertinoColors.systemBackground.resolveFrom(context), child: Column(children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ CupertinoButton(child: const Text('取消'), onPressed: () => Navigator.pop(context)), CupertinoButton(child: const Text('确定'), onPressed: () => Navigator.pop(context)), ], ), Expanded( child: CupertinoDatePicker( mode: CupertinoDatePickerMode.date, onDateTimeChanged: (date) {}, ), ), ]), ), ); } else { // ✅ Android:Material 日历选择器 showDatePicker( context: context, initialDate: DateTime.now(), firstDate: DateTime(1900), lastDate: DateTime.now(), ); } } } // ✅ 平台自适应开关组件 class _AdaptiveSwitch extends StatelessWidget { final String title; final bool value; final ValueChanged<bool> onChanged; const _AdaptiveSwitch({ required this.title, required this.value, required this.onChanged, }); @override Widget build(BuildContext context) { return ListTile( title: Text(title), trailing: Platform.isIOS ? CupertinoSwitch(value: value, onChanged: onChanged) : Switch(value: value, onChanged: onChanged), ); } }

7.3 平台差异速查表

功能iOS 实现Android 实现React Native 适配Flutter 适配
返回导航边缘滑动系统返回键gestureEnabledWillPopScope
状态栏自动适配需设置 translucentStatusBar 组件SystemChrome
键盘避让自动推高windowSoftInputModeKeyboardAvoidingViewScaffold.resizeToAvoidBottomInset
阴影shadow* 属性elevationPlatform.selectBoxShadow / elevation
触觉反馈Haptic EngineVibrationexpo-hapticsHapticFeedback
安全存储KeychainEncryptedSharedPreferencesexpo-secure-storeflutter_secure_storage
推送通知APNsFCMexpo-notificationsfirebase_messaging
深色模式系统级系统级useColorScheme()MediaQuery.platformBrightnessOf
字体San FranciscoRoboto系统默认系统默认
最小触摸目标44x44pt48x48dp手动设置Material 自动处理

7.4 提示词模板:平台一致性检查

请检查以下跨平台移动端代码的平台一致性: 框架:[React Native / Flutter] [粘贴代码] 检查清单: 1. 阴影效果是否在 iOS 和 Android 上都正确显示? 2. 键盘弹出时内容是否在两个平台上都正确避让? 3. 状态栏样式是否在两个平台上都正确? 4. 导航行为(返回手势/返回键)是否在两个平台上都正确? 5. 日期/时间选择器是否使用了平台原生样式? 6. 触觉反馈是否在两个平台上都有实现? 7. 安全区域(刘海屏、Dynamic Island)是否正确处理? 8. 字体渲染和间距是否在两个平台上视觉一致? 9. 组件是否遵循各自平台的设计规范(iOS HIG / Material Design)? 对于发现的每个不一致,请提供平台适配的修复代码。

8. 反模式六:包体积过大(Oversized App Bundle)

8.1 问题描述

包体积直接影响用户的下载意愿和留存率。研究表明,应用体积每增加 6MB,安装转化率就会下降约 1%。AI 生成的代码特别容易导致包体积膨胀,因为 AI 倾向于引入功能丰富但体积庞大的第三方库,而不是使用轻量级替代方案或原生 API。

常见的包体积问题:

  • 引入全量工具库:如 lodash 全量导入(~70KB gzipped)而非按需导入
  • 未压缩的图片资源:高分辨率 PNG/JPEG 未经压缩直接打包
  • 未使用的依赖:安装了但从未使用的 npm/pub 包
  • 重复的功能库:同时引入 moment.js 和 date-fns,或 axios 和 fetch
  • 未启用代码压缩:未配置 ProGuard/R8(Android)或未启用 Bitcode(iOS)
  • 字体文件过大:引入完整字体族而非仅需要的字重
  • 未使用 App Bundle:Android 使用 APK 而非 AAB 格式

8.2 AI 生成的问题代码 vs 正确代码

React Native:依赖膨胀

// ❌ AI 常见错误:引入不必要的重量级依赖 import _ from 'lodash'; // ❌ 全量导入 lodash(~70KB gzipped) import moment from 'moment'; // ❌ moment.js(~67KB gzipped,已废弃) import axios from 'axios'; // ❌ 有原生 fetch,不需要 axios import { v4 as uuidv4 } from 'uuid'; // ❌ 可以用 crypto.randomUUID() import numeral from 'numeral'; // ❌ 可以用 Intl.NumberFormat function ProductCard({ product }) { // 使用 lodash 只是为了一个简单的格式化 const formattedPrice = _.padStart(product.price.toFixed(2), 8, ' '); // 使用 moment 只是为了格式化日期 const formattedDate = moment(product.createdAt).format('YYYY-MM-DD'); // 使用 numeral 只是为了格式化数字 const formattedSales = numeral(product.sales).format('0,0'); // 使用 uuid 生成唯一 ID const trackingId = uuidv4(); return (/* ... */); }
// ✅ 正确写法:使用原生 API 和轻量级替代 // ✅ 不需要 lodash —— 使用原生方法 // ✅ 不需要 moment —— 使用 Intl.DateTimeFormat // ✅ 不需要 axios —— 使用原生 fetch // ✅ 不需要 uuid —— 使用 crypto.randomUUID() // ✅ 不需要 numeral —— 使用 Intl.NumberFormat function ProductCard({ product }) { // ✅ 原生字符串方法 const formattedPrice = product.price.toFixed(2).padStart(8, ' '); // ✅ Intl.DateTimeFormat(零依赖) const formattedDate = new Intl.DateTimeFormat('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', }).format(new Date(product.createdAt)); // ✅ Intl.NumberFormat(零依赖) const formattedSales = new Intl.NumberFormat('zh-CN').format(product.sales); // ✅ crypto.randomUUID()(React Native 0.72+ 支持) const trackingId = crypto.randomUUID(); return (/* ... */); } // ✅ 如果确实需要 lodash 的某个函数,按需导入 import debounce from 'lodash/debounce'; // ~1KB vs ~70KB // 或使用更轻量的替代 import { debounce } from 'lodash-es'; // 支持 tree-shaking

Flutter:资源未优化

// ❌ AI 常见错误:未优化的资源加载 class ProfileScreen extends StatelessWidget { @override Widget build(BuildContext context) { return Column( children: [ // 问题 1:直接加载原始大图(可能 5MB+) Image.asset('assets/images/background.png'), // ❌ 未压缩的 PNG // 问题 2:网络图片没有缓存和尺寸限制 Image.network(user.avatarUrl), // ❌ 可能加载 4000x4000 的原图 // 问题 3:使用完整的图标字体 Icon(Icons.home), // Material Icons 字体文件 ~1.5MB ], ); } } // pubspec.yaml // ❌ 引入了大量未使用的依赖 // dependencies: // http: ^1.2.0 // dio: ^5.4.0 # ❌ 和 http 功能重复 // intl: ^0.19.0 // google_fonts: ^6.1.0 # ❌ 运行时下载字体,增加网络开销 // flutter_svg: ^2.0.0 // lottie: ^3.0.0 # ❌ 如果只用了一个动画,不值得引入
// ✅ 正确写法:优化资源和依赖 class ProfileScreen extends StatelessWidget { const ProfileScreen({super.key}); @override Widget build(BuildContext context) { return Column( children: [ // ✅ 使用 WebP 格式(比 PNG 小 25-35%) Image.asset( 'assets/images/background.webp', // ✅ 限制解码尺寸,减少内存占用 cacheWidth: MediaQuery.sizeOf(context).width.toInt(), ), // ✅ 使用 cached_network_image 缓存 + 限制尺寸 CachedNetworkImage( imageUrl: user.avatarUrl, // ✅ 请求服务端缩略图而非原图 // imageUrl: '${user.avatarUrl}?w=200&h=200&fit=crop', memCacheWidth: 200, // ✅ 内存缓存尺寸限制 memCacheHeight: 200, placeholder: (_, __) => const CircleAvatar( backgroundColor: Colors.grey, child: Icon(Icons.person), ), errorWidget: (_, __, ___) => const CircleAvatar( backgroundColor: Colors.grey, child: Icon(Icons.error), ), ), ], ); } } // ✅ pubspec.yaml 优化 // flutter: // uses-material-design: false # ✅ 如果不用 Material Icons,关闭可省 ~1.5MB // assets: // - assets/images/ # ✅ 只包含需要的资源 // // dependencies: // http: ^1.2.0 # ✅ 只保留一个 HTTP 库 // cached_network_image: ^3.3.0 # ✅ 图片缓存 // flutter_svg: ^2.0.0 # ✅ 仅在确实需要 SVG 时引入

8.3 包体积优化检查清单

优化项预期节省适用平台操作方法
启用 ProGuard/R8APK 减少 20-30%AndroidminifyEnabled true in build.gradle
使用 App Bundle (.aab)下载减少 15-20%Androidflutter build appbundle / EAS Build
图片转 WebP图片减少 25-35%全平台cwebp 工具批量转换
移除未使用依赖视情况全平台depcheck / dart pub outdated
按需导入 lodash~69KBReact Nativeimport debounce from 'lodash/debounce'
替换 moment.js~67KBReact Native使用 date-fnsIntl API
字体子集化字体减少 80-90%全平台fonttools / pyftsubset
启用 HermesJS bundle 减少 50%+React NativeExpo SDK 53 默认启用
Tree Shaking视情况Flutterflutter build --release(自动)
延迟加载首屏减少 30-50%全平台代码分割 + 懒加载

8.4 包体积分析工具

工具平台价格用途
react-native-bundle-visualizerReact Native免费(开源)JS bundle 可视化分析
BundlephobiaReact Native免费依赖体积在线查询
source-map-explorerReact Native免费(开源)Source map 分析
flutter build —analyze-sizeFlutter免费Flutter 官方体积分析
Xcode Build ReportiOS免费iOS 包体积详细报告
Android Studio APK AnalyzerAndroid免费APK/AAB 内容分析
Emerge ToolsiOS/Android免费额度 / $500/月CI 集成的体积监控
depcheckReact Native免费(开源)检测未使用的 npm 依赖

8.5 提示词模板:包体积优化

请分析以下移动端项目的包体积优化机会: 框架:[React Native / Flutter / iOS / Android] 当前包体积:[XX MB] 目标包体积:[XX MB] package.json / pubspec.yaml 内容: [粘贴依赖列表] 请检查: 1. 是否有可以用原生 API 替代的第三方库? 2. 是否有功能重复的依赖? 3. 是否有未使用的依赖? 4. 图片资源是否已压缩?是否可以转为 WebP? 5. 字体文件是否已子集化? 6. 是否启用了代码压缩(ProGuard/R8/Hermes)? 7. 是否使用了 App Bundle 而非 APK? 8. 是否有可以延迟加载的模块? 对于每个优化建议,请提供: - 预期节省的体积 - 具体的操作步骤 - 是否有功能影响

9. 反模式七:生命周期管理缺失(Missing Lifecycle Management)

9.1 问题描述

移动端应用有复杂的生命周期——前台、后台、挂起、终止、低内存警告等状态切换。AI 生成的代码通常只考虑”应用在前台正常运行”的场景,忽略了后台切换、低内存、屏幕旋转等关键生命周期事件。

9.2 AI 生成的问题代码 vs 正确代码

React Native:不处理应用生命周期

// ❌ AI 常见错误:不处理应用前后台切换 function VideoPlayerScreen({ videoUrl }) { const videoRef = useRef(null); const [isPlaying, setIsPlaying] = useState(true); useEffect(() => { // 问题 1:应用进入后台时视频继续播放(浪费电池,可能违反 App Store 规则) // 问题 2:应用恢复前台时不刷新数据 // 问题 3:低内存时不释放缓存 // 问题 4:WebSocket 连接在后台不断开 }, []); return <Video ref={videoRef} source={{ uri: videoUrl }} />; }
// ✅ 正确写法:完整的生命周期管理 import { useEffect, useRef, useState, useCallback } from 'react'; import { AppState, Platform } from 'react-native'; import { Video, ResizeMode } from 'expo-av'; import { useIsFocused } from '@react-navigation/native'; function VideoPlayerScreen({ videoUrl }) { const videoRef = useRef<Video>(null); const [isPlaying, setIsPlaying] = useState(true); const appState = useRef(AppState.currentState); const wasPlayingBeforeBackground = useRef(false); const isFocused = useIsFocused(); // ✅ 导航焦点状态 // ✅ 应用前后台切换处理 useEffect(() => { const subscription = AppState.addEventListener('change', (nextState) => { if (appState.current === 'active' && nextState.match(/inactive|background/)) { // 进入后台:暂停视频 wasPlayingBeforeBackground.current = isPlaying; videoRef.current?.pauseAsync(); setIsPlaying(false); } else if ( appState.current.match(/inactive|background/) && nextState === 'active' ) { // 恢复前台:如果之前在播放,则继续 if (wasPlayingBeforeBackground.current) { videoRef.current?.playAsync(); setIsPlaying(true); } } appState.current = nextState; }); return () => subscription.remove(); }, [isPlaying]); // ✅ 导航离开时暂停 useEffect(() => { if (!isFocused) { videoRef.current?.pauseAsync(); } }, [isFocused]); // ✅ 组件卸载时释放资源 useEffect(() => { return () => { videoRef.current?.unloadAsync(); // ✅ 释放视频资源 }; }, []); return ( <Video ref={videoRef} source={{ uri: videoUrl }} resizeMode={ResizeMode.CONTAIN} shouldPlay={isPlaying && isFocused} // ✅ 处理播放错误 onError={(error) => { console.error('Video playback error:', error); setIsPlaying(false); }} /> ); }

Jetpack Compose:不处理配置变更

// ❌ AI 常见错误:屏幕旋转时状态丢失 @Composable fun FormScreen() { // 问题:这些状态在配置变更(屏幕旋转)时会丢失 var name by remember { mutableStateOf("") } var email by remember { mutableStateOf("") } var selectedDate by remember { mutableStateOf<LocalDate?>(null) } var formStep by remember { mutableIntStateOf(0) } // ❌ 用户填了一半表单,旋转屏幕后全部清空 Column { if (formStep == 0) { TextField(value = name, onValueChange = { name = it }, label = { Text("姓名") }) TextField(value = email, onValueChange = { email = it }, label = { Text("邮箱") }) } else { // 第二步... } } }
// ✅ 正确写法:使用 ViewModel 保存状态 class FormViewModel : ViewModel() { // ✅ ViewModel 在配置变更时存活 var name by mutableStateOf("") var email by mutableStateOf("") var selectedDate by mutableStateOf<LocalDate?>(null) var formStep by mutableIntStateOf(0) // ✅ 使用 SavedStateHandle 在进程被杀死后恢复 // constructor(savedStateHandle: SavedStateHandle) : this() { // name = savedStateHandle["name"] ?: "" // email = savedStateHandle["email"] ?: "" // } } @Composable fun FormScreen(viewModel: FormViewModel = viewModel()) { // ✅ 状态在屏幕旋转时保持 Column { if (viewModel.formStep == 0) { TextField( value = viewModel.name, onValueChange = { viewModel.name = it }, label = { Text("姓名") } ) TextField( value = viewModel.email, onValueChange = { viewModel.email = it }, label = { Text("邮箱") } ) } else { // 第二步... } } // ✅ 处理返回键:未保存的表单需确认 BackHandler(enabled = viewModel.name.isNotEmpty() || viewModel.email.isNotEmpty()) { // 显示"放弃编辑?"确认对话框 } }

10. 反模式八:安全存储缺失(Insecure Data Storage)

10.1 问题描述

AI 生成的移动端代码经常将敏感数据存储在不安全的位置——使用 AsyncStorage/SharedPreferences 存储 token、在日志中输出用户信息、在代码中硬编码 API 密钥。移动端的安全存储比 Web 更复杂,因为应用数据可能被越狱/root 设备访问。

10.2 AI 生成的问题代码 vs 正确代码

React Native:不安全的数据存储

// ❌ AI 常见错误:敏感数据存储在不安全的位置 import AsyncStorage from '@react-native-async-storage/async-storage'; async function login(email: string, password: string) { const response = await fetch('https://api.example.com/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email, password }), }); const data = await response.json(); // ❌ Token 存储在 AsyncStorage(未加密,可被其他应用读取) await AsyncStorage.setItem('auth_token', data.token); await AsyncStorage.setItem('refresh_token', data.refreshToken); // ❌ 用户密码存储在本地(绝对不应该) await AsyncStorage.setItem('user_password', password); // ❌ 在日志中输出 token console.log('Login successful, token:', data.token); } // ❌ API 密钥硬编码在代码中 const API_KEY = 'sk-1234567890abcdef'; const STRIPE_KEY = 'pk_live_xxxxxxxxxxxxx';
// ✅ 正确写法:使用安全存储 import * as SecureStore from 'expo-secure-store'; // ✅ API 密钥通过环境变量注入 // 在 app.config.ts 或 .env 中配置,不提交到代码仓库 const API_BASE_URL = process.env.EXPO_PUBLIC_API_URL; async function login(email: string, password: string) { const response = await fetch(`${API_BASE_URL}/login`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email, password }), }); if (!response.ok) { throw new Error(`Login failed: ${response.status}`); } const data = await response.json(); // ✅ Token 存储在安全存储(iOS Keychain / Android EncryptedSharedPreferences) await SecureStore.setItemAsync('auth_token', data.token, { keychainAccessible: SecureStore.WHEN_UNLOCKED, // ✅ 仅在设备解锁时可访问 }); await SecureStore.setItemAsync('refresh_token', data.refreshToken, { keychainAccessible: SecureStore.WHEN_UNLOCKED, }); // ✅ 绝不存储密码 // ✅ 绝不在日志中输出敏感信息 if (__DEV__) { console.log('Login successful'); // ✅ 仅在开发环境输出,且不包含 token } } // ✅ 安全的 token 读取 async function getAuthToken(): Promise<string | null> { try { return await SecureStore.getItemAsync('auth_token'); } catch { return null; // ✅ 安全存储不可用时优雅降级 } } // ✅ 安全的登出 async function logout() { await SecureStore.deleteItemAsync('auth_token'); await SecureStore.deleteItemAsync('refresh_token'); }

10.3 安全存储对比

存储方式加密安全级别适用数据平台
AsyncStorage / SharedPreferences❌ 无🔴 低非敏感设置、缓存全平台
MMKV❌ 默认无🟡 中非敏感高频读写数据全平台
expo-secure-store✅ 系统级🟢 高Token、密码、API keyReact Native
flutter_secure_storage✅ 系统级🟢 高Token、密码、API keyFlutter
iOS Keychain✅ 硬件级🟢 最高凭证、证书iOS
Android EncryptedSharedPreferences✅ AES-256🟢 高凭证、敏感配置Android
Android Keystore✅ 硬件级🟢 最高加密密钥、证书Android

实战案例:从”AI 生成的 MVP”到”生产级移动应用”

案例背景

一个独立开发者使用 Claude Code + Expo 在 3 天内生成了一个外卖配送追踪应用的 MVP。应用在模拟器上运行完美,但上线后收到大量用户投诉:

  • ⭐ 1.2 分(App Store 评分)
  • 投诉 1:“用了 2 小时手机就没电了”(电池消耗)
  • 投诉 2:“地铁里打开全是白屏”(离线支持差)
  • 投诉 3:“Android 上按钮点不了”(平台不一致)
  • 投诉 4:“用着用着就闪退”(内存泄漏)
  • 投诉 5:“下载要 180MB 太大了”(包体积过大)
  • 投诉 6:“第一次打开就要 6 个权限,吓人”(权限处理差)

问题诊断

开发者使用以下 Steering 规则 Prompt 让 AI 诊断问题:

请作为移动端性能专家,审查这个 Expo + React Native 外卖配送追踪应用。 用户反馈了以下问题:电池消耗快、离线白屏、Android 兼容性差、频繁闪退、包体积大、权限请求体验差。 请逐一分析可能的根因,并按严重程度排序。 项目技术栈: - Expo SDK 53 + React Native 0.79 - React Navigation v7 - 状态管理:useState + useContext(无外部库) - 网络请求:axios - 定位:expo-location - 地图:react-native-maps - 存储:AsyncStorage 请检查以下文件:[列出关键文件]

诊断结果与修复

问题根因严重程度修复方案修复耗时
电池消耗GPS 每秒更新 + 5 秒轮询订单🔴 致命降低定位频率到 50m/10s,轮询改为 30s + WebSocket2 小时
离线白屏无缓存,所有数据依赖网络🔴 致命引入 TanStack Query + AsyncStorage 持久化4 小时
Android 兼容阴影用 iOS API,键盘不避让🟡 严重Platform.select 适配 + KeyboardAvoidingView3 小时
闪退5 个 useEffect 无清理函数🔴 致命添加清理函数 + LeakCanary 检测2 小时
包体积lodash 全量 + 未压缩图片 + moment.js🟡 严重按需导入 + WebP + date-fns1 小时
权限体验启动时一次请求 6 个权限🟡 严重改为 just-in-time 按需请求 + 说明弹窗2 小时

修复后的 Steering 规则

开发者在修复后创建了以下 CLAUDE.md 规则,防止 AI 再次生成同类问题:

# CLAUDE.md — 外卖配送追踪应用 ## 🚫 绝对禁止(上次上线踩过的坑) 1. 禁止 GPS accuracy 设置为 BestForNavigation(使用 Balanced) 2. 禁止轮询间隔 < 30 秒(使用 WebSocket 替代高频轮询) 3. 禁止 useEffect 没有 return 清理函数 4. 禁止 import _ from 'lodash'(按需导入) 5. 禁止 import moment(使用 date-fns 或 Intl) 6. 禁止在启动时请求权限(按需请求) 7. 禁止 AsyncStorage 存储 token(使用 expo-secure-store) 8. 禁止不处理离线状态(所有 API 调用必须有离线降级) ## ⚠️ 每次生成代码前检查 - 是否处理了 iOS 和 Android 的差异? - 是否有离线降级方案? - 是否有清理函数? - 新依赖的体积是多少?有没有更轻量的替代?

修复效果

指标修复前修复后改善
App Store 评分⭐ 1.2⭐ 4.3+3.1
电池消耗(2 小时使用)45%12%-73%
离线可用性0%(白屏)85%(缓存数据)+85%
Android 崩溃率8.5%0.3%-96%
包体积(iOS)180MB52MB-71%
冷启动时间4.2s1.8s-57%

案例分析

这个案例的核心教训:

  1. AI 生成的 MVP 不等于生产级应用:AI 擅长快速生成功能代码,但缺乏移动端特有的性能、电池、离线等非功能性考量
  2. Steering 规则是”踩坑记录”:最好的 Steering 规则来自实际踩过的坑,每次修复 bug 后都应该更新规则
  3. 移动端的”隐性需求”比 Web 多得多:电池、权限、离线、平台差异、包体积——这些在 Web 上不存在或不重要的问题,在移动端是核心体验
  4. 诊断工具不可或缺:没有 Instruments、Android Profiler、LeakCanary 等工具,很多问题在开发阶段根本发现不了

避坑指南

❌ 常见错误

  1. useEffect 没有清理函数

    • 问题:订阅、定时器、监听器在组件卸载后继续运行,导致内存泄漏和意外行为
    • 正确做法:每个 useEffect 都必须返回清理函数,取消所有副作用
  2. GPS 定位精度设置过高

    • 问题:BestForNavigation 精度会持续使用 GPS 硬件,极度耗电
    • 正确做法:大多数场景使用 Balanced(~100m)精度即可,仅导航场景使用高精度
  3. 不检查权限直接调用 API

    • 问题:相机、定位等 API 在无权限时崩溃或返回空数据
    • 正确做法:先检查权限状态,显示说明弹窗,处理拒绝和永久拒绝
  4. 完全依赖网络,无离线策略

    • 问题:弱网/离线时应用白屏或显示无意义的错误
    • 正确做法:本地缓存关键数据,离线队列保存写操作,网络恢复后自动同步
  5. 忽略 iOS 和 Android 的差异

    • 问题:阴影、键盘、导航、触觉反馈等在两个平台上行为不同
    • 正确做法:使用 Platform.select() 处理差异,分别测试两个平台
  6. 引入全量工具库

    • 问题:lodash(~70KB)、moment.js(~67KB)等大幅增加包体积
    • 正确做法:按需导入或使用原生 API(Intl、Array 方法等)
  7. 敏感数据存储在 AsyncStorage

    • 问题:AsyncStorage 未加密,越狱/root 设备可直接读取
    • 正确做法:Token 和密码使用 expo-secure-store / flutter_secure_storage
  8. 不处理应用生命周期

    • 问题:后台时继续播放视频/轮询/定位,浪费电池和流量
    • 正确做法:监听 AppState 变化,后台时暂停非必要操作

✅ 最佳实践

  1. 先写 Steering 规则,再让 AI 生成代码:在项目开始前就创建移动端专用的 CLAUDE.md / Kiro Steering 规则
  2. 每次修复 bug 后更新 Steering 规则:把踩过的坑转化为规则,防止 AI 再犯
  3. 在真机上测试,不只是模拟器:内存泄漏、电池消耗、性能问题在模拟器上往往不明显
  4. 使用平台原生的性能分析工具:Xcode Instruments、Android Profiler、Flipper
  5. 包体积预算制:设定包体积上限(如 50MB),每次添加依赖前检查影响
  6. 离线优先设计:假设网络不可用,先设计离线体验,再添加在线增强
  7. 权限按需请求:在用户即将使用功能时才请求权限,并提供清晰的理由说明
  8. 自动化质量门:CI/CD 中集成包体积检查、lint 规则、性能基准测试

相关资源与延伸阅读

工具与框架

  1. LeakCanary  — Android 内存泄漏自动检测库,Square 开源,集成简单,生产环境必备
  2. Flipper  — Meta 开源的移动端调试平台,支持 React Native/iOS/Android 的内存、网络、布局调试
  3. TanStack Query  — 服务端状态管理库,内置缓存持久化、离线支持、自动重试,React Native 离线优先架构的核心
  4. WatermelonDB  — React Native 高性能离线数据库,基于 SQLite,支持懒加载和同步
  5. Emerge Tools  — iOS/Android 包体积分析和监控平台,CI 集成,自动检测体积回归

社区与教程

  1. React Native Performance  — React Native 官方性能优化指南,覆盖 JS 线程、原生线程、内存管理
  2. Flutter Performance Best Practices  — Flutter 官方性能最佳实践,覆盖 Widget 重建、着色器编译、内存管理
  3. Apple Human Interface Guidelines - iOS  — Apple 官方 iOS 设计规范,权限请求、导航模式、无障碍的权威参考
  4. Material Design 3  — Google Material Design 3 规范,Android 组件、动态颜色、自适应布局的权威参考
  5. Awesome React Native  — React Native 社区精选资源列表,包含性能优化、调试工具、最佳实践

参考来源


📖 返回 总览与导航 | 上一节:AI辅助移动端UI | 下一节:AI辅助游戏开发概览

Last updated on