Flutter 纹理缓存(Texture Registry):外部纹理(Video/Camera)的零拷贝渲染

好的,下面开始讲解 Flutter 纹理缓存(Texture Registry)以及如何利用它实现外部纹理(Video/Camera)的零拷贝渲染。

引言:渲染的本质与性能瓶颈

在深入 Flutter 纹理缓存之前,我们需要理解图形渲染的本质。在移动设备上,无论是绘制 UI 界面还是播放视频,最终都需要将像素数据呈现到屏幕上。这个过程涉及多个步骤,包括:

  1. 数据准备: CPU 处理图像/视频数据,将其转换为像素格式(例如,RGBA)。
  2. 数据传输: 将像素数据从 CPU 内存传输到 GPU 内存。
  3. 渲染: GPU 根据像素数据和渲染指令,在屏幕上绘制图像。

数据传输是性能瓶颈之一。传统的渲染方式通常涉及将数据从 CPU 复制到 GPU。这个复制过程消耗时间和资源,尤其是在处理高分辨率视频或实时相机数据时。零拷贝渲染旨在避免这种复制,从而提高性能。

Flutter 纹理缓存(Texture Registry)的作用

Flutter 纹理缓存(Texture Registry)是 Flutter 引擎提供的一项机制,用于管理由平台原生代码创建和管理的纹理。它允许原生代码将纹理句柄(纹理 ID)注册到 Flutter 引擎,然后 Flutter 可以使用这些句柄来渲染纹理,而无需复制纹理数据。

Texture Registry 的主要作用:

  • 外部纹理集成: 允许 Flutter 应用集成来自原生平台(Android/iOS)的纹理,例如相机预览、视频解码输出或自定义 OpenGL 渲染结果。
  • 零拷贝渲染: 通过直接使用原生纹理句柄,避免了 CPU 和 GPU 之间的数据复制,从而实现零拷贝渲染。
  • 性能优化: 减少了内存占用和 CPU 负载,提高了渲染性能,尤其适用于需要处理大量图像数据的场景。

实现零拷贝渲染的步骤

要使用 Flutter 纹理缓存实现零拷贝渲染,通常需要以下步骤:

  1. 原生平台代码创建纹理: 在 Android 或 iOS 平台,使用相应的 API 创建纹理对象(例如,OpenGL ES 纹理)。
  2. 注册纹理到 Flutter 引擎: 将纹理句柄(纹理 ID)注册到 Flutter 的 Texture Registry。
  3. Flutter 代码使用纹理: 在 Flutter 代码中使用 Texture 组件,并指定注册的纹理 ID。
  4. 原生平台代码更新纹理: 原生平台代码负责更新纹理数据(例如,从相机预览或视频解码器获取新的帧)。
  5. Flutter 自动渲染更新后的纹理: Flutter 引擎会自动渲染更新后的纹理,无需额外的代码干预。

Android 平台示例:相机预览的零拷贝渲染

下面是一个 Android 平台使用 Texture Registry 实现相机预览零拷贝渲染的示例。

1. Android 原生代码 (Kotlin):

import android.Manifest
import android.content.Context
import android.content.pm.PackageManager
import android.graphics.SurfaceTexture
import android.hardware.camera2.*
import android.os.Handler
import android.os.HandlerThread
import android.util.Log
import android.view.Surface
import androidx.core.app.ActivityCompat
import io.flutter.plugin.common.BinaryMessenger
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import io.flutter.view.TextureRegistry

