Flutter OH 外接纹理问题定位指南 返回 Flutter OH平台 DFX 问题定位导航1. 什么是外接纹理1.1 通俗解释外接纹理External Texture是一种让原生侧视频播放器、相机、动画等把画面喂给 Flutter 显示的机制。打个比方想象一条画面传送带生产者视频播放器/相机把画面一帧一帧放到传送带上消费者Flutter 引擎从传送带上取画面画到屏幕上如果传送带出了问题生产太快消费不过来、生产者停了、消费者卡了画面就会黑屏、卡顿或冻结。1.2 什么场景会用到外接纹理场景生产者典型问题视频播放视频播放器插件画面黑屏/卡顿相机预览相机插件预览画面不更新动画播放动画插件动画卡顿WebViewWebView 插件画面不更新直播直播 SDK画面延迟/卡顿1.3 问题类型速查你看到的现象可能的原因去看哪一节视频/相机画面黑屏纹理创建失败 / 生产者没产出帧§5画面第一帧后不更新帧闸门开启 / Surface 销毁 / onInactive 被误触发§6, §7画面卡顿/丢帧消费过慢 / 跳帧§8画面拉伸/变形尺寸变更未收敛§9应用闪退纹理释放后还在访问§10退后台后还在耗电可见区域监控未启用§72. 外接纹理架构2.1 画面传送带模型原生生产者 Flutter 消费者 (视频/相机/动画) (Raster 线程) │ │ │ 把画面写入窗口 │ 从窗口取出画面 ▼ ▼ ┌─────────────────────────────────────┐ │ OH_NativeImage 缓冲队列 │ │ (画面传送带) │ └─────────────────────────────────────┘ │ │ │ 新画面来了! │ 生成 GPU 图像 ▼ ▼ 通知 Flutter 重绘 画到屏幕上角色通俗理解实现类生产者把画面放到传送带上的人原生侧通过 surfaceId 获取窗口写入消费者从传送带取画面画到屏幕的人OHOSExternalTextureRaster 线程传送带中间的缓冲队列OH_NativeImageBufferQueue2.2 两种渲染后端后端什么时候用通俗解释OpenGL ESRenderingApi kOpenGLES用 GL 纹理显示画面Vulkan (Impeller)RenderingApi kImpellerVulkan用 Vulkan 信号量同步画面注册时会输出RegisterExternalTexture api type N texture_id idN就是渲染后端编号。2.3 开发者 API如果你是插件开发者通过TextureRegistry使用外接纹理方法作用什么时候用registerTexture(id)注册纹理返回 surfaceId创建视频/相机纹理时registerPixelMap(pixelMap)注册静态图片纹理显示一张图片unregisterTexture(id)注销纹理不再需要时先停生产者再注销setTextureBufferSize(id,w,h)设置生产者窗口尺寸画面尺寸变化时setExternalNativeImagePtr(id,img)替换为外部 NativeImage高级用法3. 纹理生命周期3.1 注册流程创建传送带registerTexture(textureId) │ ├─ 创建 OH_NativeImage创建传送带 │ ├─ 成功 → 继续 │ └─ 失败 → 日志 OH_NativeImage_Create() failed │ ├─ 获取 NativeWindow获取传送带入口 │ ├─ 成功 → 继续 │ └─ 失败 → 日志 OH_NativeImage_AcquireNativeWindow() failed │ ├─ 获取 surfaceId给生产者用的地址 │ └─ 失败 → 日志 OH_NativeImage_GetSurfaceId() failed │ └─ 注册到引擎 → 日志 RegisterExternalTexture api type N texture_id X成功的日志I Flutter: RegisterExternalTexture api type 2 texture_id 1 I Flutter: OH_NativeImage_AcquireNativeWindow() success I Flutter: OH_NativeImage_GetSurfaceId() success, surfaceId 123456783.2 注销流程拆除传送带顺序很重要必须先停生产者再注销纹理。否则生产者还在往已拆除的传送带上放东西会崩溃。✅ 正确顺序 1. 停止生产者暂停视频/相机 2. 调用 unregisterTexture ❌ 错误顺序 1. 调用 unregisterTexture传送带拆了 2. 生产者还在写入 → 崩溃4. 帧调度传送带怎么运转4.1 帧序号机制引擎用两个计数器跟踪画面的生产和消费计数器通俗理解含义now_new_frame_seq_num“生产了多少帧”生产者产出的帧数now_paint_frame_seq_num“画了多少帧”消费者已绘制的帧数如果生产的远大于画了的说明消费跟不上引擎会主动跳帧丢掉一些旧画面只画最新的保证实时性。4.2 跳帧策略缓冲队列大小跳帧阈值通俗解释≤ 5size - 1普通场景保留 1 个缓冲 5size * 2/3视频/相机场景保留更多缓冲跳帧日志I Flutter: external_texture skip one frame(slow consumer): ... buffer_queue_size 5 max_jank_frame 4 I Flutter: MarkNewFrameAvailable avail-seq 120 paint-seq 100 texture_id 1skip one frame(slow consumer) 消费太慢了跳过了一些帧5. 纹理黑屏/不显示5.1 排查流程画面黑屏 │ ├─ 搜索 RegisterExternalTexture api type │ └─ 没搜到 → 纹理根本没注册检查 registerTexture 调用 │ ├─ 搜索 OH_NativeImage_Create() failed │ └─ 搜到了 → 创建失败检查系统资源 │ ├─ 搜索 OH_NativeImage_GetSurfaceId() failed │ └─ 搜到了 → surfaceId 获取失败纹理没真正注册 │ ├─ 搜索 No DlImage available │ └─ 搜到了 → 传送带上没有画面生产者没产出帧 │ ├─ 检查原生侧是否已向 surfaceId 写入数据 │ └─ 检查 Texture 控件的 size 是否为 0 │ └─ 检查渲染后端是否匹配5.2 常见原因和修复方法原因通俗解释怎么修纹理未注册压根没调用registerTexture检查注册代码NativeImage 创建失败系统资源不足检查系统资源生产者没产出帧视频还没开始播放 / 相机没启动确保生产者已开始写入Texture 控件 size 为 0画面区域是 0x0检查 Widget 布局surfaceId 不对生产者写入了错误的 surfaceId确认 surfaceId 传递正确6. 帧闸门Frame Gate6.1 通俗解释当应用退到后台时引擎会开启帧闸门——继续排空传送带上的画面防止生产者阻塞但不再调度重绘反正用户看不到。打个比方就像快递柜——你退后台后快递还在往柜子里放排空队列但你不取快递了不渲染。回前台后恢复正常取件。6.2 状态切换时机帧闸门状态日志退后台开启不渲染但排空frame gate enabled, drain-only for texture X回前台关闭恢复正常ExecuteReclaimRestore - restoring foreground state6.3 排查纹理不更新如果纹理在后台回前台后不更新搜索frame gate enabled→ 确认帧闸门是否仍开启搜索ExecuteReclaimRestore→ 确认是否已恢复搜索Surface REBUILT→ 确认 Surface 重建是否成功搜索NotifyDestroyed→ 确认 Surface 是否已销毁7. 可见区域监控7.1 通俗解释3.35 版本新增了可见区域监控功能。当 PlatformView嵌入 Flutter 的原生组件不可见时可以自动暂停纹理的生产比如暂停视频/动画避免不可见时的无效耗电。打个比方就像你走开不看 TV 时TV 自动暂停播放。7.2 怎么启用默认是关闭的enablefalse插件需要重写getPlatformViewVisibleAreaEventOptions()来开启// 在你的 PlatformView 子类中重写getPlatformViewVisibleAreaEventOptions():PlatformViewVisibleAreaEventOptions{return{enable:true,// 开启监控ratios:[0.0,1.0],// 可见比例阈值expectedUpdateInterval:1000,// 期望回调间隔毫秒onInactiveThreshold:0.0,// 可见比例 ≤ 此值时触发 onInactive()暂停onActiveThreshold:1.0,// 可见比例 ≥ 此值时触发 onActive()恢复}asPlatformViewVisibleAreaEventOptions;}// 暂停纹理生产比如暂停视频onInactive():void{this.videoPlayer?.pause();}// 恢复纹理生产onActive():void{this.videoPlayer?.play();}7.3 关键日志I Flutter: setPlatformViewVisibleAreaEventCallback surfaceId:12345, enable:true I Flutter: PlatformViewVisibleAreaEventCallback surfaceId:12345, isExpanding:false, currentRatio:0日志含义isExpanding:false, currentRatio:0不可见了触发onInactive()暂停isExpanding:true, currentRatio:1完全可见触发onActive()恢复8. 纹理卡顿/丢帧8.1 排查流程画面卡顿 │ ├─ 搜索 skip one frame(slow consumer) │ └─ 搜到了 → 消费过慢引擎在跳帧 │ ├─ 检查 Raster 线程是否被其他任务阻塞 │ └─ 检查 buffer_queue_size 是否过小 │ ├─ 搜索 MarkNewFrameAvailable avail-seq │ ├─ avail-seq 不增长 → 生产者没产出帧 │ └─ avail-seq 增长但 paint-seq 不增长 → Raster 线程卡了 │ ├─ 搜索 GpuReclaim │ └─ 搜到了 → GPU 回收导致中断详见 dfx-memory.md │ └─ 搜索 get error buffer queue size └─ 搜到了 → 缓冲队列异常1009. 画面拉伸/变形9.1 排查搜索size change相关日志日志含义size change took N frames尺寸变更在 N 帧内完成正常stop size change state: frame 10尺寸变更超过 10 帧异常direct release size changed buffer缓冲尺寸变了但绘制区域没变防拉伸修复确保setTextureBufferSize和notifyTextureResizing调用一致。10. 纹理访问崩溃10.1 通俗解释就像你把快递箱扔了但还有人去箱子里拿东西——当然会出问题。常见原因unregisterTexture后原生侧还在写入 surfaceId外部 NativeImage 被提前释放GPU 上下文销毁后还引用 GPU 资源修复先停生产者再注销纹理// ✅ 正确顺序stopProducer();// 先停textureRegistry.unregisterTexture(textureId);// 后注销详见 Flutter OH 崩溃问题定位指南 §2.6 场景 2。11. 日志关键字速查表搜索这个关键字含义严重程度RegisterExternalTexture api type纹理注册—OH_NativeImage_Create() failed创建失败高No DlImage available无可绘制画面黑屏中frame gate enabled, drain-only后台帧闸门开启正常skip one frame(slow consumer)消费过慢跳帧中MarkNewFrameAvailable avail-seq帧序号监控—OnGrContextCreated texture_idGPU 上下文重建—size change took N frames尺寸变更完成正常PlatformViewVisibleAreaEventCallback可见区域变化—UnRegisterExternalTexture纹理注销—~OHOSExternalTexture纹理析构—12. 排查清单黑屏/不显示搜索RegisterExternalTexture确认纹理已注册搜索OH_NativeImage_Create() failed确认创建成功搜索No DlImage available确认是否有画面检查原生侧是否已向 surfaceId 写入数据检查 Texture 控件 size 是否为 0不更新搜索frame gate enabled确认帧闸门状态搜索NotifyDestroyed确认 Surface 是否存活搜索OnGrContextCreated/Destroyed确认 GPU 上下文搜索PlatformViewVisibleAreaEventCallback确认是否被onInactive暂停卡顿搜索skip one frame确认跳帧类型搜索MarkNewFrameAvailable avail-seq对比生产/消费序号搜索GpuReclaim确认是否伴随 GPU 回收后台行为搜索frame gate enabled确认帧闸门开启搜索ExecuteReclaimRestore确认回前台后恢复检查onInactive/onActive是否实现