【DFX系列】Flutter 鸿蒙应用外接纹理介绍及问题定位

文章来源声明: 原文作者:技术任我行XTing; 来源站点:掘金; 原文链接:https://juejin.cn/post/7686063376205791251; 本文基于上述来源整理/加工,觅优补充点评,仅供技术学习交流。版权归原作者所有。
觅优短评

外接纹理是 Flutter 鸿蒙音视频类应用的排障基石,文中日志关键字速查表实用性强,适合客户端开发与音视频方向同学对照定位渲染异常。

在我们调试Flutter 鸿蒙应用过程中,如果遇到视频播放画面黑屏卡顿,相机预览画面不更新,动画卡等异常场景...那么今天的这篇关于外接纹理异常的专题应该能帮助到你!

一、外接纹理概念

外接纹理(External Texture) :是 Flutter Engine 提供的一种外部图像数据接入机制,允许鸿蒙原生侧产生的 GPU 图像帧直接注册到 Flutter 渲染管线,Flutter 通过Texture Widget在 Dart 层进行布局、变换、叠加渲染,而不是把图像像素拷贝到 Dart 内存里。

外接纹理与PlatformView 的区别:

外接纹理:原生输出 GPU 纹理帧,由 Flutter 统一合成,属于 Flutter LayerTree 里的TextureLayer。支持 Flutter 的动画、旋转、缩放、圆角、透明度叠加;z 轴和 Flutter UI 完全融合。

PlatformView:原生是独立的 XComponent 窗口,Flutter 只是 “挖个洞” 把原生视图盖在 Flutter 上面,叠加、动画、裁剪会有各种边界问题。

二、外接纹理注册流程

cke_9176.png​如果你的日志显示如下,代表就成功了:

<span>I</span> Flutter:  RegisterExternalTexture api type <span>2</span> texture_id <span>1</span> 
I Flutter: <span>OH_NativeImage_AcquireNativeWindow</span>() success 
I Flutter: <span>OH_NativeImage_GetSurfaceId</span>() success, surfaceId = <span>12345678</span>

注销的时候,必须先停生产者,再注销纹理。如果代码顺序搞反了就会出现崩溃问题:

<span>// 正确顺序</span>
<span>stopProducer</span>();                         
<span>// 1. 先停:暂停视频 / 相机</span>
textureRegistry<span>.unregisterTexture</span>(textureId); 
<span>// 2. 后注销</span>
<span>// 错误顺序:先注销,生产者还在写入 → 崩溃</span>

可以基于以下代码加深理解:

ArtTS侧:

<span>/*
 * 外接纹理注册演示插件(ArkTS 侧)
 *
 * 流程与引擎源码的对应:
 *   registerTexture(textureId)
 *     → 引擎内: OH_NativeImage_Create + AcquireNativeWindow + GetSurfaceId
 *     → 返回 SurfaceTextureEntry, getSurfaceId() 即"给生产者用的地址"
 *   AVPlayer 把 surfaceId 设上后, 解码出的每一帧都写进这条传送带
 *   引擎收到帧回调(OnNativeImageFrameAvailable)后通知 Flutter 重绘 Texture 控件
 *
 * 写法参照本机 video_player_ohos / camera_ohos 官方插件实现。
 */</span>
