Flutter 多引擎渲染,外接纹理实践
挺多读者评论上都对 multiple-flutters Flutter 多引擎渲染有兴趣,希望能有更多的资料可供参考。
笔者后续大概有两个方向的文章,一是继续介绍我们在 Flutter 多引擎渲染里做了些什么,踩了哪些坑,二是从零开始讲解如何实现 Flutter 多引擎方案。
本篇还是介绍做了些什么。
前言
在 Flutter add to App 混合开发中,资源在 Native 和 Flutter 重复加载,导致内存 double 的性能问题属于司空见惯的现象了。
当然,这个是有“成熟”的解决方案的,各大厂在 Flutter 单引擎时代中,也都是推荐用 Texture
外接纹理的方式来缓解内存压力。
理论上,多引擎应该比单引擎更需要外接纹理方案,毕竟在多引擎的机制下,FlutterEngine 和 FlutterEngine 之间也是不共享资源的,更容易导致内存浪费的问题。
那在 Flutter 多引擎上我们也能用 Texture
外接纹理吗?
答案当然是可以,但还是有一些使用上的不同。
方案
先看一下 Texture
在 Flutter 上是如何使用的,其实很简单,只要有 textureId
即可显示
Texture(textureId: textureId)
那 textureId 怎么来的呢?以前一般是特定的 channel
返回特定场景的 textureId
。比如视频播放,画布渲染等。
但在 Flutter 多引擎组件化的思路上,我们希望这个能力是通用的,不局限于场景,对 native 开发调用者来说不再关心 textureId
这件事,对 Flutter 组件开发者来说,也不再关心是 textureId
的来源,拿来即渲染即可。
定义
- name: TestImage
options:
note: GUI 图像外接纹理测试
autolayout: true
init:
- { name: imageList, type: List<Image>, note: 图像列表 }
properties:
- { name: "image", type: Image, note: 图像 }
如上图所示,我们新增了一种自定义 Image
对象的声明类型,它在 iOS 里对标 UIImage
,在 Android 里对标 Bitmap
。
那组件在 Native 使用上,就如下方式:
iOS
FGUITestImage *image = [[FGUITestImage alloc] initWithMaker:^(FGUIImageInitConfig * _Nonnull make) {
UIImage *test1 = [GDVEResource imageNamed:@"video_canvas_bg_blur_ gaussian_selected"];
UIImage *test2 = [GDVEResource imageNamed:@"video_menu_background_normal"];
UIImage *test3 = [GDVEResource imageNamed:@"video_canvas_bg_blur_none_normal"];
UIImage *test4 = [GDVEResource imageNamed:@"video_template_main_track_add"];
make.imageList = @[test1, test2, test3, test4];
} hostVC:self];
image.image = [GDVEResource imageNamed:@"video_template_video_track_icon_image"];
[self.view addSubview:image.view];
[image.view mas_makeConstraints:^(MASConstraintMaker *make) {
make.height.equalTo(@500);
make.center.width.equalTo(self.view);
}];
Android
val view1 = findViewById<FGUIImage>(R.id.test_image)
view1.let {
it.init(supportFragmentManager)
var image = BitmapFactory.decodeResource(getResources(),
R.drawable.bg_clear_guide_2
)
it.setImage(image)
}
可以看到,对 native 说就是传自身的对象即可,没有多余的开发成本。
实现
那如何做到的呢,原理也十分简单,大概分为2个部分:
Image 模型转换
有看过笔者前几篇文章的同学,应该对模型转换就比较熟悉了,用于抹平各端类型差异,且提供 model
而不是 map
的确定出入参。
Image 比较特殊一点,毕竟在 Flutter 侧只需要 textureId
,那其实我们是构建一个抽象的图片对象(宽高用于 Flutter 约束图片大小,这个很重要,可以看测试结论)。
/// 图像外接纹理
class GDImageTexture {
/// 纹理 ID
int? textureId;
/// 图像宽度
double? width;
/// 图像高度
double? height;
GDImageTexture(Map? map) : super() {
if (map == null) {
return;
}
textureId = map["textureId"] as int?;
width = map["width"] as double?;
height = map["height"] as double?;
}
...
}
那剩下的工作就是在传输过程中,将 UIImage
、 Bitmap
转换成如上对象即可。
iOS:
/// 「通用」获取 FGUIComponentImage 对象
- (NSDictionary *)fetchComponentImage:(UIImage *)image {
// FGUIComponentImageTexture 就是 Texture 实现
FGUIComponentImageTexture *imageTexture = [[FGUIComponentImageTexture alloc] initWithImage:image];
[self.imageTextures addObject:imageTexture];
int64_t textureId = [[self.registrar textures] registerTexture:imageTexture];
return @{
@"textureId": @(textureId),
@"width": @(image.size.width),
@"height": @(image.size.height)
};
}
...
Android:
/**
* 「通用」获取 FGUIComponentImage 对象
*/
private fun fetchComponentImage(@NonNull image: Bitmap): Map<String, Any> {
var surfaceEntry = textureRegistry.createSurfaceTexture()
surfaceEntryList.add(surfaceEntry)
var textureId = surfaceEntry.id()
var surface = Surface(surfaceEntry.surfaceTexture().apply {
setDefaultBufferSize(image.width, image.height)
})
var rect = Rect(0, 0, image.width, image.height)
val canvas = surface.lockCanvas(rect)
canvas.drawBitmap(image, rect, rect, null)
image.recycle()
surface.unlockCanvasAndPost(canvas)
var result = mutableMapOf<String, Any>()
result["textureId"] = textureId
result["width"] = image.width.toFloat()
result["height"] = image.height.toFloat()
return result
}
如上所示,提供一个工具转换方法,在传输过程中还是用 map
,在 Flutter 侧转换成 GDImageTexture
模型即可,当然这一切都是用 FGUIComponentAPI
进行的自动生成,对开发者来说直接定义 yaml
文件即可。
实现 Texture
然后我们来看一下外接纹理如何实现的,这个其实跟单引擎用的也没什么差别,简单的放一下双端代码。
iOS:
static uint32_t bitmapInfoWithPixelFormatType(OSType inputPixelFormat, bool hasAlpha) {
if (inputPixelFormat == kCVPixelFormatType_32BGRA) {
uint32_t bitmapInfo = kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Host;
if (!hasAlpha) {
bitmapInfo = kCGImageAlphaNoneSkipFirst | kCGBitmapByteOrder32Host;
}
return bitmapInfo;
} else if (inputPixelFormat == kCVPixelFormatType_32ARGB) {
uint32_t bitmapInfo = kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Big;
return bitmapInfo;
} else {
NSLog(@"不支持此格式");
return 0;
}
}
BOOL CGImageRefContainsAlpha(CGImageRef imageRef) {
if (!imageRef) {
return NO;
}
CGImageAlphaInfo alphaInfo = CGImageGetAlphaInfo(imageRef);
BOOL hasAlpha = !(alphaInfo == kCGImageAlphaNone ||
alphaInfo == kCGImageAlphaNoneSkipFirst ||
alphaInfo == kCGImageAlphaNoneSkipLast);
return hasAlpha;
}
@interface FGUIComponentImageTexture ()
@property (nonatomic, strong) UIImage *image;
@end
@implementation FGUIComponentImageTexture
- (instancetype)initWithImage:(UIImage *)image {
self = [super init];
if (self) {
self.image = image;
}
return self;
}
- (CVPixelBufferRef)copyPixelBuffer {
return [self pixelBufferRefFromUIImage:self.image];
}
- (void)dispose {}
- (CVPixelBufferRef)pixelBufferRefFromUIImage:(UIImage *)image {
if (!image) {
GDAssert(0);
return nil;
}
CGImageRef imageRef = [image CGImage];
CGFloat frameWidth = CGImageGetWidth(imageRef);
CGFloat frameHeight = CGImageGetHeight(imageRef);
BOOL hasAlpha = CGImageRefContainsAlpha(imageRef);
CFDictionaryRef empty = CFDictionaryCreate(kCFAllocatorDefault, NULL, NULL, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithBool:YES], kCVPixelBufferCGImageCompatibilityKey,
[NSNumber numberWithBool:YES], kCVPixelBufferCGBitmapContextCompatibilityKey,
empty, kCVPixelBufferIOSurfacePropertiesKey,
nil];
CVPixelBufferRef pxbuffer = NULL;
CVReturn status = CVPixelBufferCreate(kCFAllocatorDefault, frameWidth, frameHeight, kCVPixelFormatType_32BGRA, (__bridge CFDictionaryRef) options, &pxbuffer);
NSParameterAssert(status == kCVReturnSuccess && pxbuffer != NULL);
CVPixelBufferLockBaseAddress(pxbuffer, 0);
void *pxdata = CVPixelBufferGetBaseAddress(pxbuffer);
NSParameterAssert(pxdata != NULL);
CGColorSpaceRef rgbColorSpace = CGColorSpaceCreateDeviceRGB();
uint32_t bitmapInfo = bitmapInfoWithPixelFormatType(kCVPixelFormatType_32BGRA, (bool)hasAlpha);
CGContextRef context = CGBitmapContextCreate(pxdata, frameWidth, frameHeight, 8, CVPixelBufferGetBytesPerRow(pxbuffer), rgbColorSpace, bitmapInfo);
NSParameterAssert(context);
CGContextConcatCTM(context, CGAffineTransformIdentity);
CGContextDrawImage(context, CGRectMake(0, 0, frameWidth, frameHeight), imageRef);
CGColorSpaceRelease(rgbColorSpace);
CGContextRelease(context);
CVPixelBufferUnlockBaseAddress(pxbuffer, 0);
return pxbuffer;
}
@end
Android:
@Keep
class FGUIImageTexturePlugin(engine: FlutterEngine) {
private var textureRegistry: TextureRegistry
private var surfaceEntryList: MutableList<TextureRegistry.SurfaceTextureEntry>
init {
var pluginRegistryField = engine.javaClass.getDeclaredField("pluginRegistry")
pluginRegistryField.isAccessible = true
val pluginRegistry = pluginRegistryField.get(engine)
var bindingField = pluginRegistry.javaClass.getDeclaredField("pluginBinding")
bindingField.isAccessible = true
var binding = bindingField.get(pluginRegistry) as FlutterPlugin.FlutterPluginBinding
surfaceEntryList = mutableListOf()
textureRegistry = binding.textureRegistry
}
fun destroy() {
for (surfaceEntry in surfaceEntryList) {
surfaceEntry.release()
}
}
...
}
测试
展示
先看下双端展示效果


