查看: 99|回复: 3

鸿蒙UIAbility备份恢复实战:后台杀进程数据保活与页面栈恢复

[复制链接]
发表于 2 小时前 | 显示全部楼层 |阅读模式
在做另一个功能时,我发现鸿蒙还有一个叫“UIAbility 备份恢复”的机制,乍一看和 appRecovery 挺像的,都是挂了之后恢复数据。但搞了一下午才发现这两个东西完全不是一回事。appRecovery 侧重的是“崩溃时保命”,而 UIAbility 备份恢复侧重的是“后台被杀后保数据”。而且这俩的 API 调用方式、数据存储位置、触发场景都不一样。这篇文章就把 UIAbility 备份恢复的来龙去脉讲清楚,顺便把和 appRecovery 的区别也说透。

什么时候会触发备份恢复
官方文档就几句话,但实际理解起来有点绕。我用自己的话翻译一下。

备份触发条件
应用在后台运行的时候,如果因为下面这些原因被系统干掉,才会触发备份:
- 系统资源管控(内存不够了把你杀了)
- 进程被 kill(比如系统做了进程清理)
- 应用异常崩溃(JS Crash、C++ 异常之类的)

说白了就是“非自愿死亡”才备份。如果你自己主动调 terminateSelf 退出,或者用户在多任务中心滑掉卡片,不触发备份。

恢复触发条件
恢复就更挑场景了。应用被 kill 之后,下次冷启动进来的时候,系统才会自动恢复之前备份的数据。

什么情况不触发恢复?官方说了两条:
- 应用正常关闭(比如用户主动退出),不触发恢复
- 通过 startAbility 或者点图标正常启动的,不触发恢复

等等,这里有个让人懵的地方——应用被杀了之后重新启动,不就是冷启动吗?这难道不是点图标启动?对,确实是的。所以实际逻辑是这样的:应用被系统杀掉了 -> 用户再次点击图标打开应用 -> 此时是冷启动,但因为“正常启动不触发恢复”,你可能会觉得恢复没生效。

关键点:系统判定“是否恢复”的依据不是启动方式,而是上次退出是否备份了数据。如果 onSaveState 被调用了且保存了数据,下次冷启动不管什么方式进来的,系统都会把数据塞到 want.parameters 里。但页面栈的恢复只在特定场景下生效——比如通过任务列表恢复。这个区别后面讲运行机制的时候再细说。

踩坑:别以为开启了备份恢复就万事大吉。我一开始以为只要调了 setRestoreEnabled(true),应用被杀后重启就能恢复到之前的页面。结果测试发现页面回到了首页,查了半天文档才发现页面栈恢复是有条件的——它依赖任务保留机制。如果你的应用设置了 removeMissionAfterTerminate: true,备份恢复直接不生效。

运行机制
数据怎么备份的
当应用在后台被系统杀掉时,系统会触发 UIAbility 的 onSaveState 回调。你在这个回调里把需要保存的数据塞到 wantParam 这个 Record<string, Object> 里,系统会帮你序列化存到沙箱里。签名长这样:
  1. onSaveState(reason: AbilityConstant.StateType, wantParam: Record<string, Object>): AbilityConstant.OnSaveResult
复制代码
参数说明:
- reason:备份的原因,目前主要是 StateType.APP_LOW_MEMORY 之类的
- wantParam:你要保存的数据,键值对往里塞就行
- 返回值:返回 OnSaveResult.ALL_AGREE 表示同意备份

这里有个细节:wantParam 最终会被存储到 Want 的 parameters 字段里,所以恢复的时候是从 want.parameters 读的。

数据怎么恢复的
恢复分两部分:
- 业务数据:在 onCreate 生命周期里,从 want.parameters 中读取你之前保存的数据。因为冷启动的时候系统会把之前备份的 Want 传进来。
- 页面栈:在 onWindowStageCreate 生命周期中恢复。系统会自动恢复之前保存的页面栈信息,所以你不需要手动做任何操作——前提是条件满足(没有设置 removeMissionAfterTerminate、设备支持任务保留等)。