<span>import</span> { <span>MethodCall</span>, <span>MethodCallHandler</span>, <span>MethodChannel</span>, <span>MethodResult</span> } <span>from</span> <span>'@ohos/flutter_ohos'</span>;
<span>import</span> { <span>FlutterPlugin</span>, <span>FlutterPluginBinding</span> } <span>from</span> <span>'@ohos/flutter_ohos/src/main/ets/embedding/engine/plugins/FlutterPlugin'</span>;
<span>import</span> { <span>TextureRegistry</span> } <span>from</span> <span>'@ohos/flutter_ohos/src/main/ets/view/TextureRegistry'</span>;
<span>import</span> media <span>from</span> <span>'@ohos.multimedia.media'</span>;
<span>import</span> { <span>BusinessError</span> } <span>from</span> <span>'@kit.BasicServicesKit'</span>;
<span>const</span> <span>TAG</span>: <span>string</span> = <span>'NativeVideoTexture'</span>;
<span>/** 一路视频 = 一个生产者(AVPlayer) + 一条传送带(注册进引擎的外接纹理) */</span>
<span>class</span> <span>VideoTextureEntry</span> {
  <span>textureId</span>: <span>number</span> = -<span>1</span>;
  <span>surfaceId</span>: <span>string</span> = <span>''</span>;
  <span>avPlayer</span>: media.<span>AVPlayer</span> | <span>null</span> = <span>null</span>;
  <span>releasePending</span>: <span>boolean</span> = <span>false</span>; <span>// AVPlayer.release 是异步的, 记住"谁先谁后"</span>
}
<span>export</span> <span>class</span> <span>NativeVideoTexturePlugin</span> <span>implements</span> <span>FlutterPlugin</span>, <span>MethodCallHandler</span> {
  <span>private</span> <span>static</span> <span>readonly</span> <span>CHANNEL</span> = <span>'demo.native_video/texture'</span>;
  <span>private</span> <span>channel</span>: <span>MethodChannel</span> | <span>null</span> = <span>null</span>;
  <span>private</span> <span>textureRegistry</span>: <span>TextureRegistry</span> | <span>null</span> = <span>null</span>;
  <span>private</span> <span>entries</span>: <span>Map</span><<span>number</span>, <span>VideoTextureEntry</span>> = <span>new</span> <span>Map</span>();
  <span>// ---------- 插件生命周期 ----------</span>
  <span>onAttachedToEngine</span>(<span>binding</span>: <span>FlutterPluginBinding</span>): <span>void</span> {
    <span>this</span>.<span>textureRegistry</span> = binding.<span>getTextureRegistry</span>();
    <span>this</span>.<span>channel</span> = <span>new</span> <span>MethodChannel</span>(binding.<span>getBinaryMessenger</span>(), <span>NativeVideoTexturePlugin</span>.<span>CHANNEL</span>);
    <span>this</span>.<span>channel</span>.<span>setMethodCallHandler</span>(<span>this</span>);
  }
  <span>onDetachedFromEngine</span>(<span>binding</span>: <span>FlutterPluginBinding</span>): <span>void</span> {
    <span>this</span>.<span>channel</span>?.<span>setMethodCallHandler</span>(<span>null</span>);
    <span>this</span>.<span>channel</span> = <span>null</span>;
    <span>this</span>.<span>textureRegistry</span> = <span>null</span>;
  }
  <span>// ---------- MethodChannel 分发 ----------</span>
  <span>onMethodCall</span>(<span>call</span>: <span>MethodCall</span>, <span>result</span>: <span>MethodResult</span>): <span>void</span> {
    <span>switch</span> (call.<span>method</span>) {
      <span>case</span> <span>'create'</span>:
        <span>// uri 例: 'https://xxx.mp4'</span>
        <span>this</span>.<span>create</span>(call.<span>argument</span>(<span>'uri'</span>) <span>as</span> <span>string</span>, result);
        <span>break</span>;
      <span>case</span> <span>'play'</span>:
        <span>this</span>.<span>play</span>(call.<span>argument</span>(<span>'textureId'</span>) <span>as</span> <span>number</span>, result);
        <span>break</span>;
      <span>case</span> <span>'pause'</span>:
        <span>this</span>.<span>pause</span>(call.<span>argument</span>(<span>'textureId'</span>) <span>as</span> <span>number</span>, result);
        <span>break</span>;
      <span>case</span> <span>'dispose'</span>:
        <span>this</span>.<span>dispose</span>(call.<span>argument</span>(<span>'textureId'</span>) <span>as</span> <span>number</span>, result);
        <span>break</span>;
      <span>default</span>:
        result.<span>notImplemented</span>();
    }
  }
  <span>// ---------- 核心: 注册三步 ----------</span>
  <span>private</span> <span>async</span> <span>create</span>(<span>uri</span>: <span>string</span>, <span>result</span>: <span>MethodResult</span>): <span>Promise</span><<span>void</span>> {
    <span>const</span> registry = <span>this</span>.<span>textureRegistry</span>;
    <span>if</span> (registry === <span>null</span>) {
      result.<span>error</span>(<span>'NO_REGISTRY'</span>, <span>'TextureRegistry not ready'</span>, <span>null</span>);
      <span>return</span>;
    }
    <span>const</span> entry = <span>new</span> <span>VideoTextureEntry</span>();
    <span>// ★ 注册三步(引擎内完成 ①创建OH_NativeImage ②取NativeWindow ③取surfaceId ④注册到引擎)</span>
    entry.<span>textureId</span> = registry.<span>getTextureId</span>();
    <span>const</span> surfaceEntry = registry.<span>registerTexture</span>(entry.<span>textureId</span>); <span>// ①②③④ 一次完成</span>
    entry.<span>surfaceId</span> = surfaceEntry.<span>getSurfaceId</span>().<span>toString</span>();      <span>// 生产者要用的"地址"</span>
    <span>console</span>.<span>info</span>(<span>`<span>${TAG}</span> texture_id=<span>${entry.textureId}</span> surfaceId=<span>${entry.surfaceId}</span>`</span>);
    <span>// 生产者: AVPlayer 解码每一帧都写入 surfaceId 对应的传送带</span>
    <span>try</span> {
      <span>const</span> avPlayer = <span>await</span> media.<span>createAVPlayer</span>();
      entry.<span>avPlayer</span> = avPlayer;
      avPlayer.<span>on</span>(<span>'stateChange'</span>, <span>async</span> (<span>state</span>: <span>string</span>) => {
        <span>switch</span> (state) {
          <span>case</span> <span>'idle'</span>:
            avPlayer.<span>url</span> = uri; <span>// 设置源后进入 initialized</span>
            <span>break</span>;
          <span>case</span> <span>'initialized'</span>:
            <span>// ★ surfaceId 必须在 prepare 之前设置(官方 video_player_ohos 同款时机),</span>
            <span>//   晚了生产者没有窗口可写 → 黑屏, 日志会出现 No DlImage available</span>
            avPlayer.<span>surfaceId</span> = entry.<span>surfaceId</span>;
            avPlayer.<span>prepare</span>();
            <span>break</span>;
          <span>case</span> <span>'prepared'</span>:
            <span>// 画面尺寸确定后设置生产者窗口尺寸, 防拉伸/黑边</span>
            <span>// (画面尺寸变化时同样要调用, 配合 notifyTextureResizing)</span>
            registry.<span>setTextureBufferSize</span>(entry.<span>textureId</span>, avPlayer.<span>width</span>, avPlayer.<span>height</span>);
            avPlayer.<span>play</span>();
            <span>break</span>;
          <span>case</span> <span>'released'</span>:
            <span>// AVPlayer 真正释放完毕后才注销纹理(先停生产者的"完成信号")</span>
            <span>if</span> (entry.<span>releasePending</span>) {
              registry.<span>unregisterTexture</span>(entry.<span>textureId</span>); <span>// 传送带拆除</span>
              <span>this</span>.<span>entries</span>.<span>delete</span>(entry.<span>textureId</span>);
            }
            <span>break</span>;
          <span>default</span>:
            <span>break</span>;
        }
      });
      <span>this</span>.<span>entries</span>.<span>set</span>(entry.<span>textureId</span>, entry);
      result.<span>success</span>(entry.<span>textureId</span>); <span>// 把 textureId 交给 Dart 侧的 Texture 控件</span>
    } <span>catch</span> (e) {
      <span>// 创建失败也要把已注册的纹理拆掉, 否则引擎里留死纹理</span>
      registry.<span>unregisterTexture</span>(entry.<span>textureId</span>);
      result.<span>error</span>(<span>'CREATE_FAILED'</span>, <span>`<span>${e}</span>`</span>, <span>null</span>);
    }
  }
  <span>private</span> <span>play</span>(<span>textureId</span>: <span>number</span>, <span>result</span>: <span>MethodResult</span>): <span>void</span> {
    <span>const</span> entry = <span>this</span>.<span>entries</span>.<span>get</span>(textureId);
    entry?.<span>avPlayer</span>?.<span>play</span>();
    result.<span>success</span>(<span>true</span>);
  }
  <span>private</span> <span>pause</span>(<span>textureId</span>: <span>number</span>, <span>result</span>: <span>MethodResult</span>): <span>void</span> {
    <span>const</span> entry = <span>this</span>.<span>entries</span>.<span>get</span>(textureId);
    entry?.<span>avPlayer</span>?.<span>pause</span>();
    result.<span>success</span>(<span>true</span>);
  }
  <span>// ---------- 释放: 铁律 = 先停生产者, 再注销纹理 ----------</span>
  <span>private</span> <span>dispose</span>(<span>textureId</span>: <span>number</span>, <span>result</span>: <span>MethodResult</span>): <span>void</span> {
    <span>const</span> entry = <span>this</span>.<span>entries</span>.<span>get</span>(textureId);
    <span>if</span> (entry === <span>undefined</span> || entry.<span>avPlayer</span> === <span>null</span>) {
      <span>// 播放器已不在, 直接拆传送带</span>
      <span>this</span>.<span>textureRegistry</span>?.<span>unregisterTexture</span>(textureId);
      <span>this</span>.<span>entries</span>.<span>delete</span>(textureId);
      result.<span>success</span>(<span>true</span>);
      <span>return</span>;
    }
    <span>// 先停生产者: release 是异步的, 真正 released 后才在 stateChange 回调里</span>
    <span>// unregisterTexture —— 顺序反了会出现"纹理释放后还在访问"崩溃</span>
    entry.<span>releasePending</span> = <span>true</span>;
    entry.<span>avPlayer</span>.<span>release</span>().<span>catch</span>(<span>(<span>err: BusinessError</span>) =></span> {
      <span>console</span>.<span>error</span>(<span>`<span>${TAG}</span> release failed: <span>${err.code}</span> <span>${err.message}</span>`</span>);
      <span>this</span>.<span>textureRegistry</span>?.<span>unregisterTexture</span>(textureId);
      <span>this</span>.<span>entries</span>.<span>delete</span>(textureId);
    });
    result.<span>success</span>(<span>true</span>);
  }
}

