开启生长之旅!这是我参与「日新计划 12 月更文应战」的第4天,点击检查活动概况

Flutter 多引擎系列: 《Flutter 多引擎烘托,在稿定 App 的实践》 等等,专栏里可检查

挺多读者谈论上都对 multiple-fluttersFlutter 多引擎烘托有兴趣,期望能有更多的资料可供参考。

笔者后续大约有两个方向的文章,一是持续介绍咱们在 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?;
  }
  ...
}

那剩余的工作就是在传输进程中,将 UIImageBitmap 转化成如上目标即可。

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()
        }
    }
    ...
}

测验

展现

先看下双端展现效果

Flutter 多引擎渲染,外接纹理实践
Flutter 多引擎渲染,外接纹理实践

进程

这儿罗列一些内存测验进程,没兴趣的同学能够直接看定论。

布景

因为图画外接纹路计划无法脱离 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 进程开释后才会彻底开释)

定论

Flutter 多引擎渲染,外接纹理实践

感想

多引擎外接纹路笔者这儿还并没用于实践项目,现在只用来做跨端 UI 组件,还没有遇到需求的场景,而且不利于 Web 转化。但计划确实是可行的。

这儿顺便说一说,笔者在开发时喜欢用成果反推的办法,先确认要做一个什么样的,再往那个方向补进程,就和上述计划相同,先写出最终的“界说”是什么样,然后想办法补全完成。这也算是一种 “OKR”?[手动狗头]

如果对你开发学习上有丝丝效果,请点个赞[高兴] ~