import webview from '@ohos.web.webview';
import { UIContext } from '@ohos.arkui.UIContext';
import { SDK_VERSION } from '../SdkVersion';
import { WebHostModel } from './WebHostModel';
import { WebNodeController } from './TencentQianWebNode';
import { TencentQianWebViewHandlers, ScriptItemLike } from '../../TencentQianWebViewConfigurator';
import { WebSettingsApplier } from '../WebSettingsApplier';
import { NetworkStateInjector } from '../NetworkStateInjector';
/**
* 鸿蒙 WebView 离屏复用池(容量 = 1,**借还池**,与 Android 规格一致)。
*
* 基于官方「离线 Web 组件」:`WebNodeController` + `BuilderNode` 命令式创建 Web 组件(离屏、
* Hidden/Inactive、启动渲染进程),HEAVY 加载 `warmupUrl` 热网络缓存。真实页容器用
* `NodeContainer(nodeController)` 绑定复用,通过热替换 `WebHostModel.handlers` / `url` 切换为签署态。
*
* **生命周期(借还池,同 Android)**:
* - HEAVY `warm()`:离屏建组件 + load(warmupUrl);多次 prewarm 复用同一实例,绝不重复建。
* - `open()` → `obtain()`:借出(UA 配置匹配才复用),容器热替换 handlers/url 并 loadUrl 真实页。
* - 容器关闭 → `recycle()`:**不销毁**,`about:blank` + `clearHistory` 后放回池,待下次复用。
* - 客户主动 `release()`(或内存告警):才真正 `dispose` 销毁。
*
* **约束**:`BuilderNode` 需 `UIContext`(`prewarm(HEAVY, bizType, uiContext)` 传入);
* 每组件约 200MB,容量恒 1;`dispose` 前须先从 `NodeContainer` 解绑(`onUnbind`),否则白屏。
*/
/** UA 签名:复用门控——预热 UA 配置需与 `open()` config UA 一致,否则不复用(避免 warmupUrl 缓存 miss)。 */
function uaSignatureOf(injectUA: boolean, uaExtra: string): string {
return (injectUA ? '1' : '0') + '|' + uaExtra;
}
/**
* 构造预热用最小 handlers:DEFAULTS settings + UA 注入 + no-op 事件。
* 真实页复用时会被 `Configurator.apply` 生成的真实 handlers 热替换(见 Page/Component)。
*/
function buildPrewarmHandlers(
controller: webview.WebviewController,
injectUA: boolean,
uaExtra: string
): TencentQianWebViewHandlers {
const injector: NetworkStateInjector = new NetworkStateInjector();
const emptyScripts: Array<ScriptItemLike> = [];
const handlers: TencentQianWebViewHandlers = {
webSettings: WebSettingsApplier.DEFAULTS,
mixedMode: 'None',
documentStartScripts: emptyScripts,
networkStateInjector: injector,
onControllerAttached: (): void => {
try {
if (injectUA) {
const base: string = controller.getUserAgent();
let ua: string = base + ' qianwv/' + SDK_VERSION;
if (uaExtra.length > 0) {
ua = ua + ' ' + uaExtra;
}
controller.setCustomUserAgent(ua);
}
} catch (_e) {
// best-effort
}
try {
injector.attach(controller);
} catch (_e) {
// ignore
}
},
onLoadIntercept: (_event: ESObject): boolean => false,
onPageBegin: (_event: ESObject): void => {},
onPageEnd: (_event: ESObject): void => {},
onPermissionRequest: (_event: ESObject): void => {},
onShowFileSelector: (_event: ESObject): boolean => false,
onAlert: (_event: ESObject): boolean => false,
onConfirm: (_event: ESObject): boolean => false,
onPrompt: (_event: ESObject): boolean => false,
onDownloadStart: (_event: ESObject): void => {},
onErrorReceive: (_event: ESObject): void => {},
onHttpErrorReceive: (_event: ESObject): void => {},
onSslErrorEventReceive: (_event: ESObject): void => {},
// 预热态 / 归还态守卫:此时节点未绑定在可见容器上,渲染进程崩溃直接销毁并作废池实例
// (避免"渲染进程已死的僵尸实例"被后续 obtain 复用导致持久白屏)。对齐 Android PoolGuardClient。
// 借出态不走这里:借出后 handlers 被 Configurator 真实 handlers 热替换(其 onRenderExited 仅发事件、
// 不作废池),作废由统一 Builder 的 onPoolInvalidateRequest → unpoolBorrowed() 完成
// (只脱池、不销毁正在显示的活节点)。
onRenderExited: (_event: ESObject): void => {
HarmonyWebViewPool.invalidate();
},
onProgressChange: (_event: ESObject): void => {}
};
return handlers;
}
/**
* 鸿蒙离屏借还池(单例,容量 1)。
*/
export class HarmonyWebViewPool {
private static node: WebNodeController | null = null;
private static warmedUrl: string = '';
/** 是否已被容器借出(借出中不可再 obtain)。 */
private static inUse: boolean = false;
/** 预热时的 UA 签名,供复用门控比对。 */
private static uaSig: string = '';
/** 预热时的原始 UA 配置,供归还时重建预热守卫 handlers(避免从 uaSig 反解)。 */
private static warmedInjectUA: boolean = false;
private static warmedUaExtra: string = '';
private static enabled: boolean = true;
public static setEnabled(value: boolean): void {
HarmonyWebViewPool.enabled = value;
if (!value) {
HarmonyWebViewPool.release();
}
}
/**
* HEAVY:离屏建 Web 组件并加载 `warmupUrl`。多次 prewarm 复用同一实例(按需切 URL),绝不重复建。
*/
public static warm(warmupUrl: string, uiContext: UIContext, injectUA: boolean, uaExtra: string): void {
if (!HarmonyWebViewPool.enabled || warmupUrl.length === 0) {
return;
}
try {
if (HarmonyWebViewPool.node === null) {
const controller: webview.WebviewController = new webview.WebviewController();
const model: WebHostModel = new WebHostModel(controller);
model.url = warmupUrl;
model.handlers = buildPrewarmHandlers(controller, injectUA, uaExtra);
const ctrl: WebNodeController = new WebNodeController();
ctrl.init(uiContext, model);
HarmonyWebViewPool.node = ctrl;
HarmonyWebViewPool.uaSig = uaSignatureOf(injectUA, uaExtra);
HarmonyWebViewPool.warmedInjectUA = injectUA;
HarmonyWebViewPool.warmedUaExtra = uaExtra;
} else if (warmupUrl !== HarmonyWebViewPool.warmedUrl && !HarmonyWebViewPool.inUse) {
const model: WebHostModel | null = HarmonyWebViewPool.node.getModel();
if (model !== null) {
try {
model.controller.loadUrl(warmupUrl);
} catch (_e) {
// best-effort
}
}
}
HarmonyWebViewPool.warmedUrl = warmupUrl;
} catch (_e) {
// best-effort:预热失败绝不影响主流程
}
}
/**
* 容器复用:取出池 node(未借出、已就绪、且 UA 配置匹配时返回;否则 null → 容器自建)。
*/
public static obtain(injectUA: boolean, uaExtra: string): WebNodeController | null {
if (!HarmonyWebViewPool.enabled || HarmonyWebViewPool.node === null || HarmonyWebViewPool.inUse) {
return null;
}
if (HarmonyWebViewPool.uaSig !== uaSignatureOf(injectUA, uaExtra)) {
return null;
}
HarmonyWebViewPool.inUse = true;
return HarmonyWebViewPool.node;
}
/**
* 归还(借还池,**不销毁**):清空内容 + 清历史,保留实例待下次复用(同 Android)。
* 容器须已先从 `NodeContainer` 解绑。
*
* **归还态守卫(对齐 Android QianFallbackWebViewClient)**:把 `model.handlers` 复位为
* 预热守卫 handlers,其 `onRenderExited` 会作废池——覆盖"归还后 ~ 下次借出前"窗口内渲染
* 进程崩溃的场景,避免僵尸实例被后续 `obtain` 复用。同时重置 `model.fromPool=false`。
*/
public static recycle(): void {
if (HarmonyWebViewPool.node === null) {
return;
}
const model: WebHostModel | null = HarmonyWebViewPool.node.getModel();
if (model !== null) {
try {
model.controller.loadUrl('about:blank');
model.controller.clearHistory();
} catch (_e) {
// ignore
}
// 复位为预热态守卫 handlers:归还态渲染进程崩溃时作废池(三态闭环)。
// 直接复用预热时保存的原始 UA 配置,避免从 uaSig 反解。
model.handlers = buildPrewarmHandlers(
model.controller, HarmonyWebViewPool.warmedInjectUA, HarmonyWebViewPool.warmedUaExtra);
model.fromPool = false;
}
HarmonyWebViewPool.warmedUrl = 'about:blank';
HarmonyWebViewPool.inUse = false;
}
public static isInUse(): boolean {
return HarmonyWebViewPool.inUse;
}
/**
* 内存压力回收:**仅在实例未被借出(`!inUse`)且存在时**才 dispose 销毁,
* 避免打断正在使用的签署容器。
*
* 借出中(`inUse === true`)则跳过——等容器关闭 `recycle()` 归还回空闲态后,
* 下次内存告警再回收。
*
* @returns 是否真正释放了实例(供上层据此决定是否重置去重状态,使后续 prewarm 可重新预热)
*/
public static releaseIfIdle(): boolean {
if (HarmonyWebViewPool.inUse || HarmonyWebViewPool.node === null) {
return false;
}
HarmonyWebViewPool.release();
return true;
}
/**
* 作废池实例(**预热态 / 归还态**渲染进程崩溃场景,对齐 Android `invalidateIfPooled`)。
*
* 此时 node 未绑定在任何可见容器上,销毁当前池 node 并清空全部池状态是安全的,
* 避免"渲染进程已死的僵尸实例"被后续 `obtain` 反复借出导致持久白屏。容量恒 1,
* 故直接作废当前实例即可;下次 `obtain` 返回 null → 容器自建。
*
* ⚠️ **借出态**(node 正绑在容器 `NodeContainer` 上显示)崩溃时**不要**用本方法:
* 销毁正在显示、且本可原地 `refresh()` 恢复的活节点会导致白屏/闭页,请用 {@link unpoolBorrowed}。
*/
public static invalidate(): void {
HarmonyWebViewPool.release();
}
/**
* 借出态渲染进程崩溃专用:**只脱池、不销毁正在显示的活节点**
* (对齐 Android `invalidateIfPooled`——只清池槽位、不销毁 Activity 正用的 WebView)。
*
* 借出后 node 仍绑在容器 `NodeContainer` 上,`WebviewController` 渲染进程崩溃后依旧有效,
* 容器会在同一 controller 上 `refresh()` 原地恢复(鸿蒙无需与界面节点分离 / 重建)。
* 故这里仅清空池账本,使该崩溃实例**不再被 `obtain` 借出、也不再被 `recycle` 收回**;
* 活节点由容器继续持有,最终在容器关闭(`aboutToDisappear`,`fromPool=false` → `disposeNode()`)
* 时销毁,生命周期闭环不变。下次 `obtain` 因 `node=null` 返回 null → 新 `open()` 自建新实例,不复用僵尸。
*
* **刻意不做** `disposeNode()` / `cancelPendingRecover()`——避免打断正在进行的原地崩溃恢复。
*/
public static unpoolBorrowed(): void {
HarmonyWebViewPool.node = null;
HarmonyWebViewPool.warmedUrl = '';
HarmonyWebViewPool.inUse = false;
HarmonyWebViewPool.uaSig = '';
HarmonyWebViewPool.warmedInjectUA = false;
HarmonyWebViewPool.warmedUaExtra = '';
}
/**
* 客户主动释放池实例(销毁)。签署流程结束、不再需要时调用。
*/
public static release(): void {
if (HarmonyWebViewPool.node !== null) {
// 取消可能挂起的延迟恢复,避免回调访问即将 dispose 的 controller(野指针)。
const model: WebHostModel | null = HarmonyWebViewPool.node.getModel();
if (model !== null) {
model.cancelPendingRecover();
}
HarmonyWebViewPool.node.disposeNode();
HarmonyWebViewPool.node = null;
}
HarmonyWebViewPool.warmedUrl = '';
HarmonyWebViewPool.inUse = false;
HarmonyWebViewPool.uaSig = '';
HarmonyWebViewPool.warmedInjectUA = false;
HarmonyWebViewPool.warmedUaExtra = '';
}
// ArkTS 类禁止实例化
private constructor() {}
}
|