Dart侧:

<span>///<span> 外接纹理演示 —— Dart 侧通道封装</span></span>
<span>///</span>
<span>///<span> ArkTS 侧完成注册后只回传一个 textureId,</span></span>
<span>///<span> Dart 侧用它构建 Texture 控件, 画面即从原生传送带流入 Flutter。</span></span>
<span>library</span>;
<span>import</span> <span>'dart:async'</span>;
<span>import</span> <span>'package:flutter/services.dart'</span>;
<span>///<span> 一路原生视频纹理。</span></span>
<span>///</span>
<span>///<span> 用法:</span></span>
<span>///<span> <span>```dart</span></span></span>
<span>///<span><span> final video = await NativeVideoTexture.create(uri);</span></span></span>
<span>///<span><span> video.play();</span></span></span>
<span>///<span><span> Texture(textureId: video.textureId);</span></span></span>
<span>///<span><span> video.dispose(); // 必须调用: 先停生产者再注销(插件内已保证顺序)</span></span></span>
<span>///<span><span> ```</span></span></span>
<span><span>class</span> <span>NativeVideoTexture</span> </span>{
  <span>static</span> <span>const</span> MethodChannel _channel =
      MethodChannel(<span>'demo.native_video/texture'</span>);
  <span>///<span> 注册一路纹理并创建 AVPlayer 生产者, 返回可用的 [NativeVideoTexture]。</span></span>
  <span>static</span> Future<NativeVideoTexture> create(<span>String</span> uri) <span>async</span> {
    <span>final</span> textureId =
        <span>await</span> _channel.invokeMethod<<span>int</span>>(<span>'create'</span>, {<span>'uri'</span>: uri});
    <span>if</span> (textureId == <span>null</span>) {
      <span>throw</span> PlatformException(
          code: <span>'CREATE_FAILED'</span>, message: <span>'textureId 为空, 注册失败'</span>);
    }
    <span>return</span> NativeVideoTexture._(textureId);
  }
  NativeVideoTexture._(<span>this</span>.textureId);
  <span>///<span> 传给 Texture 控件的 id。</span></span>
  <span>final</span> <span>int</span> textureId;
  <span>bool</span> _disposed = <span>false</span>;
  Future<<span>void</span>> play() =>
      _channel.invokeMethod(<span>'play'</span>, {<span>'textureId'</span>: textureId});
  Future<<span>void</span>> pause() =>
      _channel.invokeMethod(<span>'pause'</span>, {<span>'textureId'</span>: textureId});
  <span>///<span> 释放: 插件内先 release 生产者(AVPlayer), 等 released 后再注销纹理。</span></span>
  <span>///</span>
  <span>///<span> 顺序反了 = "快递箱扔了还有人往里放东西" → 纹理访问崩溃。</span></span>
  Future<<span>void</span>> dispose() <span>async</span> {
    <span>if</span> (_disposed) <span>return</span>;
    _disposed = <span>true</span>;
    <span>await</span> _channel.invokeMethod(<span>'dispose'</span>, {<span>'textureId'</span>: textureId});
  }
}