过程
这里罗列一些内存测试过程,没兴趣的同学可以直接看结论。
背景
由于图像外接纹理方案无法脱离 Native 环境,直接使用 Web 测试,所以单独做了一个 Example 来验证效果是否符合预期
测试环境:Debug + Flutter_Release(2.10.5)
测试设备:iPhoneX
测试专注:内存占用
步骤
-
新建空白项目,引用关键 pod
-
新增首页页,启动 flutter 引擎,观测内存情况(这里直接加载一个 FGUISwitch)
-
跳转图像测试页,加载 FGUIImage 测试 FlutterView, 分别记录同时传入1、2、3张图片的内存消耗情况
-
跳转新页面,观测内存释放情况
-
返回图像测试页,观测内存加载情况
-
放置多个 FGUIImage,观测内存加载情况
-
加载同一个 Image, 观测内存加载情况
记录
(截图略,主要是懒)
-
初始化:内存占用10.5MB
-
加载 Flutter 引擎:内存占用36.7MB
-
单个 FlutterView 加载一张图片(绘制 300*300 pt):内存占用49.9MB
-
使用 UIImageView 加载同一张图片(绘制 300*300 pt):内存占用39.8MB
-
同时加载 UIImageView 和 FlutterView,同一个图片内存:内存占用 52.9MB
-
加载两个 UIImageView,同一张图片:内存占用 42.3MB
-
加载两个 FlutterView,同一张图片:内存占用 61MB
-
加载一个 FlutterView,2张不同的图片:内存占用 47.5MB
-
加载一个 FlutterView,3张不同的图片:内存占用 47.5MB (相同的原因是因为外部高度设置为 300,第三张图片没有绘制)
-
加载一个 FlutterView,3张不同的图片(300 * 500 pt):内存占用 73.8MB (以上就基本说明 Flutter 外接纹理内存占用跟绘制宽高强有强相关)
-
再打开二级 VC,加载新的 FlutterView,加载1张图片:60.2MB
-
关闭二级 VC:47.1MB (二级页面内存可完全释放)
-
关闭当前 VC:40.5MB (内存只释放了7M,不能完全释放,原因是 IOSurface 未释放,且没有手动释放的方式,只有整个 EngineGroup 进程释放后才会完全释放)
结论
感想
多引擎外接纹理笔者这里还并没用于实际项目,现在只用来做跨端 UI 组件,还没有遇到需要的场景,而且不利于 Web 转化。但方案确实是可行的。
这里顺便说一说,笔者在开发时喜欢用结果反推的方式,先确定要做一个什么样的,再往那个方向补过程,就和上述方案一样,先写出最终的“定义”是什么样,然后想办法补全实现。这也算是一种 “OKR”?[手动狗头]
如果对你开发学习上有丝丝作用,请点个赞[开心] ~
转载自:https://juejin.cn/post/7170887223955423268