不过说实话,页面栈恢复这个东西效果有限。我之前测试的时候发现它只能恢复页面导航栈,但页面内部的状态(比如滚动位置、输入框内容、Tab 选中状态)并不会自动恢复。这些需要你自己在 onSaveState 里保存,然后在页面初始化时手动恢复。

完整的生命周期流程:
正常情况:
  1. 冷启动 -> onCreate -> onWindowStageCreate -> onForeground -> 用户操作 -> onBackground -> 被杀
复制代码
有备份恢复的情况下:
  1. 被杀 (触发 onSaveState) -> ...
  2. 下次冷启动 -> onCreate(从 want.parameters 读数据) -> onWindowStageCreate(恢复页面栈) -> onForeground
复制代码

约束限制
这块不仔细看文档的话很容易踩坑,我列几个重要的。

数据大小限制:200KB
备份数据存储在 Want 的 parameters 里,序列化之后的体积不能超过 200KB。200KB 是什么概念?如果你存一屏幕的文本数据可能还行,但如果你想把一张 Base64 图片塞进去——别想了,肯定超。我当时写了个 demo,把用户编辑的富文本内容(包含多张图片的 Base64)存到 wantParam 里,结果一跑就发现数据没恢复。排查了半天,日志里也没报错。后来才发现是超限了,系统默默地丢弃了数据。

保存期限:7 天
备份数据是以文件形式存在应用的沙箱路径里的,保留 7 天。7 天后系统会自动清理。这意味着如果你用户 7 天没打开你的应用,回来之后数据就没了。不过这也很合理——7 天都没打开的 app,估计用户早就不 care 那点数据了。

重启设备不支持还原
这一点真的坑。如果你的手机重启了,之前的备份数据就全没了。我一开始不理解,后来想到可能是备份数据存在内存文件系统里,重启就丢了。但官方没有明确说存在哪里,只说了不支持重启设备后还原。经验之谈:如果你的应用有非常重要的数据,别依赖这个备份恢复机制。它更适合保存“没了会有点麻烦但也不是不能忍”的临时状态,比如用户正在编辑的表单内容、上次浏览的位置等。真正的关键数据还是得自己做持久化。

removeMissionAfterTerminate 冲突
这个前面提到了,如果你的 module.json5 里设置了 removeMissionAfterTerminate: true,备份恢复机制不生效。为什么呢?因为备份恢复依赖任务保留机制(mission),如果 Ability 退出时就把任务记录删了,系统就无从恢复。所以如果你同时用了备份恢复和 removeMissionAfterTerminate,要注意这两个功能是互斥的。
  1. // module.json5 - 错误配置
  2. {
  3.   "abilities": [
  4.     {
  5.       "name": "EntryAbility",
  6.       "removeMissionAfterTerminate": true, // 这个会禁用备份恢复
  7.       // ...
  8.     }
  9.   ]
  10. }
复制代码

UIExtensionAbility 不支持
这个好理解,UIExtensionAbility 没有自己的页面栈,本身就是附属于其他 Ability 的,所以不支持备份恢复。

接口说明
备份恢复的核心接口就一个:
  1. setRestoreEnabled(enabled: boolean): void
复制代码
这个方法是 UIAbilityContext 提供的,从 API 14 开始支持。调用时机要求:必须在 onForeground 之前调用,一般放在 onCreate 里。
  1. import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
  2. export default class EntryAbility extends UIAbility {
  3.   onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
  4.     // 必须在 onForeground 前调用
  5.     this.context.setRestoreEnabled(true);
  6.     // 其他初始化...
  7.   }
  8. }
复制代码
接口就这么简单,一个 boolean 参数,开了就是开了,关了就是关了。

开发步骤
第一步:启用备份恢复
在 EntryAbility 的 onCreate 里调用 setRestoreEnabled(true):
  1. // entry/src/main/ets/entryability/EntryAbility.ets
  2. import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
  3. import { hilog } from '@kit.PerformanceAnalysisKit';
  4. const DOMAIN = 0x0000;
  5. export default class EntryAbility extends UIAbility {
  6.   onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
  7.     // 启用 UIAbility 备份恢复
  8.     this.context.setRestoreEnabled(true);
  9.     hilog.info(DOMAIN, 'Demo', 'UIAbility backup restore enabled');
  10.     // 其他初始化...
  11.   }
  12. }
复制代码