class CameraHandler(
    private val context: Context,
    private val messenger: BinaryMessenger,
    private val textureRegistry: TextureRegistry
) : MethodCallHandler {

    private val channel = MethodChannel(messenger, "camera_channel")
    private var cameraDevice: CameraDevice? = null
    private var captureSession: CameraCaptureSession? = null
    private var textureEntry: TextureRegistry.SurfaceTextureEntry? = null
    private var surfaceTexture: SurfaceTexture? = null
    private var previewSize: android.util.Size? = null
    private var backgroundThread: HandlerThread? = null
    private var backgroundHandler: Handler? = null

    init {
        channel.setMethodCallHandler(this)
    }

    override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
        when (call.method) {
            "initialize" -> {
                initializeCamera(result)
            }
            "dispose" -> {
                disposeCamera(result)
            }
            else -> {
                result.notImplemented()
            }
        }
    }

    private fun initializeCamera(result: MethodChannel.Result) {
        startBackgroundThread()

        val cameraManager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager
        val cameraId = cameraManager.cameraIdList[0] // Use the first camera

        try {
            if (ActivityCompat.checkSelfPermission(context, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
                result.error("PERMISSION_DENIED", "Camera permission not granted", null)
                return
            }

            textureEntry = textureRegistry.createSurfaceTexture()
            surfaceTexture = textureEntry?.surfaceTexture()
            surfaceTexture?.setDefaultBufferSize(640, 480) // Adjust size as needed

            cameraManager.openCamera(cameraId, object : CameraDevice.StateCallback() {
                override fun onOpened(camera: CameraDevice) {
                    cameraDevice = camera
                    createCameraPreviewSession()
                    val textureId = textureEntry?.id()
                    result.success(textureId)
                }

                override fun onDisconnected(camera: CameraDevice) {
                    camera.close()
                    cameraDevice = null
                    result.error("CAMERA_DISCONNECTED", "Camera disconnected", null)
                }

                override fun onError(camera: CameraDevice, error: Int) {
                    camera.close()
                    cameraDevice = null
                    result.error("CAMERA_ERROR", "Camera error: $error", null)
                }
            }, backgroundHandler)
        } catch (e: CameraAccessException) {
            result.error("CAMERA_ACCESS_ERROR", "Failed to access camera: ${e.message}", null)
        } catch (e: SecurityException) {
            result.error("CAMERA_PERMISSION_ERROR", "Camera permission required", null)
        }
    }

    private fun createCameraPreviewSession() {
        try {
            val surface = Surface(surfaceTexture)

            val captureRequestBuilder = cameraDevice?.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW)?.apply {
                addTarget(surface)
            }

            cameraDevice?.createCaptureSession(listOf(surface), object : CameraCaptureSession.StateCallback() {
                override fun onConfigured(session: CameraCaptureSession) {
                    captureSession = session
                    try {
                        captureRequestBuilder?.build()?.let {
                            session.setRepeatingRequest(it, null, backgroundHandler)
                        }
                    } catch (e: CameraAccessException) {
                        Log.e("CameraHandler", "Failed to start camera preview: ${e.message}")
                    }
                }

                override fun onConfigureFailed(session: CameraCaptureSession) {
                    Log.e("CameraHandler", "Failed to configure camera session")
                }
            }, backgroundHandler)
        } catch (e: CameraAccessException) {
            Log.e("CameraHandler", "Failed to create camera preview session: ${e.message}")
        }
    }

    private fun disposeCamera(result: MethodChannel.Result) {
        stopBackgroundThread()
        captureSession?.close()
        captureSession = null
        cameraDevice?.close()
        cameraDevice = null
        textureEntry?.release()
        textureEntry = null
        surfaceTexture?.release()
        surfaceTexture = null

        result.success(null)
    }

    private fun startBackgroundThread() {
        backgroundThread = HandlerThread("CameraBackground").also { it.start() }
        backgroundHandler = Handler(backgroundThread?.looper)
    }

    private fun stopBackgroundThread() {
        backgroundThread?.quitSafely()
        try {
            backgroundThread?.join()
            backgroundThread = null
            backgroundHandler = null
        } catch (e: InterruptedException) {
            Log.e("CameraHandler", "Interrupted while stopping background thread: ${e.message}")
        }
    }
}

2. Flutter 代码 (Dart):

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

class CameraPreview extends StatefulWidget {
  const CameraPreview({Key? key}) : super(key: key);

  @override
  _CameraPreviewState createState() => _CameraPreviewState();
}

class _CameraPreviewState extends State<CameraPreview> {
  static const platform = MethodChannel('camera_channel');
  int? _textureId;

  @override
  void initState() {
    super.initState();
    _initializeCamera();
  }

  Future<void> _initializeCamera() async {
    try {
      final int textureId = await platform.invokeMethod('initialize');
      setState(() {
        _textureId = textureId;
      });
    } on PlatformException catch (e) {
      print("Failed to initialize camera: ${e.message}");
    }
  }

  @override
  void dispose() {
    _disposeCamera();
    super.dispose();
  }

  Future<void> _disposeCamera() async {
    try {
      await platform.invokeMethod('dispose');
    } on PlatformException catch (e) {
      print("Failed to dispose camera: ${e.message}");
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Camera Preview'),
      ),
      body: _textureId == null
          ? const Center(child: CircularProgressIndicator())
          : Center(
              child: SizedBox(
                width: 640, // Adjust as needed
                height: 480, // Adjust as needed
                child: Texture(textureId: _textureId!),
              ),
            ),
    );
  }
}