<span>/// 外接纹理演示页:原生视频画面显示在 Flutter 里</span>
<span>///</span>
<span>/// 页面结构: 按钮触发注册 → Texture 控件展示 → 退出时按"先停生产者再注销"释放。</span>
import <span>'package:flutter/material.dart'</span>;
import <span>'package:flutter/services.dart'</span>;
import <span>'native_video_texture.dart'</span>;
<span><span>class</span> <span>TextureDemoPage</span> <span>extends</span> <span>StatefulWidget</span> </span>{
  <span>const</span> <span>TextureDemoPage</span>({super.key, this.uri = <span>'https://media.w3.org/2010/05/sintel/trailer.mp4'</span>});
  <span>/// 换成你自己的视频地址</span>
  <span>final</span> String uri;
  @override
  State<TextureDemoPage> <span>createState</span>() => <span>_TextureDemoPageState</span>();
}
<span><span>class</span> <span>_TextureDemoPageState</span> <span>extends</span> <span>State</span><<span>TextureDemoPage</span>> </span>{
  NativeVideoTexture? _video;
  String? _error;
  Future<<span>void</span>> <span>_create</span>() async {
    <span>setState</span>(() => _error = <span>null</span>);
    <span>try</span> {
      <span>final</span> video = await NativeVideoTexture.<span>create</span>(widget.uri);
      <span>setState</span>(() => _video = video);
    } on <span>PlatformException catch </span>(e) {
      <span>// 对应黑屏排查图第 1 步: 日志里搜不到 RegisterExternalTexture</span>
      <span>// 说明注册链路就断了, 看这里拿到什么错误</span>
      <span>setState</span>(() => _error = <span>'${e.code}: ${e.message}'</span>);
    }
  }
  @override
  <span>void</span><span> dispose</span>() {
    <span>// 铁律: 先停生产者(AVPlayer.release), 再注销纹理(unregisterTexture)</span>
    <span>// —— 顺序在插件 dispose 内部保证, 这里只管调用</span>
    _video?.<span>dispose</span>();
    super.<span>dispose</span>();
  }
  @override
  <span>Widget build</span>(BuildContext context) {
    <span>final</span> video = _video;
    <span>return</span><span> Scaffold</span>(
    <span>  appBar</span>: <span>AppBar</span>(<span>title</span>: <span>const</span><span> Text</span>(<span>'外接纹理演示'</span>)),
    <span>  body</span>: <span>Center</span>(
      <span>  child</span>: <span>Column</span>(
        <span>  mainAxisAlignment</span>: MainAxisAlignment.center,
        <span>  children</span>: [
            <span>if</span><span> </span>(video != <span>null</span>)
              // ★ 画面入口: textureId 对上, 传送带上的帧就会画进这一块
              <span>AspectRatio</span>(
              <span>  aspectRatio</span>: <span>16</span> / <span>9</span>,
              <span>  child</span>: <span>Texture</span>(<span>textureId</span>: video.textureId),
              )
            <span>else</span><span> if </span>(_error != <span>null</span>)
              <span>Padding</span>(
              <span>  padding</span>: <span>const</span> EdgeInsets.<span>all</span>(<span>24</span>),
              <span>  child</span>: <span>Text</span>(<span>'创建失败: $_error'</span>,
                  <span>  style</span>: <span>const</span><span> TextStyle</span>(<span>color</span>: Colors.red)),
              )
            <span>else</span>
              <span>const</span><span> Text</span>(<span>'点击下方按钮注册纹理并开始播放'</span>),
            <span>const</span><span> SizedBox</span>(<span>height</span>: <span>24</span>),
            <span>if</span><span> </span>(video != <span>null</span>)
              <span>Row</span>(
              <span>  mainAxisAlignment</span>: MainAxisAlignment.center,
              <span>  children</span>: [
                  <span>IconButton</span>(
                    <span>  onPressed</span>: video.play,<span> icon</span>: <span>const</span><span> Icon</span>(Icons.play_arrow)),
                  <span>IconButton</span>(
                    <span>  onPressed</span>: video.pause,<span> icon</span>: <span>const</span><span> Icon</span>(Icons.pause)),
                ],
              )
            <span>else</span>
              <span>FilledButton</span>(<span>onPressed</span>: _create,<span> child</span>: <span>const</span><span> Text</span>(<span>'创建纹理 + 播放'</span>)),
          ],
        ),
      ),
    );
  }
}