第二步:保存数据
实现 onSaveState 方法,把需要恢复的数据存到 wantParam 里:
  1. // entry/src/main/ets/entryability/EntryAbility.ets
  2. import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
  3. import { hilog } from '@kit.PerformanceAnalysisKit';
  4. const DOMAIN = 0x0000;
  5. export default class EntryAbility extends UIAbility {
  6.   // ... onCreate ...
  7.   onSaveState(reason: AbilityConstant.StateType, wantParam: Record<string, Object>): AbilityConstant.OnSaveResult {
  8.     hilog.info(DOMAIN, 'Demo', 'onSaveState triggered, reason: %{public}s', JSON.stringify(reason));
  9.     // 保存用户正在编辑的数据
  10.     // 假设这些数据来自 AppStorage 或页面传入
  11.     wantParam['editContent'] = AppStorage.get<string>('draftContent') ?? '';
  12.     wantParam['scrollPosition'] = AppStorage.get<number>('scrollPosition') ?? 0;
  13.     wantParam['selectedTab'] = AppStorage.get<number>('selectedTabIndex') ?? 0;
  14.     return AbilityConstant.OnSaveResult.ALL_AGREE;
  15.   }
  16. }
复制代码
注意这里 reason 的类型是 AbilityConstant.StateType,常见的值包括:StateType.APP_LOW_MEMORY(内存不足被回收)、StateType.WINDOW_VISIBILITY_CHANGE(窗口可见性变化)等。

第三步:恢复数据
在 onCreate 里从 want.parameters 读取备份的数据:
  1. // entry/src/main/ets/entryability/EntryAbility.ets
  2. import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
  3. import { hilog } from '@kit.PerformanceAnalysisKit';
  4. const DOMAIN = 0x0000;
  5. export default class EntryAbility extends UIAbility {
  6.   onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
  7.     this.context.setRestoreEnabled(true);
  8.     // 检查是否有备份恢复数据
  9.     if (want && want.parameters) {
  10.       const editContent = want.parameters['editContent'] as string;
  11.       const scrollPosition = want.parameters['scrollPosition'] as number;
  12.       const selectedTab = want.parameters['selectedTab'] as number;
  13.       if (editContent !== undefined) {
  14.         hilog.info(DOMAIN, 'Demo', '恢复草稿内容');
  15.         // 将恢复的数据存入 AppStorage,页面读取后回显
  16.         AppStorage.setOrCreate('draftContent', editContent);
  17.         AppStorage.setOrCreate('scrollPosition', scrollPosition);
  18.         AppStorage.setOrCreate('selectedTabIndex', selectedTab);
  19.       }
  20.     }
  21.     // 其他初始化...
  22.   }
  23.   // ... onSaveState ...
  24. }
复制代码

完整示例
把上面三步合起来,就是一个完整的可运行例子:
  1. // entry/src/main/ets/entryability/EntryAbility.ets
  2. import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
  3. import { hilog } from '@kit.PerformanceAnalysisKit';
  4. import { window } from '@kit.ArkUI';
  5. const DOMAIN = 0x0000;
  6. export default class EntryAbility extends UIAbility {
  7.   onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
  8.     // 1. 启用备份恢复
  9.     this.context.setRestoreEnabled(true);
  10.     hilog.info(DOMAIN, 'Demo', 'UIAbility backup restore enabled');
  11.     // 2. 检查是否有备份数据需要恢复
  12.     if (want && want.parameters) {
  13.       const savedData = want.parameters['myAppState'] as string;
  14.       if (savedData) {
  15.         hilog.info(DOMAIN, 'Demo', '恢复数据: %{public}s', savedData);
  16.         AppStorage.setOrCreate('restoredState', savedData);
  17.       }
  18.     }
  19.   }
  20.   onSaveState(reason: AbilityConstant.StateType, wantParam: Record<string, Object>): AbilityConstant.OnSaveResult {
  21.     hilog.info(DOMAIN, 'Demo', 'onSaveState 被触发');
  22.     // 保存应用当前状态
  23.     wantParam['myAppState'] = JSON.stringify({
  24.       lastPage: AppStorage.get<string>('currentPage'),
  25.       editText: AppStorage.get<string>('editText'),
  26.       timestamp: Date.now()
  27.     });
  28.     return AbilityConstant.OnSaveResult.ALL_AGREE;
  29.   }
  30.   onWindowStageCreate(windowStage: window.WindowStage): void {
  31.     // 系统会自动恢复页面栈,这里正常加载页面就行
  32.     windowStage.loadContent('pages/Index', (err) => {
  33.       if (err.code) {
  34.         hilog.error(DOMAIN, 'Demo', '加载页面失败: %{public}s', JSON.stringify(err));
  35.         return;
  36.       }
  37.       hilog.info(DOMAIN, 'Demo', '首页加载成功');
  38.     });
  39.   }
  40. }