3. Flutter Plugin (Dart):

import 'package:flutter/embedding/engine/plugins/FlutterPlugin.dart';
import 'package:flutter/plugin_platform_interface/plugin_platform_interface.dart';
import 'package:flutter/services.dart';
import 'package:camera_example/camera_example.dart';

class CameraExamplePlugin extends FlutterPlugin implements CameraExample {
  static const MethodChannel _channel = MethodChannel('camera_example');

  @override
  void onAttachedToEngine(FlutterPluginBinding binding) {
    _channel.setMethodCallHandler((call) async {
      // Add any method call handling needed here,
      // or delegate to an instance of CameraExample
    });
    final cameraHandler = CameraHandler(binding.getApplicationContext(), binding.getBinaryMessenger(), binding.getTextureRegistry());
  }

  @override
  void onDetachedFromEngine(FlutterPluginBinding binding) {
     _channel.setMethodCallHandler(null);
  }

  Future<String?> getPlatformVersion() async {
    final String? version = await _channel.invokeMethod('getPlatformVersion');
    return version;
  }
}

class CameraHandler {
    CameraHandler(applicationContext, binaryMessenger, textureRegistry);
}

abstract class CameraExample extends PlatformInterface {
   CameraExample({super.tokenProvider});

   static final Object _token = Object();

  static CameraExample get instance => _instance;

  static set instance(CameraExample instance) {
    PlatformInterface.verifyToken(instance, _token);
    _instance = instance;
  }

  static late final CameraExample _instance = CameraExampleImpl();

  Future<String?> getPlatformVersion() {
    throw UnimplementedError('platformVersion() has not been implemented.');
  }
}

class CameraExampleImpl implements CameraExample {
  static const MethodChannel _channel = MethodChannel('camera_example');

  @override
  Future<String?> getPlatformVersion() async {
    final String? version = await _channel.invokeMethod('getPlatformVersion');
    return version;
  }
}

代码解释:

  • Android 原生代码:
    • CameraHandler 类负责处理 Flutter 调用的方法。
    • initializeCamera 方法:
      • 创建 SurfaceTexture 并将其注册到 TextureRegistrytextureRegistry.createSurfaceTexture() 返回一个 SurfaceTextureEntry,包含了纹理 ID (textureId)。
      • 打开相机,并创建一个相机预览会话,将 SurfaceTexture 作为预览目标。
      • 将纹理 ID 返回给 Flutter。
    • disposeCamera 方法:
      • 释放相机资源和 SurfaceTexture
    • 后台线程用于处理相机操作,避免阻塞 UI 线程。
  • Flutter 代码:
    • CameraPreview StatefulWidget 负责显示相机预览。
    • _initializeCamera 方法:
      • 调用 Android 原生代码的 initialize 方法,获取纹理 ID。
      • 使用 setState 更新 _textureId,触发 Widget 重建。
    • _disposeCamera 方法:
      • 调用 Android 原生代码的 dispose 方法,释放相机资源。
    • Texture Widget 使用 _textureId 来显示相机预览。
  • Flutter Plugin
    • 注册MethodChannel,用于Flutter和Native代码之间的通信
    • 创建CameraHandler,处理具体的相机业务逻辑

关键点:

  • TextureRegistry.createSurfaceTexture(): 这是创建纹理并将其注册到 Flutter 引擎的关键方法。它返回一个 SurfaceTextureEntry 对象,该对象包含了纹理 ID 和 SurfaceTexture 对象。
  • Texture(textureId: _textureId!): Texture Widget 使用纹理 ID 来显示纹理。Flutter 引擎会自动查找注册的纹理并进行渲染。
  • 原生平台更新纹理数据: 相机预览数据直接写入到 SurfaceTexture 中,无需复制到 Flutter 内存。Flutter 引擎会自动读取 SurfaceTexture 中的数据并进行渲染。

iOS 平台示例 (Swift):

iOS 平台的实现方式类似,但使用不同的 API。

1. iOS 原生代码 (Swift):

import Flutter
import UIKit
import AVFoundation

class CameraHandler: NSObject, FlutterPlugin, AVCaptureVideoDataOutputSampleBufferDelegate {
    private var registrar: FlutterPluginRegistrar
    private var channel: FlutterMethodChannel
    private var textureRegistry: FlutterTextureRegistry
    private var captureSession: AVCaptureSession?
    private var videoOutput: AVCaptureVideoDataOutput?
    private var textureId: Int64?
    private var pixelBuffer: CVPixelBuffer?

