引言
如果你正在开发同时面向 Android 和 iOS 的 Flutter 应用,且需要满足不同的支付需求:
- Android 支持微信/支付宝等第三方支付
- iOS 仅允许使用 IAP(内购)
那么你肯定关心这两个核心问题:
- iOS 包中绝对不能包含任何第三方支付的代码或资源
- Android 包中也不应混入 iOS 的实现。
有几种方案可以实现:
- 打包时使用shell脚本复制对应平台代码和资源,难以维护。
- 将类似支付代码在原生实现,增加开发量。
- 使用 Flutter Tree Shaking能力实现,推荐。
本文将通过一个完整可运行的示例工程,为你详细讲解:
- Tree Shaking 原理及其如何实现代码"瘦身"
- 如何使用多入口设计实现平台代码的物理隔离
- 如何按平台隔离资源文件(图片、图标等)
- 打包后如何一键验证确只包含所需内容
遵循本文方案,你既能通过应用商店审核,又能优化应用体积。
一、Tree Shaking:像收拾行李一样简单
Tree Shaking 的工作原理类似于出门前收拾行李:
-
编译器从入口
main()
开始,沿着 import 链找到所有"真正需要"的代码,打包进最终产物 - 未被入口引用的代码,不会被包含在内
- Flutter 的 release 构建使用 Dart AOT(Ahead-of-Time)编译,产物通常位于:
- Android:
lib/**/libapp.so
- iOS: 应用二进制文件中
- Android:
因此,核心策略非常明确:
- 入口分叉(两个 main 文件)
- 代码路径分离(各自 import 专属平台目录 + 共享抽象层)
- Tree Shaking 只会包含从入口可达的代码路径
二、项目结构设计(可直接复用)
关键目录结构如下:
lib/
main_android.dart # Android 专属入口
main_ios.dart # iOS 专属入口
android/ # Android 专属 UI/组件
ios/ # iOS 专属 UI/组件
packages/
shared/ # 共享包(纯 Dart:抽象、DTO、用例)
lib/ai_chat_shared.dart
lib/src/...
assets/
android/ # Android 平台资源(icons/images/...)
ios/ # iOS 平台资源(icons/images/...)
shared/ # 共享资源(两端都会打包)
current/ # 构建前复制为"当前平台"资源
scripts/
build_android.sh # 复制资源 + 指定入口 + 构建
build_ios.sh
verify_isolation.sh # 打包后自动验证内容
入口文件设计(核心原则:各走各的路):
// lib/main_android.dart
import 'package:flutter/material.dart';
import 'android/home_android.dart'; // 只导入Android实现
void main() => runApp(const AndroidApp());
class AndroidApp extends StatelessWidget {
const AndroidApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'AI Chat - Android',
theme: ThemeData(colorSchemeSeed: Colors.green, useMaterial3: true),
home: const HomeAndroid(), // 使用Android专属页面
);
}
}
// lib/main_ios.dart
import 'package:flutter/material.dart';
import 'ios/home_ios.dart'; // 只导入iOS实现
void main() => runApp(const IOSApp());
class IOSApp extends StatelessWidget {
const IOSApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'AI Chat - iOS',
theme: ThemeData(colorSchemeSeed: Colors.blue, useMaterial3: true),
home: const HomeIOS(), // 使用iOS专属页面
);
}
}
三、共享包设计:只暴露抽象,隐藏实现
共享包 packages/shared
设计原则:
- 只导出接口与模型,隐藏实现细节
- 实现代码放在 src 目录,不直接 export
// packages/shared/lib/ai_chat_shared.dart
library ai_chat_shared;
// 只暴露抽象接口和数据模型
export 'src/models/message.dart';
export 'src/services/chat_service.dart'; // 抽象类,非具体实现
// packages/shared/lib/src/services/chat_service.dart
import '../models/message.dart';
// 抽象服务定义
abstract class ChatService {
Future<Message> sendMessage(String content);
}
// 示例实现(实际项目中各平台有自己的实现)
class MockChatService implements ChatService {
@override
Future<Message> sendMessage(String content) async => Message(
id: DateTime.now().millisecondsSinceEpoch.toString(),
content: 'AI: $content',
timestamp: DateTime.now(),
isUser: false,
);
}
这种设计的好处:
- 共享层不依赖任何平台 SDK,保持纯净
- 各平台在自己的目录中提供具体实现
- 通过入口文件注入平台专属的实现
四、资源隔离:确保只包含当前平台资源
pubspec.yaml 中只声明这两个目录:
flutter:
uses-material-design: true
assets:
- assets/current/ # 构建时动态填充
- assets/current/icons/
- assets/current/images/
- assets/shared/ # 共享资源
- assets/shared/icons/
- assets/shared/images/
构建脚本负责资源复制:
#!/bin/bash
echo "=== 构建Android应用 ==="
# 使用根目录现有 android/ 构建,并将隔离资源复制到 assets/current(避免符号链接在打包时失效)
rm -rf assets/current && mkdir -p assets/current && cp -R assets/android/. assets/current/
flutter clean
flutter pub get
flutter build apk --release --target=lib/main_android.dart
echo "Android构建完成!"
echo "APK位置: build/app/outputs/flutter-apk/app-release.apk"
iOS 构建脚本类似,复制 assets/ios/
到 assets/current/
。
代码中统一使用 assets/current/...
路径加载资源,无需平台判断。
五、一键验证:确保真正隔离
运行验证脚本:
./scripts/verify_isolation.sh
输出示例(Android构建后):
✅ 找到 assets/current/ 资源清单条目
✅ 确认 assets/current/ 实际文件存在
✅ 检测到 ANDROID 专属标识字符串
❌ 未发现 IOS 专属标识字符串
✅ 代码import检查通过(无跨平台引用)
✅ 原生依赖检查通过(Gradle配置正确)
🎉 隔离验证完全通过!
验证原理:
- 资源检查:确认只有当前平台和共享资源被打包
- 代码检查:在编译产物中搜索平台专属标识字符串
- Import检查:确保没有跨平台import语句
- 原生依赖检查:验证Gradle/Podfile配置正确
六、完整验证脚本设计
#!/bin/bash
set -e
APP_APK="build/app/outputs/flutter-apk/app-release.apk"
IOS_APP_DIR="build/ios/iphoneos/Runner.app"
echo "=== 验证资源与代码隔离 ==="
RESULT_OK=1
if [ -f "$APP_APK" ]; then
echo "[Android] 检查 APK: $APP_APK"
echo "- 资源: AssetManifest.json 中的 current 与 shared"
if unzip -p "$APP_APK" assets/flutter_assets/AssetManifest.json | grep -Eq 'assets/current/|assets/shared/'; then
echo "✅ 发现 assets/current/assets/shared 清单条目"
else
echo "❌ Android 资源清单缺失(隔离失败)"; RESULT_OK=0
fi
echo "- 资源: 实际文件列表 (assets/current)"
if unzip -l "$APP_APK" | grep -q 'assets/flutter_assets/assets/current/'; then
echo "✅ 发现 assets/current 实际文件"
else
echo "❌ Android 未发现 assets/current 文件(隔离失败)"; RESULT_OK=0
fi
echo "- 代码: 搜索哨兵字符串 (仅 ANDROID 存在,IOS 不应存在)"
SNAPSHOT_CANDIDATES=$(unzip -l "$APP_APK" | awk '{print $4}' | grep '^assets/flutter_assets/' | grep -E 'snapshot|kernel|app.dill|vm_snapshot_data|isolate_snapshot_data' || true)
if [ -z "$SNAPSHOT_CANDIDATES" ]; then
echo "- 未找到 snapshot 文件,尝试在 libapp.so 中搜索"
SO_PATHS=$(unzip -l "$APP_APK" | awk '{print $4}' | grep -E '^lib/.*/libapp.so$' || true)
if [ -z "$SO_PATHS" ]; then
echo "❌ 未找到 libapp.so,无法进行代码哨兵校验(隔离失败)"; RESULT_OK=0
else
AND_FOUND=0
IOS_FOUND=0
for f in $SO_PATHS; do
if unzip -p "$APP_APK" "$f" | strings | grep -q 'SENTINEL_ONLY_IN_ANDROID_CODE'; then AND_FOUND=1; fi
if unzip -p "$APP_APK" "$f" | strings | grep -q 'SENTINEL_ONLY_IN_IOS_CODE'; then IOS_FOUND=1; fi
done
if [ $AND_FOUND -eq 1 ]; then echo "✅ ANDROID 哨兵存在"; else echo "❌ 未发现 ANDROID 哨兵(可能被优化,确保在代码中被引用)"; RESULT_OK=0; fi
if [ $IOS_FOUND -eq 0 ]; then echo "✅ 未发现 IOS 哨兵"; else echo "❌ 发现 IOS 哨兵(隔离失败)"; RESULT_OK=0; fi
fi
else
AND_FOUND=0
IOS_FOUND=0
for f in $SNAPSHOT_CANDIDATES; do
if unzip -p "$APP_APK" "$f" | strings | grep -q 'SENTINEL_ONLY_IN_ANDROID_CODE'; then AND_FOUND=1; fi
if unzip -p "$APP_APK" "$f" | strings | grep -q 'SENTINEL_ONLY_IN_IOS_CODE'; then IOS_FOUND=1; fi
done
if [ $AND_FOUND -eq 1 ]; then echo "✅ ANDROID 哨兵存在"; else echo "❌ 未发现 ANDROID 哨兵(可能被优化,确保在代码中被引用)"; RESULT_OK=0; fi
if [ $IOS_FOUND -eq 0 ]; then echo "✅ 未发现 IOS 哨兵"; else echo "❌ 发现 IOS 哨兵(隔离失败)"; RESULT_OK=0; fi
fi
fi
if [ -d "$IOS_APP_DIR" ]; then
echo "[iOS] 检查 APP: $IOS_APP_DIR"
echo "- 资源: AssetManifest.json 中的 current 与 shared"
if [ -f "$IOS_APP_DIR/Frameworks/App.framework/flutter_assets/AssetManifest.json" ]; then
if grep -Eq 'assets/current/|assets/shared/' "$IOS_APP_DIR/Frameworks/App.framework/flutter_assets/AssetManifest.json"; then
echo "✅ 发现 assets/current/assets/shared 清单条目"
else
echo "❌ iOS 资源清单缺失(隔离失败)"; RESULT_OK=0
fi
fi
echo "- 资源: 实际文件列表 (assets/current)"
if find "$IOS_APP_DIR" -path '*/Flutter/flutter_assets/assets/current/*' | grep -q .; then
echo "✅ 发现 assets/current 实际文件"
else
echo "❌ iOS 未发现 assets/current 文件(隔离失败)"; RESULT_OK=0
fi
echo "- 代码: 搜索哨兵字符串 (仅 IOS 存在,ANDROID 不应存在)"
APP_BIN="$IOS_APP_DIR/Frameworks/App.framework/App"
if [ -f "$APP_BIN" ]; then
if strings "$APP_BIN" | grep -q 'SENTINEL_ONLY_IN_IOS_CODE'; then echo "✅ IOS 哨兵存在"; else echo "❌ 未发现 IOS 哨兵(可能被优化,确保在代码中被引用)"; RESULT_OK=0; fi
if strings "$APP_BIN" | grep -q 'SENTINEL_ONLY_IN_ANDROID_CODE'; then echo "❌ 发现 ANDROID 哨兵(隔离失败)"; RESULT_OK=0; else echo "✅ 未发现 ANDROID 哨兵"; fi
else
echo "❌ 未找到 iOS App 二进制,无法进行代码哨兵校验(隔离失败)"; RESULT_OK=0
fi
fi
if [ $RESULT_OK -eq 1 ]; then
echo "✅ 隔离验证通过"
echo "=== 验证结束 ==="
exit 0
else
echo "❌ 隔离验证失败(详见上方 ❌ 提示项)"
echo "=== 验证结束 ==="
exit 1
fi
每次构建后,只需查看脚本最后输出:"✅ 隔离验证通过" 即可确认这次打包是干净的。
总结
通过本文介绍的多入口+资源隔离+自动验证方案,你可以彻底解决Flutter跨平台应用中的代码和资源隔离问题。这个方案不仅适用于支付场景,任何需要平台特定实现的场景都可以借鉴这种方法。
最重要的是,通过自动化验证脚本,你可以在每次构建后快速确认隔离是否成功,让审核问题无所遁形,让应用体积得到优化。
这个方案我觉得不完美的地方在于资源仍然需要脚本复制这样极其难维护,如果你有好的复制资源的方案可在评论区留言,谢谢。