复制代码

如何在页面中使用恢复的数据
备份恢复的核心在 Ability 层,但你恢复的数据最终是要给页面用的。这里说一个常见的做法:通过 AppStorage 桥接。在 Ability 的 onCreate 里把数据写入 AppStorage,然后在页面中通过 @StorageLink 或 AppStorage.get 读取:
  1. // pages/EditorPage.ets
  2. @Entry
  3. @Component
  4. struct EditorPage {
  5.   @StorageLink('restoredState') restoredState: string = '';
  6.   @State editContent: string = '';
  7.   aboutToAppear(): void {
  8.     if (this.restoredState) {
  9.       try {
  10.         const state = JSON.parse(this.restoredState);
  11.         this.editContent = state.editText || '';
  12.         console.info('恢复编辑内容成功');
  13.       } catch (e) {
  14.         console.error('恢复数据解析失败');
  15.       }
  16.     }
  17.   }
  18.   build() {
  19.     Column() {
  20.       TextArea({ text: this.editContent })
  21.         .onChange((value: string) => {
  22.           this.editContent = value;
  23.           // 实时同步到 AppStorage,方便 onSaveState 读取
  24.           AppStorage.set('editText', value);
  25.         })
  26.     }
  27.   }
  28. }
复制代码
经验之谈:页面数据最好实时同步到 AppStorage,这样 onSaveState 被触发时能拿到最新的数据。别想着在 onSaveState 里再去读页面组件的数据,那个时机下页面可能已经销毁了,读不到。

和 appRecovery 的区别
这是这篇文章的重点。很多同学容易搞混。

appRecovery 是什么
简单回顾一下,appRecovery 是应用恢复模块,它做的事情是:
- 监听应用崩溃(JS Crash、C++ 异常、应用无响应)
- 崩溃后自动重启当前 Ability
- 可以选择保存 UI 状态,重启后恢复到之前的页面
核心 API 是 appRecovery.enableAppRecovery()。

两者的核心区别
||appRecovery|UIAbility 备份恢复|
|---|---|---|
|触发场景|应用在前台崩溃或ANR|应用在后台被系统杀掉|
|触发时机|崩溃发生时立即重启|下次冷启动时恢复|
|数据来源|崩溃前保存的UI状态|onSaveState保存的数据|
|页面栈恢复|自动恢复(包含UI状态)|仅恢复导航栈,不恢复页面内部状态|
|数据持久化|内存级别,重启设备丢失|文件级别,保留7天(重启设备丢失)|
|调用方式|appRecovery.enableAppRecovery()|context.setRestoreEnabled(true)|
|API支持|API 9+(不同能力分阶段)|API 14+|

怎么选
他俩不是二选一的关系,实际上是互补的。我的理解是:
- appRecovery 管的是“应用正在前台跑,突然崩了怎么办”——它的目标是让用户别看到崩溃白屏,直接悄悄重启回去
- UIAbility 备份恢复管的是“应用在后台被杀了,用户再打开时数据还在不在”——它的目标是让用户觉得应用从来没被杀过