EntryAbility:

<span>import</span> { <span>FlutterAbility</span>, <span>FlutterEngine</span> } <span>from</span> <span>'@ohos/flutter_ohos'</span>;
<span>import</span> { <span>GeneratedPluginRegistrant</span> } <span>from</span> <span>'../plugins/GeneratedPluginRegistrant'</span>;
<span>import</span> { <span>NativeVideoTexturePlugin</span> } <span>from</span> <span>'../plugins/NativeVideoTexturePlugin'</span>;
<span>export</span> <span>default</span> <span>class</span> <span>EntryAbility</span> <span>extends</span> <span>FlutterAbility</span> {
  <span>configureFlutterEngine</span>(<span>flutterEngine: FlutterEngine</span>) {
    <span>super</span>.<span>configureFlutterEngine</span>(flutterEngine)
    <span>GeneratedPluginRegistrant</span>.<span>registerWith</span>(flutterEngine)
    flutterEngine.<span>getPlugins</span>()?.<span>add</span>(<span>new</span> <span>NativeVideoTexturePlugin</span>())   <span>// ← 加这行</span>
  }
}

三、外接纹理在哪些场景会用到,常见的问题有哪些?

以下业务场景通常会用到外接纹理,场景:(1)视频播放(2)相机预览(3)动画播放(4)WebView(5)直播 SDK等

