
1. 项目概述与整体设计思路最近一直在折腾 Flutter 在 OpenHarmony 上的落地正好手上有个口腔护理 App 的项目需求就把提醒设置这个功能完整的走了一遍。这里我直接说结论Flutter 跑在 OpenHarmony 上已经不是什么实验性质的东西了openharmony-sig 维护的 flutter_flutter、flutter_engine、flutter_packages 三个仓库已经能支撑真实的业务开发。口腔护理这个场景很适合作为实战案例因为它的功能边界清晰——刷牙计时、习惯提醒、口腔档案、知识科普每一块都能映射到 Flutter 加 OpenHarmony 的能力交叉点上。先说清楚这篇实战笔记要解决什么问题。你拿到手的是一个标准 Flutter 工程目标设备是一台搭载 OpenHarmony 的平板或手机你要做的不是一个 Demo而是一个能装到设备上、能定时弹通知、能记录用户刷牙数据的完整应用。其中提醒设置这个功能是最容易翻车的因为 OpenHarmony 的闹钟和通知机制跟 Android 有区别而 Flutter 侧通过 Platform Channel 通信的方式也需要踩过一遍坑才能顺手。这篇文章适合三类人看一是想在 OpenHarmony 设备上跑 Flutter 应用的开发者二是做健康类、工具类 App 想对接鸿蒙生态的产品团队三是对 Platform Channel 通信机制还不熟的 Flutter 初学者——你会看到一条从 Dart 到 ArkTS 的完整链路。1.1 为什么选 Flutter 而不是直接上 ArkUI这个问题我在项目立项时纠结过。如果是纯鸿蒙生态的单端应用ArkUI 的声明式写法和系统能力调用的紧密度确实好。但口腔护理这个品类有个特点——它有极强的跨端需求用户的手机可能是 Android 或 iOS家里可能还有带屏设备而诊所的展示终端可能用的是 OpenHarmony 系统。这种情况下用 Flutter 做一套 UI 和业务逻辑再分别对接各端底层能力比每个端单独维护一套 ArkTS 代码划算得多。另一个关键点是 Flutter 的渲染引擎在 OpenHarmony 上的适配已经跑通了。Flutter 的 UI 绘制不依赖系统原生控件它在 ArkUI 之上做的是一层独立的渲染所以只要 flutter_engine 的 ohos 分支适配到位Dart 层写好的界面在鸿蒙设备上的表现跟 Android 上高度一致。实测下来大部分纯 UI 组件Material 组件、自定义绘制、动画几乎零改动跑通真正需要改的是两处一是系统能力调用通知、闹钟、传感器二是文件路径和权限模型。这也正是提醒设置这个功能要做深的原因——它恰好踩中了“系统能力调用”这个最需要手工适配的点。1.2 功能模块拆解一个完整口腔护理 App 的边界提到口腔护理很多人的第一反应是加一个“刷牙倒计时 2 分钟”的页面就完事了。但真正做成一个能上架的产品功能边界要比这大得多。我按实际开发顺序拆了四个模块第一个是刷牙引导这是整个 App 的体验核心。用户点开始后界面进入一个 2 分钟的计时流程按口腔四分区左上、右上、左下、右下每 30 秒切换一次提示引导用户均匀刷牙。这里要用到 Timer.periodic界面最好有一个圆形进度环同时伴有振动反馈。第二个是习惯提醒也就是这篇实战笔记的主角。用户设置每天早晚两个时间点到点后系统弹通知。这里有三个细节第一是提醒要在 App 被杀死后依然生效这就不能靠 Dart 层的 Timer必须落到 OpenHarmony 的原生闹钟能力上第二是通知要能区分“早间护理”和“晚间护理”的文案第三是提醒要支持星期重复——很多人只在工作日早晚刷牙周末会赖床提醒规则需要能配置。第三个是口腔档案。每次刷牙结束后记录一条数据包括时间、刷牙时长、是否使用牙线、是否使用漱口水。数据量不大本地存储就够了用 shared_preferences 或 hive 都行。但要注意一个问题OpenHarmony 上文件存储路径跟 Android 有差异shared_preferences 插件在 ohos 分支里是有适配实现的直接用就行但如果你用原生路径写文件一定要通过 PathProvider 拿标准目录不能硬编码 /data/data。第四个是知识科普一个内容列表页基本就是静态页面加一个详情页没什么技术含量但能撑起 App 的内容价值。功能模型有了下面要解决的是技术选型。整个 App 的状态管理我用的是 flutter_bloc 的 Cubit——不是 Bloc是 Cubit。原因很简单这个项目没有特别复杂的状态流提醒开关、刷牙倒计时、档案列表这几个状态都是独立的用 Cubit 写起来轻、测试容易不需要理解 Event 转 State 的那一套繁琐机制。有人可能问为什么不用 Provider 或 GetX我个人的经验是这种跨端项目后期会越来越复杂Cubit 的纪律性比 GetX 好又不至于像 Bloc 那样增加样板代码折中刚好。2. 环境准备与工程搭建实战如果说 Flutter 在 OpenHarmony 上最大的门槛是什么我的回答一定是环境搭建。Android 那一套 gradle 配置在鸿蒙上完全不适用取而代之的是 hvigor 构建系统加上 DevEco Studio 这套 IDE。第一次接触的人很容易在“项目建好了但跑不起来”这个环节耗掉半天时间。2.1 工具链准备DevEco Studio 与 Flutter SDK 的 ohos 分支先说 SDK 这一层。官方 flutter SDK 是不支持 OpenHarmony 的你需要拉取 openharmony-sig 维护的 flutter_flutter 仓库。这里有个细节这个仓库的版本分支是对应 OpenHarmony SDK 版本的比如 OpenHarmony 4.x 对应的是 flutter 3.x 的分支选版本的时候要先确认你本机装的 DevEco Studio 对应的 SDK 版本再反过来倒推 flutter 分支。我一开始就是没注意这个对应关系拉了个 master 分支结果编译到一半报一堆 API 不匹配的错白白浪费了半个小时。然后是 DevEco Studio。这个 IDE 基于 IntelliJ核心是 hvigor 构建插件和 ArkTS 编译器。安装的时候记得选对 SDK 版本装好之后用它的 SDK Manager 确认 OpenHarmony SDK 路径。最后是 flutter 配置。在环境变量里把 flutter SDK 指向你拉下来的 ohos 分支后跑一下 flutter doctor这里要注意doctor 里的 Android toolchain 显示的是不支持的只要 ohos 相关那一项正常就可以。有人会问Android SDK 还要不要装答案是不需要OpenHarmony 应用的构建链路是 ArkTS 源码直接编译成 hap 包中间不走 Android 的 dex 过程。2.2 混合工程结构Flutter Module 与鸿蒙工程的目录布局一个典型的 Flutter for OpenHarmony 工程长这样你有一个标准的 Flutter module 或 Flutter app然后有一个 ohos 目录里面是 DevEco Studio 生成的鸿蒙工程。你的 Dart 代码在 lib/ 下ArkTS 原生代码在 ohos/entry/src/main/ets/ 下两个工程通过 Flutter 插件机制和 Platform Channel 通信。创建步骤很简单先建一个 Flutter 工程然后在工程根目录用 DevEco Studio 打开它会自动识别并添加 ohos 支持。如果没自动生成也可以手动在 DevEco Studio 里新建一个空工程再把工程结构并进来。我建议的做法是先跑通一个 hello_world 级别的混合工程再去填业务代码。这个验证过程非常重要因为它把环境问题都提前暴露了不然等你写了上千行业务代码再来排查构建问题够你喝一壶的。工程目录建好后你的注意力要放在 ohos/entry/src/main/module.json5 这个文件上。这是鸿蒙侧的模块配置类似 Android 的 AndroidManifest.xml权限声明、Ability 注册、后台任务的权限都在这里配。2.3 从零跑通第一个 Flutter 鸿蒙页面跑通第一个页面的关键点在于原生侧入口的配置。鸿蒙侧加载 Flutter 页面的方式跟 Android 不太一样Android 是在 MainActivity 里用 FlutterEngine 加 FlutterFragmentOpenHarmony 这边是在 EntryAbility 里通过 Flutter 引擎的适配层来创建 Flutter 容器。用 ArkTS 的 lib 里提供的 FlutterAbility 或者自己创建一个组件来承载 Flutter UI。flutter_flutter 仓库里带了一个标准的引擎初始化代码有些模板还带着 ohos 插件注册的目录。第一次运行的时候建议直接在 DevEco Studio 里点 RunIDE 会走完 hvigor 构建、打包、安装的完整流程。这里我想强调一个最常见的坑如果你之前习惯用 Android Studio 的 gradle 命令行换成鸿蒙工程以后千万不要直接去敲 gradle assemble鸿蒙构建要用 hvigorw 或者 DevEco Studio 提供的构建菜单。我见过不少人卡在这个环节报错内容五花八门本质上都是用了错误的构建工具链。3. 口腔护理核心功能实现工程项目跑通后快速把核心页面和业务逻辑搭起来。这一节我会把刷牙计时、档案数据模型、状态管理三个部分讲清楚。3.1 刷牙计时器的实现逻辑刷牙计时器是“看起来简单做起来有细节”的功能。表面上是获取用户点击的开始事件然后启动一个 120 秒的倒计时实际上你要处理页面切后台、计时器销毁、四分区切换提示等一堆问题。核心逻辑放在一个 Cubit 里管理状态状态机设计成 idle、brushing、paused、finished 四种。开始刷牙后启动 Timer.periodic每秒发一个 tick 事件给 UI。UI 层只需要根据状态和剩余秒数画进度环和文案提示。这里要补一句耗时设计的原因刷牙的 2 分钟标准来自牙科共识但用户实际执行时会有差异。你可以做成可调整的比如默认 120 秒高级设置里允许改成 90 秒或 180 秒。UI 上的分区提示文案和振动节奏不用跟着总时长变只要按比例把四个分区的提示时间算出来就行。Dart 层要注意的是 Timer 的生命周期。如果你在 initState 里启动 Timer却忘了 dispose页面退出后计时器还在跑轻则内存泄漏重则引发 setState 崩溃。用 Cubit 的一个好处是它的 close 方法会统一管理资源你在 close 里 cancel 掉 Timer 就行不会漏。3.2 数据模型与存储方案口腔护理 App 涉及的数据很简单就两张表一张是刷牙记录另一张是提醒配置。刷牙记录包含这些字段记录 ID、开始时间戳、结束时间戳、刷牙总时长、是否使用牙线、是否使用漱口水、备注。每次结束刷牙流程后写一条列表页倒序展示。提醒配置包含提醒 ID、是否开启、早间提醒时间时、分、晚间提醒时间时、分、重复星期一个 int 数组或位图、是否启用振动。这套配置有两个默认值早间 07:30晚间 21:30周一至周日全开。存储方案我选的是 shared_preferences 插件配合 JSON 序列化。这里要注意OpenHarmony 上的 shared_preferences 实现是把数据写到 ohos 的应用沙箱里跟 Android 的 SharedPreferences 不是同一个实现但接口完全一样。如果你用的是 flutter_packages 仓库里带的 shared_preferences ohos 实现直接调就行不用改代码。FAQ 时有人问过为什么不用 sqlite。说实话口腔护理这个场景一天最多十几条记录一年几千条用 SQLite 有点杀鸡用牛刀。JSON 序列化存字符串读出来反序列化成 List简单粗暴可靠。真要哪天上万条了再上 sqlite 也不迟反正数据层的接口是可以替换的。3.3 用 Cubit 管理提醒设置页的状态提醒设置页的 UI 其实很简单两个时间选择器早/晚、七个星期开关、一个总开关、若干高级选项。真正复杂的是切换这些开关后的联动状态——比如你今天关了早间提醒时间选择器就变灰明天开启了上次设置的时间要回显。我用一个 ReminderCubit 来管理这块状态。它对外暴露的状态包括reminderEnabled、morningTime、eveningTime、repeatDays、vibrationEnabled。初始化时从本地存储读配置任何改动都走一个独立的 method比如 morningTimeChanged(TimeOfDay time)Cubit 内部更新完状态后自动持久化到 shared_preferences。为什么要用独立 method 而不是一个通用的 update 方法因为 Setup 改造时越界少——每个字段的改动路径清晰测试时可以精确断言某个方法是否调用了持久化逻辑。用惯例来解决逻辑复杂性的问题后面再有新字段加进配置时扩展也方便。4. 提醒设置功能深度实现提醒功能是整个口腔护理 App 的技术核心也是标题里点名的功能。它的难度不在 UI而在“App 被杀死后提醒依然要触发”这个硬性要求。Dart 层的 Timer 只能保证应用在运行期间有效杀进程就没了。所以提醒的落点必须下沉到 OpenHarmony 原生层通过 Platform Channel 让 Dart 侧把提醒配置“递”给原生侧由原生侧的系统闹钟和通知能力来兜底。4.1 需求定义与通道设计提醒功能的需求在前文已经拆过这节直接落到通道设计上。Flutter 与原生通信有几种方式MethodChannel 适合一次性的请求-响应调用EventChannel 适合原生侧主动往 Dart 侧推事件流BasicMessageChannel 适合连续的消息通信。提醒设置这个场景恰好用了两种用户设置提醒时用 MethodChannel 把配置传给原生原生闹钟触发后用 EventChannel 把“提醒已触发”这个事件推回给 Dart 侧。MethodChannel 的名字我会在 Dart 和原生两侧严格一致比如叫 com.example.oralcare/reminderchannel 命名的最佳实践是域名反写加业务名避免跟系统已有的 channel 冲突。方法定义我先列一个表Dart 侧方法是 PlatformChannel 调用入口返回值的处理必须考虑 native 还没有实现的情况。你写完 Dart 端调用后如果没有同步写好原生侧的实现运行时就会抛 MissingPluginException。4.2 MethodChannel 的 Dart 侧封装Dart 侧我封装了一个 ReminderChannel 类专门负责跟原生通信。这样做的目的是把 Platform Channel 的调用逻辑和业务逻辑隔离开。口腔护理 App 的 UI 层永远不直接跟 MethodChannel 打交道而是调 ReminderChannel 的方法ReminderChannel 内部才是 MethodChannel.invokeMethod。早起提醒的时间字符串转换很关键。Dart 侧拿到的 TimeOfDay 是 hour 和 minute组装成字符串用 07:30 传给原生侧。为什么不用两个 int 参数因为原生侧很多时候需要把它转成系统格式字符串 DateTime.parse 的方式在 ArkTS 侧更好处理。同时 Registration 方式也要留意在 OpenHarmony 上如果你在 Engine 初始化时没有注册对应的插件Dart 侧调用时同样会报 NotImplementedException。你需要到鸿蒙入口代码里把插件的注册逻辑加上。4.3 ArkTS 原生侧实现AlarmManager 与 NotificationManager原生侧代码我拆成了三个文件ReminderPlugin.ets 负责处理 MethodChannel 调用AlarmScheduler.ets 负责跟系统闹钟打交道NotificationHelper.ets 负责发通知。这种拆分不是拍脑袋——口腔护理 App 后期一定会加更多系统能力调用比如查应用权限插件入口会越来越臃肿拆开好维护。核心代码是 AlarmScheduler.ets。OpenHarmony 上管理定时提醒用的是 —— AlarmManager 与 NotificationManager。AlarmManager 能设定指定时刻的闹钟配合 WantAgent 在闹钟触发时拉起你的 Ability 或者发送代理事件。ReminderPlugin.ets 拿到配置后做三件事把时间字符串解析成具体时刻、计算下一次触发时间戳今天没到就今天到了就从明天算、通知 AlarmManager 注册。NotificationHelper 负责在闹钟触发后弹通知。OpenHarmony 的通知系统跟 Android 的布局很像需要构造 NotificationRequest设置 content 标题和文本按钮或动作可以加到 wantAgent 里。口腔护理的通知文案默认是“该刷牙啦”正文根据早上和晚上区分早上写“开启元气一天先做个口腔护理”晚上写“忙碌一天别忘了清洁牙齿”。4.4 时间转换与周期重复的计算提醒时间格式的转换是这类功能的写制度窗口。核心逻辑是把 “07:30” 这种用户可读字符串转成距离当前时刻的毫秒差值再转成系统闹钟的触发时间。具体计算分两种情况。第一种是明天的提醒——把触发时间设为明天指定时间跟当前时刻的差值。第二种是每周重复——需要在计算下一次触发时间时把星期几的因素考虑进去如果今天已过了设置时间下一次可能不是明天而是下个星期里设定重复的那天的同一时间。我这里给出一个示例计算假设今天是周三 20:00用户设置了周三、周五、周六 21:30 提醒。下一次触发应该是周三 21:30因为距离现在只有 90 分钟不用跳到周五。在代码里这个逻辑要循环遍历重复星期列表把未来一周内的每个候选时间算出来取最小正值。这个计算最容易出 bug 的地方在于“跨天”和“跨周”的边界。我刚开始实现时忽略了“设置时间恰好是当前时刻”这种情况结果闹钟在设置的那一秒立刻触发了一次。处理方式是如果计算出的毫秒差值小于 60 秒就直接加 24 小时再算。4.5 用 EventChannel 把原生触发事件推回 Dart提醒被触发后光弹通知还不够。很多产品需求希望此时回到 App 时能看到一条“本次提醒来自早晨 07:30 的定时任务”这样的记录。这就需要原生侧主动往 Dart 发消息——EventChannel。EventChannel 双向通信机制Dart 侧创建 EventChannel 并 listen原生侧持有 EventSink随时可以往 Dart 发送数据。在 alarm 触发时通过事件流把提醒类型morning/evening、触发时间戳、动作提醒通知已发送推给 Dart。Dart 侧收到后可以做三件事更新本地档案把“提醒已通知”追加进日志、刷新 UI 上的记录列表、如果有特殊引导页需求还可以跳转。这里有个经验之谈EventChannel 的事件流是单播的一个 EventChannel 只能有一个 listener。如果你的 App 有多个页面都想监听同一个原生事件需要在 Dart 侧做一个事件分发层——一个 RoomEventBus 之类的单例原生事件进来后广播给多个订阅者。口腔护理 App 里的记录页、首页、设置页都关心提醒是否触发所以这一步很有必要。4.6 权限声明与后台运行配置提醒功能能不能生效有一半取决于权限和后台配置是否正确。OpenHarmony 上跟提醒相关的权限主要在 module.json5 里声明。这里我列一下常见配置项和说明提醒的权限如果没配好上线后你会发现大部分用户的闹钟都不触发——系统静默地把你的后台任务禁了但界面没有任何提示。配权限最稳妥的办法是在 Debug 阶段就检查一遍 module.json5同时要做好权限请求被拒绝后的降级策略如果不给通知权限App 内还可以显示一个横幅提醒用户去系统设置打开。5. 常见问题与避坑实录这一节把我在实战开发中踩过的、以及在 Flutter 社区里高频出现的问题整理出来按照问题表现、原因分析、解决方案三步走。5.1 页面切换后状态丢失问题项目里有一个“刷牙记录列表”页面用户从首页 Tab 切到记录页再切回去时列表竟然重新加载了滑动位置也回到顶。这个问题在 Flutter 里太常见了根本原因是 Tab 切换时页面被销毁重建。Flutter 的 TabBarView 默认会销毁非当前页的 State解决办法有几种。一是给页面套 AutomaticKeepAliveClientMixin让 Tab 里的子页保持存活二是把状态提升到父级或全局 Cubit 管理页面只做展示——这也是我这次项目推荐的做法。因为记录数据本身就在 Cubit 里页面重建后从 Cubit 拿数据也是一样的只是要额外处理滚动位置。除了技术解法你做口腔护理 App 时还要想清楚 product 层面的取舍如果是查看型列表重新加载也无所谓如果是填写到一半的表单比如正在编辑个人档案状态一定要保住。这时候 AutomaticKeepAliveClientMixin 就是必需的了。5.2 Build 阶段报错与构建工具选择很多人从 Android 转到 OpenHarmony 后习惯性地敲 gradle 命令。结果报出来一堆 dependencies 解析失败、compileDebugJavaWithJavac 之类的错误。还有人会在 Flutter 工程里跑 flutter build apk然后跑来问为什么没有生成鸿蒙的包。这类问题的本质是混淆了两套构建系统。OpenHarmony 应用用的是 hvigorbuild 产物是 HAP 包不是 APKflutter build apk 是 Android 侧的逻辑。在混合工程里正确做法是用 DevEco Studio 提供的 Run 按钮或者 hvigorw 相关命令来构建。报错信息 into “could not resolve all task dependencies for configuration :app:debugcompileclasspath” 时通常不是代码问题而是远程仓库连接失败了。鸿蒙 SDK 相关的仓库地址如果网络不通会报类似的依赖找不全错误。处理思路是检查本机的 SDK 路径确认 DevEco Studio 的代理配置或者换用国内的镜像仓库地址。5.3 Dart 的 part 关键字与代码组织技巧热搜词里看到有人问 Flutter 中 part顺手说一下。Dart 的 part 和 part of 允许一个库拆到多个文件里主要解决单文件过大的问题。但在实际项目中part 的使用要谨慎因为 part 文件之间共享所有私有成员颗粒度拉得太细后代码的可读性反而下降。在口腔护理 App 里我没有用 part而是用了“单一职责文件 import 相对路径”的组织方式。比如 reminder 模块拆成reminder_models.dart、reminder_cubit.dart、reminder_channel.dart、reminder_page.dart。每个文件 import 自己需要的依赖彼此之间的依赖关系一目了然。除非某个 library 文件确实过大且内部有大量共享私有状态再考虑用 part 拆分。如果用 part有个坑要记得part 的引用路径是相对当前文件的而不是相对包的 lib 根目录写错路径会报 Target of URI does not exist 之类的错。5.4 EventChannel 未监听导致的崩溃EventChannel 有一个使用误区Dart 侧如果没有在合适的时机开始监听事件流那么原生侧事件发出时很可能会丢失或者在某些实现中引发错误。这个问题在 Android 和 OpenHarmony 上都存在。口腔护理 App 的正确姿势是进入应用后立即在顶层绑定 EventChannel 的监听器事件处理函数里再按需分发。如果你想只在设置页监听事件在 initState 里开始监听在 dispose 里取消那原生侧事件发出来时如果页面退出就会因为无人处理而丢消息。5.5 关于 flutter impeller 与渲染性能论坛上经常有人问 Flutter 在鸿蒙上能不能用 Impeller。结论是OpenHarmony 分支的引擎目前主要还是用 Skia 渲染。这意味着你依赖 Impeller 的剖面性能优化在鸿蒙上暂时不成立。实测跑口腔护理 App 这种轻量场景完全没问题但如果你的页面有大量复杂的模糊、阴影、纹理叠加性能上要保守一些。同时OpenHarmony 分支在 UI 线程调度上会多一些平台开销。用 DevEco Studio 的 Profiler 工具能直接看 ArkTS 与 Flutter 引擎的线程占用。我实测的结果是Flutter 的 UI 线程不会跟 UIAbility 的主线程共享所以复杂动画不会导致 ArkUI 卡顿这一点可以放心。5.6 提醒功能“到期了没反应”的排查清单提醒是核心功能一旦“到期没反应”影响是致命的。我整理了一份排查顺序作为实战项目中定位问题的速查表排查清单里有一个很容易漏的点模拟器上测试闹钟时模拟器可能把系统时间校准了比如网络自动同步导致你的触发时间计算错乱。用真机测试时把自动时间同步关掉手动调整时间才能真实还原触发场景。6. 打包发布与后续扩展应用开发完成后的最后一个环节是打包。OpenHarmony 的打包流程跟 Android 相似但又不同。你需要在 DevEco Studio 里配置签名证书签名文件跟 Android 的 keystore 不是一回事它用的是 OpenHarmony 自己的证书体系。没有签名证书HAP 是装不上真机的更别提上架到应用市场。打包有 Debug 和 Release 两种Release 包要打开混淆和资源压缩选项。性能调优上我建议 Release 前打一次性能报告在 DevEco Profiler 里重点看 Flutter UI 帧耗时、内存曲线。口腔护理 App 的页面不复杂一般没有性能问题但提醒回调这个链路如果出现长时间事件处理会造成通知延迟。发布的后续扩展方向我个人看好两个一个是接入系统级 Wellness Kit 或 Health Kit把刷牙记录跟健康数据打通另一个是支持多设备流转——早上在手机上设置提醒晚上在平板上也能同步看到记录。这两块都需要用到 OpenHarmony 分布式能力和 Flutter 端分布式数据的适配目前还没完全打通但方向已经很清晰了。最后说一点我在这类跨端项目上的真实体会Flutter for OpenHarmony 的开发体验和成熟度比很多人想象的好但也不能抱着“零改动跨端”的幻想系统能力调用、权限模型、构建链路这三块必然要投入额外成本。这个项目从零到可演示版本大概花了两周时间其中环境准备和提醒调试各占了三分之一。如果能把这几块吃透以后你再接鸿蒙上的 Flutter 项目基本上可以按固定套路出牌技术风险是可控的。