我自己的项目里两个都开了。前台崩溃靠 appRecovery 兜底,后台被杀靠备份恢复保数据。不过要注意数据冲突的问题——两边都可能恢复数据,需要一个优先级策略。
  1. // 同时开启两种恢复机制
  2. import { AbilityConstant, UIAbility, Want, appRecovery } from '@kit.AbilityKit';
  3. export default class EntryAbility extends UIAbility {
  4.   onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
  5.     // 开启 appRecovery(崩溃后自动重启)
  6.     appRecovery.enableAppRecovery(
  7.       appRecovery.RestartFlag.ALWAYS_RESTART,
  8.       appRecovery.SaveOccasionFlag.SAVE_OCCASION_UI_STATE
  9.     );
  10.     // 开启 UIAbility 备份恢复(后台被杀后恢复数据)
  11.     this.context.setRestoreEnabled(true);
  12.     // 读取恢复数据
  13.     if (want && want.parameters && want.parameters['myAppState']) {
  14.       // 来自 UIAbility 备份恢复的数据
  15.       this.restoreFromBackup(want.parameters['myAppState']);
  16.     }
  17.   }
  18.   private restoreFromBackup(data: Object): void {
  19.     // 处理恢复的数据
  20.   }
  21.   onSaveState(reason: AbilityConstant.StateType, wantParam: Record<string, Object>): AbilityConstant.OnSaveResult {
  22.     // 保存状态...
  23.     return AbilityConstant.OnSaveResult.ALL_AGREE;
  24.   }
  25. }
复制代码
踩坑:两个机制都开的情况下,数据恢复的顺序要注意。appRecovery 是在崩溃后立即重启,这时候 UIAbility 备份恢复的数据还没写入(因为 onSaveState 可能都没来得及触发)。所以 appRecovery 恢复的是崩溃前的瞬时状态,而备份恢复恢复的是更早之前的持久化状态。如果两份数据同时存在,以 appRecovery 恢复的为准——因为它更新的。

反面教材
错误写法:往 wantParam 里塞了 200KB+ 的数据
  1. // 错误写法——数据超限
  2. onSaveState(reason: AbilityConstant.StateType, wantParam: Record<string, Object>): AbilityConstant.OnSaveResult {
  3.   // 假设有个很大的日志字符串
  4.   const hugeData = this.collectAllLogs(); // 可能几百 KB
  5.   wantParam['logs'] = hugeData;
  6.   // 还塞了很多其他东西
  7.   wantParam['userData'] = JSON.stringify(this.getAllUserData()); // 又几百 KB
  8.   return AbilityConstant.OnSaveResult.ALL_AGREE;
  9. }
复制代码
这个写法问题在于:wantParam 序列化后总大小不能超过 200KB。超了数据就丢了,而且没有任何报错。你完全不知道数据没保存成功。

正确写法:精简数据,控制体积
  1. // 正确写法——只保存关键状态
  2. onSaveState(reason: AbilityConstant.StateType, wantParam: Record<string, Object>): AbilityConstant.OnSaveResult {
  3.   // 只保存最关键的几个字段,别什么都往里塞
  4.   wantParam['lastPage'] = AppStorage.get<string>('currentPage') ?? 'home';
  5.   wantParam['draftId'] = AppStorage.get<string>('draftId') ?? '';
  6.   wantParam['scrollPos'] = AppStorage.get<number>('scrollY') ?? 0;
  7.   // 大日志、图片数据别放这里
  8.   return AbilityConstant.OnSaveResult.ALL_AGREE;
  9. }
复制代码
经验之谈:如果你不确定数据大小是否超限,可以在调试阶段把 wantParam 序列化成 JSON 然后 JSON.stringify().length 打印出来看看。超过 200KB 就做个裁剪。我当时是在 onSaveState 里打了个日志,发现有一份数据序列化后 350KB,难怪恢复不了。

踩坑记录
坑 1:setRestoreEnabled 调用时机不对
有次我把 setRestoreEnabled(true) 写到了 onForeground 里,结果完全不生效。
  1. // 错误写法——调用晚了
  2. onForeground(): void {
  3.   this.context.setRestoreEnabled(true); // 必须在 onForeground 之前调
  4. }
复制代码
官方文档明确说了:“需要在应用初始化阶段调用(onForeground 前),比如 UIAbility 的 onCreate。”后来改到 onCreate 里就好了。

坑 2:页面栈恢复没生效
如前面说的,页面栈恢复依赖好几层条件:
- module.json5 里不能设置 removeMissionAfterTerminate: true
- 设备必须支持任务保留机制(PC/2in1 设备不支持)
- 用户不能手动清理了任务卡片