可以按以下现象初步判断下问题在哪:

看到的现象可能的原因
视频、相机画面黑屏纹理创建失败 / 生产者没产出帧
画面第一帧后不更新帧闸门开启 / Surface 销毁 / onInactive 被误触发
画面卡顿、丢帧消费过慢 / 跳帧
画面拉伸、变形尺寸变更未收敛
应用闪退纹理释放后还在访问
退后台后还在耗电可见区域监控未启用

四、黑屏或不显示 如何排查

cke_92096.png​常见问题原因及修复方法

原因怎么修
纹理未注册检查注册代码
NativeImage 创建失败检查系统资源
生产者没产出帧确保视频已开始播放、相机已启动
Texture 控件 size 为 0检查 Widget 布局
surfaceId 不对确认 surfaceId 传递正确

五、卡顿 / 丢帧 如何排查

cke_105424.png​四步进行排查:

(1)搜 skip one frame(slow consumer)搜到说明消费过慢引擎在跳帧,需要检查 Raster 线程是否被其他任务阻塞,同时检查 buffer_queue_size 是否过小。

(2)搜 MarkNewFrameAvailable avail-seq:avail-seq 不增长,是生产者没产出帧;avail-seq 增长但 paint-seq 不增长,那就是 Raster 线程卡了。

(3)搜 GpuReclaim:搜到说明 GPU 回收导致中断了。

(4)搜 get error buffer queue size:搜到说明缓冲队列异常(超过 100)。

六、拉伸 / 变形 如何排查

搜 size change 相关日志:

日志含义
size change took N frames尺寸变更在 N 帧内完成,正常
stop size change state: frame > 10尺寸变更超过 10 帧,异常
direct release size changed buffer缓冲尺寸变了但绘制区域没变,防拉伸

修复方法:确保 setTextureBufferSize 和 notifyTextureResizing 调用一致。

七、以下为日志关键字速查表

关键字含义程度
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纹理析构

外接纹理相关的内容比较多,本次主要是讲个概念和基本的排查方法,希望能帮助到大家~更进一步的排查方法就需要依赖工具了,相关专题最近也在进行规划,大家可以持续关注,后续会持续更新:

小伙伴们记得点赞+关注

关注 CPF-Flutter 社区

“AI再牛,技术不能丢”