HarmonyOS 7 模块化对象接入:threadMode 写了 INSTANCE,为什么仍进 IPC 线程池?

发布时间:2026/9/28 21:25:47
HarmonyOS 7 模块化对象接入:threadMode 写了 INSTANCE,为什么仍进 IPC 线程池? HarmonyOS 7 模块化对象接入threadMode 写了 INSTANCE为什么仍进 IPC 线程池准备把一个本地文档处理能力开放给另一个应用metadata里已经把threadMode设成INSTANCE请求却没有按照预期进入实例专用线程。接着把“断开连接”当成“马上释放所有对象”又留下了userData被请求回调继续访问的风险。这两个问题的共同点是只看了配置名没把配置与对象创建路径、回调寿命连起来。本文围绕HarmonyOS 7新引入的ModularObjectExtensionAbility用一份配置检查器和一组销毁顺序测试把这条接入路径具体化。先限定设备再谈接入截至2026年9月27日核对的官方开发指南组件从API26.0.0开始提供当前仅支持PC/2in1设备。不能因为C API参考页顶部还展示了其他设备标签就省略指南中的明确限制。客户端连接时需要处于前台服务端应用需要有正在运行的UIAbility或UIExtensionAbility实例。还要留意当前规格同一服务端应用内同名Ability最多20个实例同一客户端进程针对该服务端同名Ability最多5个连接同一客户端应用连接调用不超过每秒20次。它们是不同维度的限制不是一个统一的“20个请求”上限。开发指南更新于2026年9月9日本文核对的Native头文件参考更新于8月29日。示例的JavaScript配置与时序测试在本机运行C平台适配代码未完成API26 SDK编译、PC设备运行和真实IPC压力测试。下面不会把宿主模型测试当成跨进程实测结果。三个Mode分别管什么配置含义默认与前提launchMode是否允许跨进程启动默认IN_PROCESS此时客户端与目标Ability必须同应用processMode多个实例如何共享进程默认BUNDLE仅CROSS_PROCESS时生效threadModeIPC请求执行线程的共享策略默认BUNDLE必须使用上下文专用Stub工厂processMode和threadMode都有BUNDLE、TYPE、INSTANCE但一个决定进程共享一个决定线程共享不能因为值同名就理解成同一层隔离。INSTANCE线程模式不等于所有实例全局串行也不能自动保护应用另外创建的工作线程。官方明确写出了threadMode的生效条件通过 OH_AbilityRuntime_ModObjExtensionContext_CreateIPCRemoteStub 创建OHIPCRemoteStub。走其他创建路径时IPC请求会在IPC工作线程池执行。只改metadata不改工厂调用问题自然还在。案例一配置正确创建路径不匹配下面是用于PC跨应用能力的配置片段放在实际module.json5的extensionAbilities内。srcEntry与CMake编出的so名称保持一致不是ArkTS页面路径。{ name: DocumentModuleAbility, srcEntry: libentry.so, type: modularObject, exported: true, metadata: [ { name: launchMode, value: CROSS_PROCESS }, { name: processMode, value: BUNDLE }, { name: threadMode, value: INSTANCE }, { name: isDisabled, value: false } ] }选择共享进程、实例线程是为了在这个示例中避免为每个对象独占进程同时区分实例内请求执行。它不是通用最佳配置实例数量增加会改变线程资源成本。需要更强进程隔离时再评估processModeINSTANCE而不是见到INSTANCE就全部开启。另外isDisabledtrue的含义不是“连本应用也绝对不可用”指南说的是禁用对其他应用的开放只允许本应用连接。对外开放时还需结合exported及完整安全设计不能把一个metadata字段当成调用方身份校验。把这几个条件整理成可执行检查比到设备上才发现默认值回退更早export function auditModularConfig(ability, environment) { const issues []; const metadata new Map(); for (const item of ability.metadata || []) { if (metadata.has(item.name)) issues.push(DUPLICATE_METADATA: item.name); metadata.set(item.name, item.value); } const launch metadata.get(launchMode) ?? IN_PROCESS; const process metadata.get(processMode) ?? BUNDLE; const thread metadata.get(threadMode) ?? BUNDLE; const disabled metadata.get(isDisabled) ?? false; if (environment.device ! PC/2in1) issues.push(DEVICE_NOT_IN_CURRENT_GUIDE); if (ability.type ! modularObject) issues.push(WRONG_EXTENSION_TYPE); if (![IN_PROCESS, CROSS_PROCESS].includes(launch)) issues.push(INVALID_LAUNCH_MODE); if (![BUNDLE, TYPE, INSTANCE].includes(process)) issues.push(INVALID_PROCESS_MODE); if (![BUNDLE, TYPE, INSTANCE].includes(thread)) issues.push(INVALID_THREAD_MODE); if (![true, false].includes(disabled)) issues.push(INVALID_DISABLED_VALUE); if (environment.crossApp launch ! CROSS_PROCESS) issues.push(CROSS_APP_NEEDS_CROSS_PROCESS); if (environment.crossApp (ability.exported ! true || disabled true)) issues.push(CROSS_APP_NOT_OPEN); if (launch ! CROSS_PROCESS metadata.has(processMode)) issues.push(PROCESS_MODE_IGNORED); if (!environment.contextStubFactory) issues.push(THREAD_MODE_FACTORY_MISMATCH); return issues; }这个检查器检查的是项目声明与开发者提供的环境信息不会替你侦测真实线程也不会自动扫描so里的调用。contextStubFactory必须来自代码审查或实际埋点证据不能为了“通过检查”直接写true。平台适配的关键调用如下回调类型直接沿用官方头文件不编造一个ArkTS装饰器来替代Native组件#include AbilityKit/ability_runtime/modular_object_extension_context.h OHIPCRemoteStub* CreateDocumentStub( OH_AbilityRuntime_ModObjExtensionContextHandle context, const char* descriptor, OH_OnRemoteRequestCallback onRequest, OH_OnRemoteDestroyCallback onDestroyed, void* userData) { if (context nullptr || descriptor nullptr || onRequest nullptr) { return nullptr; } return OH_AbilityRuntime_ModObjExtensionContext_CreateIPCRemoteStub( context, descriptor, onRequest, onDestroyed, userData); }它是创建入口不是一份独立可运行的IPC服务。工程仍需注册OH_AbilityRuntime_OnNativeExtensionCreate入口从基础实例获取模块化实例注册OnCreate/OnConnect/OnDisconnect/OnDestroy准备接口和Proxy/Stub完整工程骨架参考文末官方指南。工程链接库包括libability_runtime.so涉及Want和IPC序列化还需相应库。返回NULL代表创建失败。此时应用还没有获得一个可销毁的StubuserData的应用所有者仍要负责失败分支清理不能一直等一个不会由本次成功创建产生的销毁回调。下面的检查先故意使用非专用工厂再改正它另一次把launchMode改回IN_PROCESS检查跨应用条件const ability { name: DocumentModuleAbility, srcEntry: libentry.so, type: modularObject, exported: true, metadata: [ { name: launchMode, value: CROSS_PROCESS }, { name: processMode, value: BUNDLE }, { name: threadMode, value: INSTANCE }, { name: isDisabled, value: false } ] }; const env { device: PC/2in1, crossApp: true, contextStubFactory: false }; if (!auditModularConfig(ability, env).includes(THREAD_MODE_FACTORY_MISMATCH)) throw new Error(factory mismatch missed); env.contextStubFactory true; if (auditModularConfig(ability, env).length ! 0) throw new Error(valid config rejected); const localOnly { ...ability, metadata: ability.metadata.map(item item.name launchMode ? { name: item.name, value: IN_PROCESS } : item) }; if (!auditModularConfig(localOnly, env).includes(CROSS_APP_NEEDS_CROSS_PROCESS)) throw new Error(cross-app restriction missed); console.log(factory and launch-mode cases passed);案例二Destroy调用后就delete userData哪里不对上下文专用CreateIPCRemoteStub文档区分了两种传入对象descriptor在创建过程中被内部复制函数返回后可以释放原字符串userData只是传入的指针必须在对象销毁前保持有效。两者不能采用同样的释放时机。文档还明确了时序调用对应DestroyIPCRemoteStub后不再有新的requestCallback正在执行的requestCallback结束后才调用destroyCallback。因此“已经请求销毁”和“可以立即释放回调要访问的数据”是两个状态。最容易出错的简化流程是发起销毁立即delete数据正在执行的回调继续访问原指针。修正方式是把数据所有权交接说清创建失败由创建方回收创建成功后由约定的最终销毁回调回收。不要同时让两个地方delete同一份数据。下面用一个不涉及IPC的顺序模型复现错误释放检查。它只是测试所有权规则不模拟系统线程调度export class StubLifetimeModel { constructor() { this.accepting true; this.active 0; this.dataAlive true; this.destroyCallbackSeen false; } beginRequest() { if (!this.accepting) return false; if (!this.dataAlive) throw new Error(request uses freed data); this.active; return true; } requestDestroy() { this.accepting false; } endRequest() { if (!this.dataAlive) throw new Error(in-flight request uses freed data); if (this.active 0) throw new Error(unbalanced request); this.active--; } onDestroyed() { if (this.accepting || this.active ! 0) throw new Error(destroy callback too early); if (this.destroyCallbackSeen) throw new Error(duplicate destroy callback); this.destroyCallbackSeen true; this.dataAlive false; } } const life new StubLifetimeModel(); if (!life.beginRequest()) throw new Error(first request rejected); life.requestDestroy(); if (life.beginRequest()) throw new Error(new request accepted after destroy request); let earlyRejected false; try { life.onDestroyed(); } catch { earlyRejected true; } if (!earlyRejected || !life.dataAlive) throw new Error(early release not detected); life.endRequest(); life.onDestroyed(); if (life.dataAlive) throw new Error(data was not reclaimed); console.log(in-flight destroy ordering case passed);模型里的onDestroyed由测试显式调用。真实接入时它对应系统通知不是应用自己“等一会儿”后假装收到。延时几十毫秒再delete也不是可靠修复执行时间会随负载变化。另一个边界是业务派生的异步任务。requestCallback把数据指针交给自己的后台任务后返回平台能等待的是回调本身不是它看不见的业务工作。此时要复制任务输入、使用明确的共享所有权或等待业务任务退出不能扩大官方回调顺序保证的范围。OnDisconnect也不是每个客户端断开都回调一次服务端OnDisconnect的定义是当前实例的所有客户端连接都断开时触发。不要在这个回调里凭空把“连接数减一”当成系统给你的单连接事件。客户端连接选项中的断连回调与服务端实例生命周期也需要分别处理。如果根据产品策略在服务端OnDisconnect中回收Stub需要按上述专用Destroy接口及userData规则收尾OnDestroy再清理实例关联资源时要避免再次销毁已经回收的同一Stub。重复清理保护应围绕资源所有者而不是到处加一个全局布尔值覆盖所有实例。为什么不直接把所有调用都加锁互斥锁可以保护共享业务数据但它不能修正错误的Stub创建路径也不能把失效context恢复更不能让已delete的userData重新有效。先把运行线程和生命周期条件接对再讨论共享状态是否还需锁。选择收益代价或限制BUNDLE线程共享线程资源较集中一个长请求可能影响同组任务INSTANCE线程实例间执行线程分离实例多时资源成本增加业务工作线程可隔离重任务需要自行管理并发、取消与数据寿命平台文档说回调按指定线程顺序执行不等于应该把长耗时任务全部放在回调里。接口返回时机、业务任务寿命和错误传播要一起设计。学习官方计算器例子时真正值得带走的是配置到实例、实例到上下文、上下文到Stub的完整关系不是只复制一个Add方法。接入检查可以从六项开始PC/2in1与前后台条件metadata合法值专用创建和对应销毁配对每次注册返回码创建失败的所有权销毁期间仍有请求的测试。然后补真实设备线程日志与多客户端连接测试再决定是否需要调整processMode或threadMode。官方资料模块化对象开发指导设备限制、配置、服务端和客户端modular_object_extension_ability.h生命周期回调定义modular_object_extension_context.hStub工厂、销毁顺序与参数寿命