Flutter for OpenHarmony 实战:三方库 terminate_restart 的鸿蒙化适配指南
环境搭建指引:https://atomgit.com/CPF-Flutter/flutter_samples/blob/master/docs/ohos/getting-started/flutter-oh-env-setup.md
terminate_restart 做的事情听起来很朴素:让应用自己重启,或者只把界面重置一遍。它的 1.1.0 版支持 Android、iOS、Web,没有 OpenHarmony。
这件事在 Android 上是一行 startActivity + exit(0),在 iOS 上是换一个 FlutterViewController;到了鸿蒙,它变成了一个必须先理解平台规则才能动笔的问题——鸿蒙不允许应用随便"自杀再启动",唯一被官方承认的入口是应用恢复框架 appRecovery,而且它还有一条"两次重启必须间隔一分钟"的硬约束。
本文记录把这个库适配到 OpenHarmony 的完整过程:一套比前几篇多一步的选库方法(多出来的那一步筛掉了一个看起来很合适的库)、六步适配流程、三种重启语义在鸿蒙上的落地方式,以及六组实测数据——包括那条"第二次重启只会把应用杀掉"的日志。
适配对象:上游已发布的 terminate_restart 1.1.0(MIT);适配产物 TAG 1.1.0-ohos-1.0.0-beta.1。
一、这个库要解决什么
1.1 上游 API
// main() 里初始化,并用 wrapWithRestart 包住整棵组件树
TerminateRestart.instance.initialize();
runApp(TerminateRestart.wrapWithRestart(child: const MyApp()));
// 重启
await TerminateRestart.instance.restartApp(
options: const TerminateRestartOptions(
terminate: true, // true=进程级重启;false=只重建界面
clearData: false, // 重启前清数据
preserveKeychain: false,
preserveUserDefaults: false,
),
);
// 还有一种带确认弹窗的封装
await TerminateRestart.instance.restartAppWithConfirmation(context);
四种参数合起来对应四种行为,其中 terminate 是决定性的:
| 参数组合 | 预期行为 |
|---|---|
terminate: true | 结束当前进程,重新启动应用 |
terminate: false | 不换进程、不换引擎,只把界面的状态清一遍 |
clearData: true | 重启前清掉应用数据 |
preserveUserDefaults / preserveKeychain | 控制上一条清到什么程度 |
1.2 契约:两条通道,其中一条是反着走的
上游 Dart 侧只做两件事:
static const MethodChannel methodChannel =
MethodChannel('com.ahmedsleem.terminate_restart/restart'); // 正向:Dart -> 原生
final MethodChannel _internalChannel =
const MethodChannel('com.ahmedsleem.terminate_restart/internal'); // 反向:原生 -> Dart
- 正向通道两个方法:
restart(参数是{clearData, preserveKeychain, preserveUserDefaults, terminate},返回bool)、gc(无参); - 反向通道一个方法:
resetToRoot。原生侧要主动调用它,Dart 侧收到后触发整棵组件树重建:
Future<dynamic> _handleInternalMessages(MethodCall call) async {
switch (call.method) {
case 'resetToRoot':
// 默认:通过 wrapWithRestart 触发整棵树换 key 重建
_restartNotifier.value++;
break;
}
}
平台接口的默认实现是 MethodChannelTerminateRestart,而 pubspec 里只有 Android / iOS / Web 三个平台:
android:
package: com.ahmedsleem.terminate_restart
pluginClass: TerminateRestartPlugin
ios:
pluginClass: TerminateRestartPlugin
web:
pluginClass: TerminateRestartWeb
fileName: src/terminate_restart_web.dart
没有 dartPluginClass,也没有任何 defaultTargetPlatform 分支,所以在鸿蒙上 TerminateRestartPlatform.instance 会保持默认的方法通道实现——原生侧把这两条通道接住就行,Dart 一个字都不用改。
二、选库:三道筛之外,还有第四道
2.1 三筛
第一筛:pub.dev 平台列表里有没有 ohos。
[pub.dev] 版本 1.1.0 平台: android, ios, web
没有,通过。
第二筛:上游仓库根目录有没有 *_ohos / *_harmony 兄弟包,pub.dev 上有没有 terminate_restart_ohos。
$ git clone https://gh-proxy.com/https://github.com/sleem2012/terminate_restart.git
$ Get-ChildItem terminate_restart -Force | Select-Object Name
# .git .github android example ios lib test .gitignore .metadata .pubignore
# analysis_options.yaml CHANGELOG.md CONTRIBUTING.md LICENSE README.md pubspec.yaml
$ node -e "...fetch https://pub.dev/api/packages/terminate_restart_ohos..."
terminate_restart_ohos -> 404
没有兄弟包,通过。
第三筛:Dart 入口有没有平台门。
Select-String -Path lib\*.dart,lib\src\*.dart -Pattern "defaultTargetPlatform|Platform\.is|UnsupportedError"
# lib/src/terminate_restart_web.dart: web 专用实现(条件导入)
# 其余文件:无
没有平台门,通过。
2.2 第四道筛:依赖体检(本轮新增)
这一步是踩过坑之后补上的。一个插件在鸿蒙上能不能跑,不只取决于它自己——还取决于它依赖的原生插件有没有鸿蒙实现。Dart 层不允许改动,如果某个依赖在鸿蒙上没人接通道,宿主一调就是 MissingPluginException,你自己写得再完整也没用。
.agents/tools/dep-ohos-check.mjs 把这件事自动化:逐个检查候选包的依赖树,把每个依赖分成"纯 Dart / 自带 ohos 平台 / 有 <包名>_ohos 实现 / 无人适配"四类,最后一类直接判死。
$ node .agents/tools/dep-ohos-check.mjs --file candidates.txt
BLOCKED alarm [android,ios] blocked by: flutter_fgbg[android,ios]
OK terminate_restart [android,ios,web] deps ok: plugin_platform_interface(pure) web(pure)
alarm(9.8k/月)本来是我最看好的候选:闹钟/提醒这个方向在整个社区都没人做过,鸿蒙的 reminderAgentManager 也现成(模拟器上 hidumper -ls 能看到 ReminderAgentService,ohos.permission.PUBLISH_AGENT_REMINDER 还是 system_grant、不需要弹窗授权)。但它依赖 flutter_fgbg,而 pub.dev 上没有 flutter_fgbg_ohos ——除非顺手再适配一个库,否则这个方案从第一步就是死的。于是它在写下第一行 ArkTS 之前就被筛掉了。
terminate_restart 的依赖只有 plugin_platform_interface 和 web,都是纯 Dart,通过。
2.3 在线查重,以及一个 403 的坑
本地清单(check-dedup.mjs 依赖的那份数据)滞后一周以上,这几轮反复踩到,所以直接打实时 API:
$ node .agents/tools/live-dedup.mjs terminate_restart
pub.dev : v1.1.0 platforms=[android,ios,web]
atomgit : hxa-flutter/terminate_restart -> status=403 ← 状态未知
verdict : CHECK-MANUALLY
403 不能当成"没人适配",也不能当成"存在"。用组织仓库列表交叉验证一下:
// 组织仓库列表是权威枚举(分页拉全量)
GET https://atomgit.com/api/v5/orgs/hxa-flutter/repos?page=1&per_page=100&type=all
// status 200, count 100
// terminate match: (none)
hxa-flutter 的仓库列表里没有任何名字含 terminate 的仓库;同一组织下已存在的仓库(例如 usage_stats)contents API 返回 200,而 terminate_restart 返回 403。也就是说这个组织的 contents API 对不存在的仓库会返回 403,把它当"未知"再用仓库列表兜底才对。四个组织逐一核过,确认无人适配。
三、六步适配流程
第一步:把上游同步到 AtomGit
在 oh-flutter 下建同名空仓库(不加 README):
# 必须建在组织下:POST /user/repos 会静默建到个人命名空间
node .agents\tools\atomgit.mjs create oh-flutter terminate_restart "<中文描述>"
node .agents\tools\atomgit.mjs verify oh-flutter terminate_restart # 回读校验中文没被 ASCII 编码毁掉
第二步:本地克隆,并确认"以哪个版本为基线"
git clone https://gh-proxy.com/https://github.com/sleem2012/terminate_restart.git tr_work
cd tr_work
git log --oneline -1 # 5bab101 fix: iOS UI restart, web double-tap, add wrapWithRestart, clean Android
git tag # v1.1.0
git describe --tags # v1.1.0-3-g5bab101 ← 当前提交比 tag 新 3 个提交
这里有一个必须停下来核对的地方:git tag v1.1.0 与 pub.dev 上发布的 1.1.0 不是同一份代码。tag 里还没有 wrapWithRestart(那正是 tag 之后的提交加进去的):
# tag 版本的 lib/terminate_restart.dart:没有 wrapWithRestart
# pub.dev 1.1.0 的同一文件:有 wrapWithRestart 与 _RestartWrapper
审核标准要求"鸿蒙化目标库版本为上游最新版本",指的是发布版。所以基线取 main(5bab101),并核对它与 pub.dev 归档一致:
// 逐文件比较,忽略换行符差异
identical after EOL normalisation: 7 / 7
still different: []
结论:以 main 分支 5bab101 为基线 = pub.dev 上已发布的 1.1.0。这一点写进了两份 README,免得后来者对着 tag 找不到 wrapWithRestart 而困惑。
第三步:建分支,用框架命令补出鸿蒙化目录
git checkout -b feat/ohos_terminate_restart_1.1.0 main
flutter create -t plugin --platforms ohos .
这一步连着报了两个错,都要先解决:
# 报错一
The requested template type 'plugin' doesn't match the existing template type of 'package'.
# 原因:.metadata 里写着 project_type: package,而 pubspec 声明的是插件平台
# 改法:project_type: package -> plugin(顺手修正了上游这处不一致)
# 报错二
Ambiguous organization in existing files: {com.ahmedsleem, com.ahmedsleem.terminate_restart}.
The --org command line argument must be specified to recreate project.
# 改法:flutter create -t plugin --platforms ohos --org com.ahmedsleem .
第四步:适配过程(新增了什么、为什么)
新增两个文件 + 一处声明:
| 文件 | 作用 |
|---|---|
ohos/index.ets | HAR 出口,export { default } from './src/main/ets/components/plugin/TerminateRestartPlugin' |
ohos/src/main/ets/components/plugin/TerminateRestartPlugin.ets | 插件本体:正向 restart / gc,反向 resetToRoot |
pubspec.yaml | 新增 ohos: pluginClass: TerminateRestartPlugin |
另外示例工程里加了一个不属于插件、但宿主必须有的文件:example/ohos/entry/src/main/ets/abilitystage/ExampleAbilityStage.ets,并在 module.json5 的 module 级 srcEntry 上登记——原因见第五节。
第五步:补全额外文件
README.OpenHarmony.md / README.OpenHarmony_CN.md / CHANGELOG.OpenHarmony.md,并在根 README.md 的 Platform Support 列表里补上 OpenHarmony。
第六步:推送并打 TAG
git add -A
git commit -F commit-msg.txt
git remote add atomgit https://atomgit.com/oh-flutter/terminate_restart.git
git push atomgit HEAD:main
git push atomgit feat/ohos_terminate_restart_1.1.0
git tag -a 1.1.0-ohos-1.0.0-beta.1 -m "terminate_restart OpenHarmony 适配 1.1.0-ohos-1.0.0-beta.1"
git push atomgit 1.1.0-ohos-1.0.0-beta.1
提交前照例清空 example/ohos/build-profile.json5 里的 signingConfigs(devecocli signature generate 会写入明文密码),推完再回读远端树确认根 README 是 100644 普通文件:
$ git fetch atomgit main; git ls-tree FETCH_HEAD
100644 blob 8784977b... README.md
100644 blob 81e1e11b... README.OpenHarmony.md
100644 blob b2a90352... README.OpenHarmony_CN.md
100644 blob 0f1e9f11... CHANGELOG.OpenHarmony.md
040000 tree bea7c2d7... ohos
推送成功的仓库首页:

