开启生长之旅!这是我参与「日新计划 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?;
}
...
}
那剩余的工作就是在传输进程中,将 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”?[手动狗头]
如果对你开发学习上有丝丝效果,请点个赞[高兴] ~