    init(registrar: FlutterPluginRegistrar) {
        self.registrar = registrar
        self.channel = FlutterMethodChannel(name: "camera_channel", binaryMessenger: registrar.messenger())
        self.textureRegistry = registrar.textures()
        super.init()
        channel.setMethodCallHandler(handle)
    }

    public static func register(with registrar: FlutterPluginRegistrar) {
        let instance = CameraHandler(registrar: registrar)
        registrar.register(instance, with: "camera_plugin")
    }

    func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
        switch call.method {
        case "initialize":
            initializeCamera(result: result)
        case "dispose":
            disposeCamera(result: result)
        default:
            result(FlutterMethodNotImplemented)
        }
    }

    private func initializeCamera(result: @escaping FlutterResult) {
        captureSession = AVCaptureSession()
        captureSession?.sessionPreset = .medium // Adjust as needed

        guard let device = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back) else {
            result(FlutterError(code: "CAMERA_NOT_FOUND", message: "No camera found", details: nil))
            return
        }

        do {
            let input = try AVCaptureDeviceInput(device: device)
            if (captureSession?.canAddInput(input) == true) {
                captureSession?.addInput(input)
            } else {
                result(FlutterError(code: "CAMERA_ADD_INPUT_FAILED", message: "Failed to add input", details: nil))
                return
            }

            videoOutput = AVCaptureVideoDataOutput()
            videoOutput?.videoSettings = [kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32BGRA]
            videoOutput?.setSampleBufferDelegate(self, queue: DispatchQueue.global(qos: .userInitiated))

            if (captureSession?.canAddOutput(videoOutput!) == true) {
                captureSession?.addOutput(videoOutput!)
            } else {
                result(FlutterError(code: "CAMERA_ADD_OUTPUT_FAILED", message: "Failed to add output", details: nil))
                return
            }

            textureId = textureRegistry.register(self)
            result(textureId)

            captureSession?.startRunning()
        } catch {
            result(FlutterError(code: "CAMERA_ERROR", message: "Camera error: (error)", details: nil))
        }
    }

    private func disposeCamera(result: @escaping FlutterResult) {
        captureSession?.stopRunning()
        textureRegistry.unregisterTexture(textureId!)
        textureId = nil
        captureSession = nil
        videoOutput = nil
        pixelBuffer = nil
        result(nil)
    }

    // MARK: - AVCaptureVideoDataOutputSampleBufferDelegate

    func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
        pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer)
        textureRegistry.textureFrameAvailable(textureId!)
    }
}

extension CameraHandler: FlutterTexture {
    func copyPixelBuffer() -> Unmanaged<CVPixelBuffer>? {
        if let pixelBuffer = pixelBuffer {
            return Unmanaged.passRetained(pixelBuffer)
        } else {
            return nil
        }
    }
}

2. Flutter 代码 (Dart):

Flutter 代码与 Android 示例基本相同,只需要修改 MethodChannel 的名字即可。

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

class CameraPreview extends StatefulWidget {
  const CameraPreview({Key? key}) : super(key: key);

  @override
  _CameraPreviewState createState() => _CameraPreviewState();
}

class _CameraPreviewState extends State<CameraPreview> {
  static const platform = MethodChannel('camera_channel'); // iOS Channel Name
  int? _textureId;

  @override
  void initState() {
    super.initState();
    _initializeCamera();
  }

  Future<void> _initializeCamera() async {
    try {
      final int textureId = await platform.invokeMethod('initialize');
      setState(() {
        _textureId = textureId;
      });
    } on PlatformException catch (e) {
      print("Failed to initialize camera: ${e.message}");
    }
  }

  @override
  void dispose() {
    _disposeCamera();
    super.dispose();
  }

  Future<void> _disposeCamera() async {
    try {
      await platform.invokeMethod('dispose');
    } on PlatformException catch (e) {
      print("Failed to dispose camera: ${e.message}");
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Camera Preview'),
      ),
      body: _textureId == null
          ? const Center(child: CircularProgressIndicator())
          : Center(
              child: SizedBox(
                width: 640, // Adjust as needed
                height: 480, // Adjust as needed
                child: Texture(textureId: _textureId!),
              ),
            ),
    );
  }
}