四、两条通道怎么接
4.1 代码写在哪个文件
terminate_restart/
├── ohos/
│ ├── index.ets # HAR 出口(flutter create 生成)
│ ├── oh-package.json5 # 包名/版本/协议(生成后手改)
│ └── src/main/
│ ├── module.json5 # HAR 类型声明(flutter create 生成)
│ └── ets/components/plugin/
│ └── TerminateRestartPlugin.ets # ★ 插件本体,本次适配的实现都在这
├── example/
│ ├── lib/main.dart # ★ 鸿蒙演示页(本次重写)
│ └── ohos/
│ └── entry/src/main/
│ ├── module.json5 # ★ 登记 AbilityStage(宿主侧改动)
│ └── ets/abilitystage/
│ └── ExampleAbilityStage.ets # ★ 开启 appRecovery(宿主侧新增)
└── pubspec.yaml # ★ 新增 ohos: pluginClass
4.2 正向通道:两个方法,其中一个故意留空
onMethodCall(call: MethodCall, result: MethodResult): void {
if (call.method === METHOD_RESTART) {
const terminate: boolean = call.argument('terminate') === true;
const clearData: boolean = call.argument('clearData') === true;
const preserveUserDefaults: boolean = call.argument('preserveUserDefaults') === true;
const preserveKeychain: boolean = call.argument('preserveKeychain') === true;
this.handleRestart(terminate, clearData, preserveUserDefaults, preserveKeychain, result);
} else if (call.method === METHOD_GC) {
// 原生侧没有任何办法触发 Dart 的 GC(上游 Android 实现同样没有实现这个方法)
result.success(null);
} else {
result.notImplemented();
}
}
gc() 值得说一句:上游 Android 侧压根没实现它(else -> result.notImplemented()),Dart 侧 gc() 里又 catch 掉了异常。鸿蒙这边返回一个成功的空操作,比返回 notImplemented 少一条无意义的异常日志;但它确实什么都没做,这点在 README 的已知限制里写清楚了。
4.3 反向通道:原生主动通知 Dart 重建
onAttachedToEngine(binding: FlutterPluginBinding): void {
const messenger = binding.getBinaryMessenger();
this.channel = new MethodChannel(messenger, RESTART_CHANNEL);
this.channel.setMethodCallHandler(this);
// 反向通道只用于向 Dart 发消息,不设置 handler
this.internalChannel = new MethodChannel(messenger, INTERNAL_CHANNEL);
}
private notifyResetToRoot(): void {
this.internalChannel?.invokeMethod('resetToRoot', null);
}
引擎的 MethodChannel.invokeMethod(method: string, args: Any, callback?: MethodResult) 直接支持这种"原生当客户端"的用法,不需要额外的 sender 抽象。
五、三种"重启"在鸿蒙上怎么落地
5.1 terminate: true:唯一的官方入口是 appRecovery
鸿蒙里"应用结束自己"和"应用重新起来"是两件事:context.terminateSelf() 只结束当前 ability(进程可能还活着),而真正意义上的"自杀后重启"只有 appRecovery 这条路。它由三个 API 组成:
appRecovery.enableAppRecovery(restart?, saveOccasion?, saveMode?): void; // 只能在 AbilityStage 里开
appRecovery.setRestartWant(want: Want): void; // 指定重启后拉起哪个 ability
appRecovery.restartApp(): void; // 重启当前进程
插件侧的调用顺序:
private performProcessRestart(): void {
const context = ability.context as common.UIAbilityContext;
appRecovery.saveAppState(context); // 给宿主的 onSaveState 一次机会
const want: Want = {
bundleName: context.abilityInfo.bundleName,
abilityName: context.abilityInfo.name,
};
appRecovery.setRestartWant(want); // 钉死目标,不让系统按"前台且支持恢复"的规则挑
appRecovery.restartApp();
}
enableAppRecovery() 必须在宿主的 AbilityStage 里调用,插件是够不着的——AbilityStage 的 onCreate() 早于任何 ability,而插件只能在引擎挂载之后才有机会执行。所以插件交付物里包含了一处宿主要配合的改动:
// example/ohos/entry/src/main/ets/abilitystage/ExampleAbilityStage.ets
export default class ExampleAbilityStage extends AbilityStage {
onCreate(): void {
appRecovery.enableAppRecovery(
appRecovery.RestartFlag.ALWAYS_RESTART,
appRecovery.SaveOccasionFlag.SAVE_WHEN_ERROR,
appRecovery.SaveModeFlag.SAVE_WITH_FILE,
);
}
}
并在 module.json5 的 module 级 srcEntry 登记(不是 abilities 里的那个 srcEntry):
{
"module": {
"name": "entry",
"mainElement": "EntryAbility",
"srcEntry": "./ets/abilitystage/ExampleAbilityStage.ets",
// ...
}
}
5.2 先回执,再重启
restartApp() 会把进程立刻杀掉,方法通道的回执可能还没出站。上游 Android 的做法是先 result.success(true) 再 startActivity + exit(0);鸿蒙这边同样处理,只是多留了一拍:
result.success(true);
setTimeout((): void => {
this.performProcessRestart();
}, RESTART_DELAY_MS); // 100ms
这个细节在实测里很直观:hilog 里 restart requested → restartApp() -> ... → 系统日志 reason: appRecovery → 新进程的 channels registered,四条日志的时间戳依次相隔几十毫秒。
5.3 terminate: false:不动进程,让 Dart 换一棵树
上游 iOS 的做法是:重建 FlutterViewController(复用同一个引擎),完成后调用反向通道的 resetToRoot,Dart 侧 wrapWithRestart 把整棵组件树换一个 ValueKey 重建。鸿蒙直接复用这套语义:
if (!terminate) {
result.success(true); // 与上游 iOS 的 performUIRestart 一致
this.notifyResetToRoot();
return;
}
为什么不在鸿蒙上也"重建 ability"?因为那等于换一个 Flutter 引擎(main() 会重跑、Dart 内存状态全丢),而这恰恰是 terminate: false 想要避免的——它的卖点就是"轻"。复用引擎、只重建组件树,才和设备无关地表达出这个语义。
5.4 clearData:清沙箱,不是系统级清数据
鸿蒙没有给三方应用用的"系统级清除应用数据"接口,所以只能清自己沙箱里的目录。与上游 Android 的语义对齐:
| 目录 | 是否清理 |
|---|---|
context.cacheDir、context.tempDir | 总是清 |
context.filesDir、context.preferencesDir、context.databaseDir | preserveUserDefaults: false 时清 |
@ohos.security.asset(资产库)里的凭据 | 不清理 |
实现上逐个删目录下的子项、保留目录本身(应用重启后还要往里写):
const children: string[] = fs.listFileSync(dir);
for (const child of children) {
const childPath: string = `${dir}/${child}`;
// rmdir 会连子目录一起删;文件用它也能删(文档建议文件用 unlink)
if (fs.statSync(childPath).isDirectory()) {
fs.rmdirSync(childPath);
} else {
fs.unlinkSync(childPath);
}
}
preserveKeychain 在鸿蒙上没有对应容器:通过 Asset Store Kit 保存的凭据不在上述目录里,天然不会被清掉。于是这个参数是空操作,而且**preserveKeychain: false 也删不掉资产库里的凭据**——这是语义上的一个真实缺口,写进已知限制而不是假装支持。
六、编译与构建踩坑
6.1 .metadata 的 project_type 与实际不符
第一次执行 flutter create -t plugin --platforms ohos . 直接失败:
The requested template type 'plugin' doesn't match the existing template type of 'package'.
上游 .metadata 里写着 project_type: package,但 pubspec.yaml 声明了三个平台的插件实现。把它改成 plugin 才能继续。这类"上游元数据与事实不符"的情况只能靠读报错信息定位——错误信息本身已经把原因说清楚了,别急着重试命令。
6.2 --org 是必填的
Ambiguous organization in existing files: {com.ahmedsleem, com.ahmedsleem.terminate_restart}.
工程里同时存在父组织和子组织两种包名前缀,工具不知道该用哪个。显式给 --org com.ahmedsleem 即可。
6.3 模板垃圾这次进了 lib/ 和 test/
上一轮的模板垃圾是平台目录,这次它还往 Dart 源码目录里塞了东西。清理前先看看多出了什么(用 git 自己的清单,别靠肉眼):
git ls-files --others --exclude-standard
本次多出来的东西:
| 路径 | 处理 |
|---|---|
ohos/、example/ohos/ | 保留 |
android/.gitignore、android/*.gradle.kts、android/src/test/ | 删(插件本来就没有 Android 原生目录,Android 实现是 Kotlin,见 android/src/main/kotlin/...) |
ios/.gitignore、ios/Assets/、ios/Resources/ | 删 |
lib/terminate_restart_method_channel.dart、lib/terminate_restart_web.dart | 删(模板同名文件,会污染包的公开 API) |
test/terminate_restart_method_channel_test.dart | 删(模板测试,引用的正是上面那两个文件) |
example/android/、example/ios/Runner/SceneDelegate.swift、example/web/、example/integration_test/ | 删 |
删的时候只删 git ls-files --others 里不含 ohos/ 的那部分,就不会误伤上游已有文件:
$untracked = git ls-files --others --exclude-standard
$junk = $untracked | Where-Object { $_ -notmatch '(^|/)ohos/' }
foreach ($f in $junk) { Remove-Item -LiteralPath $f -Force }
上一轮我在这里翻过车:直接用
Remove-Item -Recurse删example/android/,连上游已被 git 跟踪的文件一起删了,git status里冒出一大片D。用上面这条从git ls-files --others出发的写法就不会有这个问题——它天生只包含未被跟踪的文件。
6.4 其它(与上一轮相同的四条)
- release 构建下插件里的
Log.i不会输出:引擎的Log类默认级别是WARN,验证一律用flutter build hap --debug --target-platform ohos-x64;hilog 的 tag 固定是Flutter,过滤要按消息内容而不是 tag; - ABI 对齐:模拟器是
ohos-x64,构建必须带--target-platform ohos-x64,否则安装报code:9568347; PUB_CACHE与工程同盘:$env:PUB_CACHE = "E:\pub-cache",否则报The srcPath is not a relative path;- 签名:先在
example/ohos下devecocli signature generate,再构建才会得到entry-default-signed.hap;提交前清空signingConfigs。
七、真机(模拟器)验证
7.1 验证环境
| 项 | 值 |
|---|---|
| Flutter for OpenHarmony SDK | 3.44.9+ohos-0.0.1-canary1(Dart 3.12.2) |
| DevEco Studio | 26.0.0.621(OpenHarmony SDK API 26) |
| 设备 | Pura X View 模拟器,HarmonyOS 7.0.0(26.0.0) Beta2,ohos-x64 |
| 构建 | flutter build hap --debug --target-platform ohos-x64 |
"重启"这种东西不好用截图证明,所以示例页把三个可观测量摆在一起:
- 进程令牌:
main()里生成的毫秒时间戳,main()一个进程只跑一次 → 只有进程级重启才会变; - 计数器:存在
State里 → 两种重启都会清零; - 沙箱标记文件:写在
Directory.systemTemp里 → 用来判断clearData到底清掉了什么(实测这个路径落在/data/storage/el2/base/haps/entry/cache/,正好在cacheDir里)。
启动后的样子:进程令牌已生成、计数器 0、标记文件不存在。

7.2 六组实测数据
| 操作 | 进程令牌 | 计数器 | 标记文件 | 插件记录的 pid |
|---|---|---|---|---|
| 启动 | 1790424091474 | 0 | 不存在 | 9082 |
| 计数 2 次 + 写标记 | 1790424091474 | 2 | 存在(written@1790424091474) | 9082 |
仅重建界面(terminate: false) | 1790424091474(不变) | 0 | 存在 | 9082(不变) |
完整重启(terminate: true) | 1790424142472(变) | 0 | 存在(上一进程写的) | 32223(新) |
| 60 秒内再按完整重启 | — | — | — | 进程被杀、未重启 |
清空数据并重启(clearData: true) | 1790424257427(变) | 0 | 不存在 | 741 → 2363(新) |
"仅重建界面"这一行是这套设计的关键证据:进程令牌与 pid 都不变,只有计数器清零。也就是说引擎没重建、main() 没重跑,变化只发生在组件树这一层。

"完整重启"之后,进程令牌与 pid 同时变化:

7.3 那条一分钟的约束
第二次重启发生在第一次之后 24 秒,插件的调用本身是成功的,但系统在 AMS 里拦下了:
10:02:41.012 32223 TerminateRestartPlugin --> restart requested: terminate=true clearData=false ...
10:02:41.159 32223 TerminateRestartPlugin --> restartApp() -> .../EntryAbility, pid=32223
10:02:41.164 817 E C01336/AMS: [ABMS11043]ScheduleRecoverAbility appRecovery recover more once in one minute, kill app(32223)
10:02:41.170 817 I C01311/AppMS: [AMSI9106]kill appRecovery NotifyApp bundleName: com.ahmedsleem.terminate_restart_example, faultType: -1, pid: 32223
日志里的 ABMS11043 就是"一分钟内重复恢复",结果是应用被结束、没有重新起来。这条约束来自 appRecovery.restartApp() 的 API 文档,实测完全一致。对调用方的含义很直接:"重启"不能做成可以连点的按钮,要么按钮点一次就禁用,要么在界面上说明需要等待一分钟。
7.4 清空数据:目录级证据
clearData: true 时插件把每个目录清了多少项都打到日志里:
TerminateRestartPlugin --> restart requested: terminate=true clearData=true preserveUserDefaults=false preserveKeychain=false
TerminateRestartPlugin --> wiped /data/storage/el2/base/haps/entry/cache (3 entries)
TerminateRestartPlugin --> wiped /data/storage/el2/base/haps/entry/temp (0 entries)
TerminateRestartPlugin --> wiped /data/storage/el2/base/haps/entry/files (1 entries)
TerminateRestartPlugin --> wiped /data/storage/el2/base/haps/entry/preferences (0 entries)
TerminateRestartPlugin --> wiped /data/storage/el2/database/entry (0 entries)
TerminateRestartPlugin --> sandbox cleared
TerminateRestartPlugin --> restartApp() -> com.ahmedsleem.terminate_restart_example/EntryAbility, pid=741
重启后新进程的日志(pid 变了、AbilityStage 又跑了一遍、插件重新注册):
ExampleAbilityStage --> appRecovery enabled
FlutterEngineCxnRegistry --> Adding plugin: TerminateRestartPlugin
TerminateRestartPlugin --> terminate_restart channels registered, pid=2363
界面上则是:进程令牌换了、计数器 0、标记文件消失(它就在 cache 目录里,被清掉了)。


八、已知限制
- 两次
restartApp()必须间隔 1 分钟(平台约束,实测见 7.3)。间隔不足时应用被结束且不会重启,调用方必须在交互上规避。 clearData只清应用沙箱:cacheDir/tempDir/filesDir/preferencesDir/databaseDir下的内容;系统保存的其它数据(权限授权记录、资产库凭据)不受影响。preserveKeychain是空操作:鸿蒙没有独立的 keychain 容器,false也删不掉 Asset Store Kit 里的凭据。gc()是空操作:原生侧无法触发 Dart 的 GC。terminate: true会丢失 Dart 内存状态:appRecovery负责的是"把 ability 重新拉起来",Dart 侧的应用数据需要自己持久化。- 宿主必须显式开启应用恢复框架:没在 AbilityStage 里调用
enableAppRecovery()时,restartApp()不会重启,也不会抛异常——这是最容易漏掉的一步。 - 示例移除了上游示例的四个依赖:
flutter_animate/font_awesome_flutter/google_fonts只服务于首页动效与在线字体(鸿蒙上会去下载字体);shared_preferences的鸿蒙实现单独发布在shared_preferences_ohos(pub.dev 上有,v2.2.0,平台列表就是ohos),而shared_preferences自己并没有为 ohos 做default_package背书,pub get不会自动带上它——要用就得显式写进依赖(这也是鸿蒙生态里用官方插件的一个通例)。示例里没有这个需要,所以直接不用。
九、常见问题
Q1:为什么鸿蒙上不能用 terminateSelf() + 重新 startAbility() 来实现重启?
terminateSelf() 只会结束当前 ability,进程不一定退出;即便进程退出,"谁来把我再拉起来"也没有答案——进程都死了,没人能执行下一句启动命令。鸿蒙对此给出的方案就是应用恢复框架:appRecovery.restartApp() 由系统负责杀掉进程并按恢复流程重新拉起 ability,还能把保存过的状态通过 want.param 交回 onCreate。这也是它要求"必须先在 AbilityStage 里开启恢复框架"的原因——系统得先知道这个应用愿意接受恢复。
Q2:为什么 setRestartWant() 不能省?
API 文档写明:不指定时,系统按"当前前台且支持恢复的 ability"来挑,一个都没挑中就不启动。插件里用 context.abilityInfo 里的 bundleName / name 明确钉死宿主当前的 ability,行为才可预期。
Q3:为什么 terminate: false 不重启 ability,只让 Dart 重建组件树?
因为这正是这个参数的语义——轻量级重置。上游 iOS 就是这么做的(复用引擎、换 FlutterViewController、回调 resetToRoot);鸿蒙若改成重建 ability,等于换一个 Flutter 引擎,main() 会重跑、Dart 内存状态全丢,和 terminate: true 就没区别了。实测里"仅重建界面"这一行 pid 与进程令牌都不变,恰好证明了它没有碰引擎。
Q4:clearData 为什么不是"系统级清除数据"?
那是系统权限的能力,三方应用没有这个接口。鸿蒙能给到应用的是自己的沙箱目录,所以实现与上游 Android 对齐:清 cacheDir / tempDir / filesDir / preferencesDir / databaseDir,并且用 preserveUserDefaults 控制后三个。preserveKeychain 在鸿蒙上没有对应容器,只能空操作——false 也删不掉资产库凭据,这一点在 README 的已知限制里明说了。
Q5:gc() 在鸿蒙上做了什么?
什么都没做,返回成功而已。Dart 的 GC 由虚拟机自己决定,原生侧没有触发接口;上游 Android 实现同样没实现这个方法(直接 notImplemented())。之所以不照抄 notImplemented,是因为 Dart 侧 gc() 本来就会 catch 异常,返回成功能少一条无意义的错误日志。
Q6:为什么重启前要延迟 100 毫秒?
restartApp() 会立刻结束进程,方法通道的回执有可能还没送到 Dart。上游 Android 是"先回执再 startActivity + exit(0)",鸿蒙这边同样是先 result.success(true),再用 setTimeout 把真正的重启推到下一拍。副作用是:Dart 侧 await restartApp() 大概率永远等不到这个 future 完成——但那时进程已经不在了,等待本身也没有意义。
Q7:是不是所有鸿蒙应用都能用这个插件?
不是,宿主必须做两件事:在 AbilityStage 的 onCreate() 里调用 appRecovery.enableAppRecovery(...),并把该 AbilityStage 登记到 module.json5 的 module 级 srcEntry。没做的话 restartApp() 既不会重启也不会报错,只能从"应用直接消失了"这个现象倒推。这两步都写在 README 的"宿主应用必须先做一件事"一节。
Q8:为什么不直接用 git tag v1.1.0 做基线?
因为那个 tag 比 pub.dev 上发布的 1.1.0 旧:tag 之后还有 3 个提交(add wrapWithRestart、iOS UI 重启修复、Web 双击修复),而发布版包含这些改动。对着 tag 适配会得到一个"编译不过"的示例——TerminateRestart.wrapWithRestart 在 tag 里根本不存在,我第一次构建就是这么失败的。判定方法是拿包文件与 pub.dev 归档逐文件比对(忽略换行符),确认基线一致再动手。
十、本篇用到的库
| 项 | 值 |
|---|---|
| 适配仓库 | https://atomgit.com/oh-flutter/terminate_restart |
| 上游仓库 | https://github.com/sleem2012/terminate_restart |
| 上游版本 | 1.1.0(MIT,对应 main 5bab101;git tag v1.1.0 比它旧) |
| 适配 TAG | 1.1.0-ohos-1.0.0-beta.1 |
| 适配分支 | feat/ohos_terminate_restart_1.1.0 |
| 平台目录 | ohos/(插件 HAR)、example/ohos/(示例工程,含 AbilityStage) |
| 接口 | TerminateRestart.instance.restartApp(options)、gc()、wrapWithRestart() |
依赖写法(写死 TAG,不跟分支):
dependencies:
terminate_restart:
git:
url: https://atomgit.com/oh-flutter/terminate_restart.git
ref: 1.1.0-ohos-1.0.0-beta.1
示例工程通过 path: ../ 引用适配库,另外宿主侧必须自己加一个 AbilityStage(见第五节)。
验证环境
| 项 | 值 |
|---|---|
| Flutter for OpenHarmony SDK | 3.44.9+ohos-0.0.1-canary1 |
| Dart | 3.12.2 |
| DevEco Studio | 26.0.0.621(OpenHarmony SDK API 26) |
| 设备 | Pura X View 模拟器,HarmonyOS 7.0.0(26.0.0) Beta2,ohos-x64 |
| 构建产物 | example/build/ohos/hap/entry-default-signed.hap |
复现命令
# 1. 构建(PUB_CACHE 必须与工程同盘;模拟器是 ohos-x64)
$env:PUB_CACHE = "E:\pub-cache"
cd example
flutter pub get
flutter build hap --debug --target-platform ohos-x64
# 2. 安装并启动
hdc shell power-shell wakeup
hdc install -r build/ohos/hap/entry-default-signed.hap
hdc shell aa start -a EntryAbility -b com.ahmedsleem.terminate_restart_example
# 3. 依次点击:计数器 +1 ×2 → 写入沙箱标记文件 → 仅重建界面 → 完整重启
# (两次"完整重启"之间必须间隔一分钟以上)
hdc shell snapshot_display -f /data/local/tmp/tr.jpeg
hdc file recv /data/local/tmp/tr.jpeg .
# 4. 取原生日志(含 pid 变化与 ABMS11043)
hdc shell hilog -x | Select-String "TerminateRestartPlugin|appRecovery"
欢迎加入 CPF-Flutter 鸿蒙社区:https://atomgit.com/CPF-Flutter
更多推荐



所有评论(0)