1. 先回答一个让人纠结的问题:为什么是OpenHarmony配Flutter
大概从2023年下半年开始,我身边陆续有做智能硬件、开发板、教育设备的团队开始问同一个问题:要不要上开源鸿蒙?要不要用Flutter做跨平台?这个问题放到两三年前其实没什么好纠结的——没有官方适配,想用也用不了。但到了现在这个时间点,OpenHarmony生态已经积累了足够多的系统和芯片适配,Flutter在开源鸿蒙上的运行路径也基本跑通了,越来越多的应用开发团队开始把“一套代码,多端运行”的目标从安卓和iOS扩展到OpenHarmony设备上。
我自己做完这个室内探险游戏应用后的体会是:这套组合最大的价值不是“赶时髦”,而是它真的把一个很现实的成本问题解决了——你不用为了一个特定系统单独养一套原生开发团队,也不用在ArkTS和Dart之间来回横跳。游戏的核心玩法、渲染逻辑、状态管理这些大头全部用Flutter搞定,平台相关的部分收紧在一个很薄的适配层里,OpenHarmony和其他平台各跑各的接口。对于想做“应用级演示、教育类产品、中小型互动应用”的团队来说,这是目前性价比很高的路线。
这篇博文不打算聊太虚的架构理念,就围绕“一个室内探险游戏应用从零到能在开源鸿蒙设备上跑起来”这条主线,把选型思路、环境配置、工程组织、核心玩法实现和真机调试这几个环节里真正重要的东西讲清楚。如果你正好在调研OpenHarmony应用开发,或者想把手里的Flutter应用扩展到开源鸿蒙设备上,这篇应该能帮你少走不少弯路。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 游戏需求拆解:这个探险游戏到底要做什么
很多人一上来就写代码,先把一个方块在屏幕上动起来,然后发现自己根本不知道要做什么游戏。做跨平台应用更是这样——需求不明确的时候,平台适配工作根本无从谈起。我建议你先把游戏定成一张清晰的需求清单,再动手。
2.1 核心玩法与功能清单
这个室内探险游戏的核心玩法,我定成了一套非常经典的单人探索机制:
- 玩家在一个网格地图上移动,地图由多个房间组成,每个房间有不同主题(卧室、书房、密室、走廊等)。
- 场景里有可收集物品(钥匙、笔记、道具),玩家经过时自动拾取,背包界面实时更新。
- 地图中分布着机关和谜题,需要特定道具才能解锁下一个区域,形成简单的“钥匙-门”循环。
- 整个地图有迷雾遮盖,玩家只能看到自己周围一定范围内的情况,走到哪里,哪里才亮起来。
- 游戏设置一个出口,集齐指定数量道具后到达出口就算通关,同时记录通关用时和收集率。
这种玩法的好处是:逻辑复杂度适中,既不会像“俄罗斯方块”那样考验细节手感,也不会像大型RPG那样需要写几万行业务代码。所有功能点都适合用Flutter的组件化思维去拆,而且天然适合跨平台演示——不管是手机、平板还是开发板,都能完整体验。
2.2 为什么这类游戏适合做OpenHarmony跨平台Demo
如果你只是想把OpenHarmony的适配链路测通,随便写个Hello World就够了。但如果你想验证的是“Flutter应用跑到OpenHarmony设备上的整体可用性”,那就需要有一些真实的交互逻辑和渲染压力。室内探险游戏这个题材非常合适:
第一,它有持续的用户输入和画面更新。玩家移动时地图要实时刷新、迷雾要动态计算、动画要连贯播放,这些都会持续生成渲染帧和状态更新,能真实暴露渲染管线和Dart虚拟机的性能问题。
第二,它有典型的平台能力调用场景。存档需要访问文件系统,音效需要走音频播放,计时器需要读取系统时间,这些都会触发Flutter和OpenHarmony系统层之间的桥接调用——而这恰恰是跨平台适配中最容易出问题的部分。
第三,它具备完整的应用生命周期。从启动、运行、暂停到退出,游戏的状态管理比普通工具类应用复杂得多,能有效检验Flutter在OpenHarmony上的生命周期管理是否可靠。
3. OpenHarmony环境搭建:版本对不上就是灾难现场
如果让我给新手一个最重要的建议,那就是:先确认开发环境的版本组合,再动手写代码。OpenHarmony和Flutter的适配版本演进非常快,我现在写这篇文章时用的组合,很可能过几个月就不是最优解了,但排查问题的思路是通用的。
3.1 需要准备的核心组件
搭建OpenHarmony + Flutter开发环境,你至少需要下面这几样:
| 组件 | 作用 | 说明 |
|---|---|---|
| OpenHarmony SDK | 提供系统API和编译工具链 | 包含API版本、Native工具链、SDK组件,不同设备厂商的SDK有细微差异 |
| Node.js和hvigor | OpenHarmony应用构建工具 | 类似安卓的Gradle,负责把HarmonyOS工程编译成可安装的HAP包 |
| Flutter SDK(OpenHarmony适配版) | 跨平台开发框架 | 需要使用OpenHarmony SIG维护的Flutter分支,不是谷歌原版直接就能用 |
| DevEco Studio / DevEco Device Tool | 集成开发环境 | 具体选哪个,取决于你是在做应用开发还是设备适配 |
这些组件里,最容易出问题的是Flutter SDK的版本。OpenHarmony生态目前的主流通路是使用OpenHarmony SIG维护的flutter_flutter分支,这个分支会定期同步上游Flutter的新特性,但存在自己独立的版本节奏。我第一次搭建环境时踩过坑:直接用官方flutter install命令装的稳定版,结果编译鸿蒙工程时提示找不到OpenHarmony平台支持。后来换了适配分支才跑通。
3.2 从零搭一套可用的开发环境
下面是我实际操作下来比较顺畅的步骤,你在自己机器上照着来就行。
第一步:安装OpenHarmony SDK
打开DevEco Studio,在SDK Manager里勾选OpenHarmony SDK。这里要注意,你先想清楚主要运行的设备系统版本。我使用的是API 12左右的版本,对应OpenHarmony 5.x系列的Behavior Kit组件。不同API版本之间,一些系统能力接口有差异,比如权限申请的API在API 9和API 12之间就有明显变化。
第二步:获取OpenHarmony适配版的Flutter SDK
这里推荐用git直接clone社区维护的Flutter分支:
bash复制git clone https://gitee.com/openharmony-sig/flutter_flutter.git -b master
clone完成后,把bin目录加到PATH里,然后跑一下:
bash复制flutter doctor
如果环境配置正确,你会看到类似“OpenHarmony”的可选通道出现在flutter doctor的输出里。如果没有,检查一下环境变量FLUTTER_STORAGE_BASE_URL和PUB_HOSTED_URL是否设置正确,国内网络环境下这两项通常需要指向镜像源。
第三步:创建Flutter工程并添加鸿蒙平台目录
普通的flutter create命令会生成android、ios、linux等平台的目录,不会自动生成鸿蒙的工程结构。你需要做的是:
- 工程根目录下创建
ohos目录。 - 在
ohos目录里放置鸿蒙应用工程文件(build-profile.json5、hvigorfile.ts、entry/src等结构)。 - 在Flutter的
pubspec.yaml中确认flutter sdk的path指向你clone的适配分支。
说白了,OpenHarmony项目不是“一条命令生成”的,它更像是把Flutter的Dart侧代码和鸿蒙的原生壳工程手工组合在一起。分为两个部分:Dart代码负责游戏业务逻辑和UI渲染,鸿蒙壳工程负责应用入口、权限声明、生命周期回调。
第四步:编译并安装到设备或模拟器
DevEco Studio支持连接真机和模拟器。对于OpenHarmony开发,常见的真机是RK3568开发板、DAYU200系列或第三方厂商的OpenHarmony设备。如果是模拟器,直接用DevEco Studio自带的开源鸿蒙模拟器就可以。
编译时注意,如果用命令行构建,确保在ohos目录下执行:
bash复制hvigorw assembleHap
生成的HAP包在entry/build/default/outputs/目录下,然后用hdc工具安装:
bash复制hdc install entry/build/default/outputs/default/entry-default-signed.hap
3.3 我的环境版本清单(供参考)
我最后跑通游戏用的是一组相对稳定的组合:
- OpenHarmony SDK:API 12,配套DevEco Studio 5.x
- Flutter SDK:OpenHarmony SIG维护分支,Dart 3.x版本
- Node.js:18 LTS
- 构建工具:hvigor 5.x
建议你在动手前先去OpenHarmony SIG的社区仓库和Gitee上看一眼最新的适配状态,以你拿到的版本为准。版本这东西没有“永恒正确答案”,只有“当前可用组合”。
4. 跨平台工程架构:让游戏核心跑到哪都能跑
很多人写跨平台应用的时候有个误区:把所有代码都堆在Widget里,美其名曰“一套代码到处跑”,实际上是把平台耦合写进了每一行。真正健康的跨平台架构应该是:核心逻辑纯Dart实现,平台差异全部收敛到适配层,UI层只做状态渲染。
4.1 分层设计是跨平台活的灵魂
我设计的工程结构是这样的:
code复制lib/
├── main.dart # 应用入口
├── game/
│ ├── engine/
│ │ ├── game_controller.dart # 游戏主控逻辑
│ │ ├── map_model.dart # 地图数据模型
│ │ ├── player.dart # 玩家状态
│ │ └── fog.dart # 迷雾算法
│ ├── events/
│ │ └── game_events.dart # 游戏事件定义
│ └── types/
│ ├── tile.dart # 格子类型
│ └── item.dart # 物品类型
├── presentation/
│ ├── pages/
│ │ ├── game_page.dart # 游戏主界面
│ │ └── settings_page.dart # 设置界面
│ ├── widgets/
│ │ ├── map_view.dart # 地图渲染组件
│ │ ├── inventory_bar.dart # 背包栏
│ │ └── fog_layer.dart # 迷雾层
│ └── controllers/
│ └── game_scope.dart # 状态管理
├── platform/
│ ├── platform_bridge.dart # 平台桥接抽象
│ ├── platform_bridge_stub.dart # 默认实现
│ └── harmony/
│ └── platform_bridge_harmony.dart # OpenHarmony实现
└── model/
├── save_model.dart # 存档模型
└── save_repository.dart # 存档仓储
这里最核心的思想是依赖倒置:game/engine目录下的代码完全不感知自己运行在什么系统上,它只和抽象接口打交道。比如存档功能,游戏引擎只调用一个SaveRepository接口,具体实现由各平台分别提供。
4.2 平台桥接层的抽象设计
平台桥接层要处理的事情很杂,文件读写、传感器、音频播放、系统信息获取等等。我建议你把这些能力定义成一个统一的抽象类:
dart复制// platform/platform_bridge.dart
abstract class PlatformBridge {
Future<String> getDeviceName();
Future<bool> saveGame(String slotName, String data);
Future<String?> loadGame(String slotName);
Future<void> vibrate(Duration duration);
Future<void> playSound(String assetPath);
// 云存档、排行榜等能力可以后续扩展
}
然后,不同平台各自实现这个接口。在OpenHarmony上,实现内部的对应关系是这样:
saveGame和loadGame,通过MethodChannel调用到原生侧,由原生代码写入应用沙箱文件目录。vibrate,通过MethodChannel调用系统震动能力。playSound,通过OpenHarmony的音频播放API实现,但为了性能,音频资源先放到鸿蒙壳工程的resources目录里,而不是走Flutter asset。
这个桥接层不要太厚,只做“翻译工作”,不要在原生侧写游戏逻辑。我见过一些项目把大量判断逻辑写进Kotlin/ArkTS侧,结果平台版本一升级,原生代码崩得比Dart侧还快。
4.3 在OpenHarmony上实现MethodChannel
Flutter在OpenHarmony上的平台通道用法,和安卓基本一致。Dart侧发起方法调用,ArkTS侧用一个插件类注册方法处理器。下面是一段Dart侧的调用代码:
dart复制// platform/harmony/platform_bridge_harmony.dart
import 'package:flutter/services.dart';
class PlatformBridgeHarmony implements PlatformBridge {
static const MethodChannel _channel = MethodChannel('com.example.indoor_explorer/platform');
@override
Future<String> getDeviceName() async {
final String? name = await _channel.invokeMethod('getDeviceName');
return name ?? 'OpenHarmony Device';
}
@override
Future<bool> saveGame(String slotName, String data) async {
final bool? result = await _channel.invokeMethod('saveGame', {
'slot': slotName,
'data': data,
});
return result ?? false;
}
}
对应的鸿蒙原生侧,需要注册这个通道并在onMethodCall里处理对应方法。这里涉及的是ArkTS侧的代码,主要逻辑是拿到Dart传来的参数,调用OpenHarmony的fss接口写文件。核心思路和安卓原生的Plugin写法非常接近,如果做过安卓原生开发,上手成本很低。
4.4 事件流:从原生到Dart的方向
除了Dart主动调用原生,还有一类调用是反方向的——原生主动向Dart发事件。常见场景是:游戏切后台再回前台时,需要通知Dart侧刷新状态。OpenHarmony的壳工程在onPageShow或onBackground生命周期里,通过EventChannel把事件推给Dart侧。
dart复制static const EventChannel _eventChannel =
EventChannel('com.example.indoor_explorer/lifecycle');
// 在initState里监听
_eventChannel.receiveBroadcastStream().listen((event) {
if (event == 'resumed') {
// 游戏恢复时暂停/恢复计时器
GameController.instance.onAppResumed();
}
});
这个能力对游戏应用非常关键。玩家在探索过程中突然来电话、切到微信回消息、或者折叠屏设备形态切换,都会触发生命周期变化。如果不在这个层面做状态保护,玩家玩到一半切出去再回来,会发现游戏计时乱了、音乐还在响,体验很糟。
5. 地图、移动与迷雾:室内探险的核心玩法实现
环境搭好了,架构定好了,接下来就是游戏本体。这一节把室内探险游戏最核心的三个模块拆开讲:地图怎么存、角色怎么动、迷雾怎么算。
5.1 地图数据结构:用二维数组搞定一切
室内探险地图本质是网格地图,最直接的存储方式就是二维数组。每个格子的类型用一个枚举表示:
dart复制// game/types/tile.dart
enum TileType {
wall, // 墙壁,不可通行
floor, // 地板,可通行
doorLocked, // 上锁的门,需要钥匙
chest, // 宝箱,包含道具
item, // 散落物品
exit, // 出口
}
地图的读取我建议直接用JSON配置,而不是在代码里手写地图数组。这样方便做多张地图,也方便以后支持地图编辑器。地图配置文件示例如下:
json复制{
"width": 12,
"height": 8,
"tiles": [
"###########",
"#P..#.....#",
"#..##.##..#",
"#..K.....##",
"#.d...E...#",
"#.....#...#",
"#......#..#",
"###########"
]
}
这段配置里,#代表墙,P是起点,K是钥匙,d是上锁的门,E是出口,.是地板。解析时只需要遍历字符串数组,把每个字符映射成对应的TileType,同时记录玩家出生点、门和出口的位置。
用这种方式做地图的好处是直观、易调试、可版本管理。我经常在地图配置文件里直接修改几个字符来测试不同的房间布局,比在可视化编辑器里拖拽快得多。
5.2 玩家移动与碰撞检测:方向向量是基础
玩家移动可以用一个很简单的模式来实现:监听上下左右按键或滑动手势,计算目标单元格,检查目标格是否可通行,如果可通行就更新玩家位置。
dart复制// 方向映射
const Map<MoveDirection, Offset> directionVectors = {
MoveDirection.up: Offset(0, -1),
MoveDirection.down: Offset(0, 1),
MoveDirection.left: Offset(-1, 0),
MoveDirection.right: Offset(1, 0),
};
bool canMove(TileType target) {
switch (target) {
case TileType.wall:
case TileType.doorLocked:
return false;
default:
return true;
}
}
void movePlayer(MoveDirection dir) {
final vector = directionVectors[dir]!;
final targetX = _player.x + vector.dx.toInt();
final targetY = _player.y + vector.dy.toInt();
// 边界检查
if (targetX < 0 || targetX >= _map.width || targetY < 0 || targetY >= _map.height) return;
final targetTile = _map.getTile(targetX, targetY);
if (!canMove(targetTile)) return;
// 处理格子事件(拾取物品、打开门、触发机关)
_onTileEntered(targetX, targetY);
_player.moveTo(targetX, targetY);
}
移动这块看着简单,但有几个细节值得注意。第一,如果用Offset(0, -1)表示上移,屏幕坐标系的Y轴是向下增长的,所以向上移动是Y减1,我第一次写的时候反了。第二,不要每帧都做碰撞检测,应该用事件驱动的方式,在用户输入时触发一次移动判断。第三,角色移动要有动画过渡,可以在两个格子之间做一个短暂的线性插值,不然玩家操作起来会觉得很“生硬”。
5.3 迷雾视野算法:只照亮自己周边九宫格还不够
迷雾系统是这个游戏氛围感的核心,也是我调试时间最长的一个模块。最简单的实现是“玩家周围固定半径内的格子全部点亮”,但这样玩起来没有探索感,因为玩家能隔着墙看到房间另一头的布局。
我采用的方案是逐格光线投影:以玩家位置为中心,向周围发射一圈光线,遇到墙壁就停止传播,被光线扫到的格子标记为“当前可见”;之前点亮过但当前不可见的格子标记为“已探索”(用稍暗的颜色表示记忆)。这样实现出来,玩家进入一个新房间时只能看到入口附近的情况,需要走进去才能看清全貌。
核心代码逻辑大致是这样的:
dart复制void updateFog(int playerX, int playerY) {
// 先重置可见性
for (var tile in _visibleTiles) {
_fogMap[tile.y][tile.x] = FogState.explored;
}
_visibleTiles.clear();
// 以玩家为中心,每个角度发射一条射线
const int rayCount = 64;
const double maxDistance = 4.5;
for (int i = 0; i < rayCount; i++) {
final angle = (i / rayCount) * 2 * pi;
final dx = cos(angle);
final dy = sin(angle);
for (double dist = 0; dist < maxDistance; dist += 0.25) {
final x = playerX + (dx * dist).round();
final y = playerY + (dy * dist).round();
// 超出地图边界就停止这条射线
if (x < 0 || x >= _map.width || y < 0 || y >= _map.height) break;
_fogMap[y][x] = FogState.visible;
_visibleTiles.add((x: x, y: y));
// 遇到墙停止传播
if (_map.getTile(x, y) == TileType.wall) break;
}
}
}
注意,这个实现是简化版本,在性能上做了取舍。真实项目中你还可以加光线偏移采样、半透明视野边缘、墙角平滑处理等优化,但基础逻辑已经足够让游戏有“探险”的感觉了。320x240这样的小地图上,每帧跑64条射线的计算量完全没问题。
5.4 道具、背包与门锁:把交互行为编程数据
道具系统的设计要遵循“数据驱动”的原则。每种道具的行为不要硬编码在游戏逻辑里,而是定义成一份配置表:
dart复制class ItemDefinition {
final String id;
final String name;
final String iconAsset;
final ItemType type;
final Map<String, Object> effects;
}
比如钥匙这个道具,它的配置是:
dart复制ItemDefinition(
id: 'rusty_key',
name: '生锈的钥匙',
iconAsset: 'assets/items/rusty_key.png',
type: ItemType.key,
effects: {'opens': 'room_1_door'},
)
游戏引擎在玩家拾取道具时,先检查道具配置,再执行对应效果。这样以后新增道具种类,只需要加配置和素材,不用改引擎代码。
门锁的处理也有讲究。上锁的门的TileType是doorLocked,当玩家身上有指定钥匙时,走到门前触发“解锁”事件,把门的TileType改成floor,同时移除背包中的钥匙。这个过程要做成不可逆的,防止玩家反复开门关门刷道具。
6. 鸿蒙真机调试手记:常见坑与绕过方案
最后这部分是我最想分享的,也是网上资料最少的部分。Flutter应用跑到开源鸿蒙设备上的调试体验,和安卓、iOS有相似之处,但坑很多不一样。
6.1 日志系统:不要只盯着flutter的print
Flutter侧用debugPrint输出的日志,在鸿蒙设备上不一定能稳定地出现在DevEco Studio的日志窗口里。我遇到过的情况是:Dart侧print的信息偶尔会丢失,尤其是闪退前的最后几条日志。
我的建议是双通道看日志:
- Flutter侧的日志,用
flutter logs命令查看。 - OpenHarmony原生侧的日志,用hdc工具拉取系统日志:
bash复制hdc hilog
还有一个更稳的方法:自己的代码里做一个轻量级日志转发器,Dart侧的输出同时通过EventChannel发到原生侧,由原生侧写入日志文件。这样即使Dart虚拟机崩溃了,崩溃前一段时间的操作记录还在文件里,方便回溯。
6.2 热重载时灵时不灵,学会“冷启动”
Flutter开发最爽的就是热重载(Hot Reload),但在OpenHarmony上,热重载的稳定性还比不上安卓。我遇到的最典型的问题:改动了pubspec.yaml里的依赖,热重载后Dart侧报一堆奇怪的类找不到错误,其实是因为依赖没有同步加载。
我的处理方式很简单:改了依赖就停掉应用,执行flutter clean再重新编译。不改依赖的情况下,热重载大多数时候是有效的,只是第一次启动会比较慢。养成“改依赖就冷启动”的习惯后,这类问题基本没有出现过。
6.3 渲染管线:Impeller在鸿蒙上的表现差异
Flutter新版本默认开启了Impeller渲染引擎,它通过预编译shader显著解决了原来Skia在低端设备上首次渲染卡顿的问题。但Impeller要求GPU对Vulkan或OpenGL ES 3.0有较好的支持,而一些低端OpenHarmony开发板的GPU驱动并不完善。
我在DAYU200开发板上跑游戏时,遇到过一次症状:画面正常但部分半透明效果显示成黑色块。排查后发现是Impeller对半透明混合模式的支持问题。后来在AndroidManifest或鸿蒙工程配置里禁用Impeller,回退到Skia,问题解决。同样的游戏在手机上跑完全正常。
所以在开发阶段建议设置一个渲染引擎的开关,方便在不同设备上做对比测试:
yaml复制# pubspec.yaml 或启动参数
flutter run --no-enable-impeller
6.4 内存管理:别让小地图吃光设备内存
OpenHarmony设备的规格跨度极大,从几MB内存的IoT设备到几GB内存的开发板都有。我做游戏时默认假设目标设备至少2GB内存,但实际测试时还是发现内存占用偏高。
原因出在地图纹理上:如果每块墙体瓷砖都用一张独立纹理图片,一张地图几十种瓷砖,纹理内存会迅速累积。解决办法是采用瓦片图集(Tile Atlas):把所有瓷砖素材装进一张大图里,渲染时按坐标裁剪。Dart侧只需要加载一张大图,GPU侧的纹理内存占用大幅下降。
6.5 机构认证与签名:别到了发布才发现签名不对
如果你不只是想在开发板上跑,还要发布到应用市场或分发给其他OpenHarmony设备,记得提前配置签名。OpenHarmony应用的签名体系和安卓类似,但流程不同。
在DevEco Studio里配置自动签名后,生成的HAP会带签名信息。如果手动用hdc安装时报签名错误,大概率是签名配置文件里的证书指纹和设备不匹配。这个问题我建议在项目启动时就处理,而不是最后发布时才做,否则会卡住整个流程。
7. 最后再分享几点做这类项目的个人体会
这个室内探险游戏从立项到跑通,前后大概用了三周业余时间。做完全套流程,我对“开源鸿蒙 + Flutter”这套组合有了更具体的认识:
第一,别把OpenHarmony设备当成安卓设备来对待。 虽然API逻辑有相似之处,但系统服务、权限模型、生命周期细节都不一样。我花了最多时间排查的问题,几乎都出在“默认它跟安卓一样”的假设上。
第二,跨平台的核心不是“一套代码跑所有”,而是“核心逻辑跑所有,平台差异精确隔离”。 这个项目里可能只有5%的代码是针对OpenHarmony写的,但这5%决定了整个项目能不能跑顺。
第三,做跨平台应用不要追求设备覆盖率,要追求设备验证覆盖率。 只在一台手机上调通不算数,我强烈建议至少准备一台低端开发板和一台主流手机,两个平台跑同一套代码,你会很快发现某些在高端设备上不明显的问题,在低端设备上会放大十倍。
后续如果要扩展这个项目,我比较看好的方向是:加在线排行榜(需要接入网络和账号系统)、把地图编辑器做成可视化工具、支持多人在线组队探险(涉及实时通信)。这些扩展的入口都已经在架构里留好了——只要往平台桥接层加新接口,往游戏引擎加新事件,UI层就能串联起来。
如果你也在做类似的项目,卡在某个具体的环境配置或功能实现上,欢迎带着具体问题来交流。这类组合项目现在资料不多,正是大家一起填坑的时候,踩过坑的经验聊起来最值钱。