代码解释:

  • iOS 原生代码:
    • CameraHandler 类实现了 FlutterPluginAVCaptureVideoDataOutputSampleBufferDelegate 协议。
    • initializeCamera 方法:
      • 配置 AVCaptureSession 以捕获相机数据。
      • 使用 textureRegistry.register(self) 注册 CameraHandler 作为纹理提供者,并获取纹理 ID。
      • 启动相机捕获会话。
    • disposeCamera 方法:
      • 停止相机捕获会话。
      • 使用 textureRegistry.unregisterTexture(textureId!) 注销纹理。
    • captureOutput 方法:
      • 当捕获到新的相机帧时,此方法被调用。
      • CMSampleBuffer 转换为 CVPixelBuffer
      • 调用 textureRegistry.textureFrameAvailable(textureId!) 通知 Flutter 引擎有新的纹理帧可用。
    • copyPixelBuffer 方法:
      • 实现了 FlutterTexture 协议的要求,返回 CVPixelBuffer 的 Unmanaged 指针。Flutter 引擎使用此指针来读取纹理数据。

表格:Android 和 iOS 平台关键 API 对比

特性 Android iOS
纹理创建 TextureRegistry.createSurfaceTexture() TextureRegistry.register(self)
纹理 ID 获取 SurfaceTextureEntry.id() textureId (register 方法的返回值)
纹理更新通知 N/A (SurfaceTexture 自动更新) textureRegistry.textureFrameAvailable()
纹理数据提供 N/A (SurfaceTexture 自动提供) copyPixelBuffer()
纹理解除注册 SurfaceTextureEntry.release() textureRegistry.unregisterTexture()
纹理数据格式 灵活 (取决于 SurfaceTexture 的配置) CVPixelBuffer (通常为 BGRA)
相机 API Camera2 API AVFoundation

其他注意事项

  • 纹理大小: 确保 Flutter 代码中 Texture Widget 的大小与原生平台创建的纹理大小匹配。如果不匹配,可能会导致图像拉伸或变形。
  • 纹理格式: 了解原生平台创建的纹理格式(例如,RGBA、BGRA、NV12)。Flutter 引擎会自动处理常见的纹理格式,但如果使用自定义格式,可能需要进行额外的处理。
  • 资源释放: 在不再需要纹理时,务必释放原生平台的纹理资源,并从 Flutter 纹理缓存中注销纹理。否则,可能会导致内存泄漏。
  • 线程安全: 确保在正确的线程上执行纹理操作。例如,在 Android 平台上,相机操作应该在后台线程上执行。在 iOS 平台上,captureOutput 方法在后台线程上调用。
  • 错误处理: 在原生平台代码中,添加适当的错误处理机制,以便在出现错误时能够及时通知 Flutter 代码。

利用纹理缓存实现视频播放器的零拷贝渲染

除了相机预览,纹理缓存还可以用于实现视频播放器的零拷贝渲染。具体步骤如下:

  1. 原生平台代码解码视频帧: 使用 Android 的 MediaCodec 或 iOS 的 AVAssetReader 解码视频帧。
  2. 将解码后的帧数据写入纹理: 将解码后的帧数据写入 OpenGL ES 纹理。
  3. 注册纹理到 Flutter 引擎: 将纹理句柄注册到 Flutter 的 Texture Registry。
  4. Flutter 代码使用纹理: 在 Flutter 代码中使用 Texture 组件,并指定注册的纹理 ID。

这种方式可以避免将解码后的视频帧数据从原生平台复制到 Flutter 内存,从而提高视频播放器的性能。

优势与局限性

优势:

  • 性能提升: 显著减少了 CPU 和 GPU 之间的数据复制,提高了渲染性能。
  • 内存优化: 降低了内存占用,尤其是在处理高分辨率图像或视频时。
  • 更好的用户体验: 更流畅的动画和视频播放,提高了用户体验。

局限性:

  • 平台依赖: 需要编写平台特定的原生代码。
  • 复杂性: 实现起来比传统的渲染方式更复杂。
  • 调试难度: 调试跨平台代码可能比较困难。

总结与未来方向

Flutter 纹理缓存是一项强大的特性,它允许 Flutter 应用集成来自原生平台的纹理,并实现零拷贝渲染。这对于需要处理大量图像数据的应用(例如,相机应用、视频编辑应用和游戏)来说,是一个重要的性能优化手段。

在未来,我们可以期待 Flutter 引擎提供更高级的纹理管理 API,简化零拷贝渲染的实现过程。例如,可以考虑提供一个统一的接口,允许开发者以更抽象的方式注册和管理纹理,而无需关心底层的平台细节。此外,还可以探索使用 GPU 共享内存等技术,进一步优化零拷贝渲染的性能。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注