我当时在平板(2in1 设备)上测试,发现页面栈就是恢复不了。查了文档才知道 2in1 设备不支持任务保留,所以备份恢复的页面栈功能在这些设备上是废的。踩坑:2in1 设备上备份恢复的页面栈能力不可用。但数据恢复(want.parameters)还是正常的。所以别指望在平板上能恢复到之前的页面。

坑 3:Want 参数在冷启动时覆盖了正常传参
有一次发现,应用被杀后重新打开,本该从 Deep Link 传进来的参数被备份恢复的 Want 数据覆盖了。排查后发现:冷启动时 onCreate 的 want 确实是新的(来自 Deep Link),但系统会把备份数据合并到 want.parameters 里。如果你的备份数据和 Deep Link 参数用了相同的 key,就会互相覆盖。
  1. // 注意:备份恢复的数据和正常启动参数共用 want.parameters
  2. onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
  3.   // 如果 Deep Link 传了 'source',而备份也存了 'source',就会冲突
  4.   const source = want.parameters?.['source'] as string;
  5.   // ...
  6. }
复制代码

总结
UIAbility 备份恢复是一个轻量级的后台数据保活方案,适合保存编辑中的表单、滚动位置、当前页面等临时状态,但不能替代持久化存储。与 appRecovery 结合使用可以覆盖前台崩溃和后台被杀两种场景。核心要点:启用时机要在 onCreate 而不是 onForeground,数据体积不能超过 200KB,注意 removeMissionAfterTerminate 冲突和 2in1 设备的限制。如果你的应用需要保证“用户就算切到后台被杀了,回来还能接着写”,这个机制就非常实用。
回复

使用道具 举报

发表于 2 小时前 | 显示全部楼层

Re: 鸿蒙UIAbility备份恢复实战:后台杀进程数据保活与页面栈恢复

楼主这篇文章太及时了!之前我也搞混过 appRecovery 和 UIAbility 备份恢复,看了你的分析终于彻底明白区别了。特别是那个 `removeMissionAfterTerminate: true` 和备份恢复互斥的坑,之前测试时死活恢复不了页面栈,原来根因在这里。另外你提到的 200KB 限制和重启设备失效也很关键——我之前拿它存用户草稿,现在得赶紧换成持久化方案。感谢分享,收藏了!
回复 支持 反对

使用道具 举报

发表于 2 小时前 | 显示全部楼层

Re: 鸿蒙UIAbility备份恢复实战:后台杀进程数据保活与页面栈恢复

感谢分享,这篇文章把 UIAbility 备份恢复和 appRecovery 的区别讲得很清楚,尤其是触发条件那块,之前我也一直以为被杀后冷启动就能自动恢复页面栈,结果测试发现页面回首页,看了你的解释才明白是 removeMissionAfterTerminate 的问题。数据 200KB 限制和重启设备失效这两个坑我也踩过,确实更适合保存轻量临时状态。想问下你实际项目中一般怎么配合持久化来保证关键数据的?
回复 支持 反对

使用道具 举报

发表于 2 小时前 | 显示全部楼层

Re: 鸿蒙UIAbility备份恢复实战:后台杀进程数据保活与页面栈恢复

感谢楼主这么详细的实战分享!我之前也把appRecovery和UIAbility备份恢复搞混过,看了你的对比一下清晰多了。特别是removeMissionAfterTerminate那个坑,不试一下真不知道会直接失效。另外200KB的限制和7天有效期提醒得很到位,看来临时状态存这里可以,但关键数据还是得自己持久化。想问一下,你实际项目里一般会把哪些数据放到onSaveState里保活?
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

指导单位

江苏省公安厅

江苏省通信管理局

浙江省台州刑侦支队

DEFCON GROUP 86025

Hacking Group 021A

旗下站点

态势感知中心

应急响应中心

红盟安全

联系我们

官方QQ群:112851260

官方邮箱:security#ihonker.org(#改成@)

官方核心成员

关注微信公众号

Archiver|手机版|小黑屋| ( 沪ICP备2021026908号 )

GMT+8, 2026-7-23 15:51 , Processed in 0.025330 second(s), 18 queries , Gzip On, Redis On.

Powered by ihonker.com

Copyright © 2015-现在.

  • 返回顶部