环境搭建指引: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.etsHAR 出口,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.databaseDirpreserveUserDefaults: 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 SDK3.44.9+ohos-0.0.1-canary1(Dart 3.12.2)
DevEco Studio26.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
启动17904240914740不存在9082
计数 2 次 + 写标记17904240914742存在(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 目录里,被清掉了)。

在这里插入图片描述
在这里插入图片描述


八、已知限制

  1. 两次 restartApp() 必须间隔 1 分钟(平台约束,实测见 7.3)。间隔不足时应用被结束且不会重启,调用方必须在交互上规避。
  2. clearData 只清应用沙箱:cacheDir / tempDir / filesDir / preferencesDir / databaseDir 下的内容;系统保存的其它数据(权限授权记录、资产库凭据)不受影响。
  3. preserveKeychain 是空操作:鸿蒙没有独立的 keychain 容器,false 也删不掉 Asset Store Kit 里的凭据。
  4. gc() 是空操作:原生侧无法触发 Dart 的 GC。
  5. terminate: true 会丢失 Dart 内存状态:appRecovery 负责的是"把 ability 重新拉起来",Dart 侧的应用数据需要自己持久化。
  6. 宿主必须显式开启应用恢复框架:没在 AbilityStage 里调用 enableAppRecovery() 时,restartApp() 不会重启,也不会抛异常——这是最容易漏掉的一步。
  7. 示例移除了上游示例的四个依赖: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 比它旧)
适配 TAG1.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 SDK3.44.9+ohos-0.0.1-canary1
Dart3.12.2
DevEco Studio26.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

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