Flutter是谷歌的高性能、跨端UI框架,可以通过一套代码,支持iOS、Android、Windows/MAC/Linux等多个平台,且能达到原生性能。Flutter也可以与平台原生代码进行混合开发。在全世界,Flutter正在被越来越多的开发者和组织使用,并且Flutter是完全免费、开源的。
在我们使用flutter框架开发windows桌面程序时,有这样一个简单需求:设置4k的背景图片。
我们通过查询flutter相关文档,可以知道使用以下方式设置背景图片:
decoration: BoxDecoration(
color: Colors.blue,
image: DecorationImage(
fit: BoxFit.cover,
image: AssetImage('assets/images/wallpaper.png'),
)
),
问题
当我们使用这个方式去添加3840×2160的4k图片作为背景时,虽然成功达到我们的需求,但是却发现,相比未加载背景图片之前,程序的内存居然多了接近70M。
读入这张4k图片,正常会占用的内存是3840x2160x3
约为24M和70M差距甚大,我们需要分析一下原因。
分析
分析flutter加载Image资源的原理,Image组件通过实现ImageProvider抽象类的loadBuffer方法(新的flutter版本load被loadBuffer替换)来加载图片资源数据,而AssetImage的loadBuffer方法实现如下:
@override
ImageStreamCompter loadBuffer(AssetBundleImageKey key, DecoderBufferCallback decode) {
InformationCollector? collector;
assert(() {
collector = () => <DiagnosticsNode>[
DiagnosticsProperty<ImageProvider>('Image provider', this),
DiagnosticsProperty<AssetBundleImageKey>('Image key', key),
];
return true;
}());
return MultiFrameImageStreamCompleter(
codec: _loadAsync(key, decode, null),
scale: key.scale,
debugLabel: key.name,
informationCollector: collector,
);
}
loadBuffer会返回一个ImageStreamCompleter对象,这里返回的是MultiFrameImageStreamCompleter多帧图片管理器,是ImageStreamCompleter的一个子类。
MultiFrameImageStreamCompleter 需要一个Future<ui.Codec>类型的参数codec。Codec 是处理图片编解码的类的一个handler,是一个flutter engine API 的包装类。
MultiFrameImageStreamCompleter({
required Future<ui.Codec> codec,
required double scale,
String? debugLabel,
Stream<ImageChunkEvent>? chunkEvents,
InformationCollector? informationCollector,
}) : assert(codec != null),
_informationCollector = informationCollector,
_scale = scale {
this.debugLabel = debugLabel;
codec.then<void>(_handleCodecReady, onError: (Object error, StackTrace stack) {
reportError(
context: ErrorDescription('resolving an image codec'),
exception: error,
stack: stack,
informationCollector: informationCollector,
silent: true,
);
});
if (chunkEvents != null) {
_chunkSubscription = chunkEvents.listen(reportImageChunkEvent,
onError: (Object error, StackTrace stack) {
reportError(
context: ErrorDescription('loading an image'),
exception: error,
stack: stack,
informationCollector: informationCollector,
silent: true,
);
},
);
}
}
codec最终的结果是一个或多个(动图)帧,而这些帧最终会绘制到屏幕上。codec的异步方法执行完成后会调用_handleCodecReady函数,调用_decodeNextFrameAndSchedule函数解码下一帧。
定位
当我们调试程序时,会发现到目前为止,程序的内存只是涨幅少许,但是当调试到图中获取下一帧数据时,内存突然涨幅非常大,因此定位到是这个getNextFrame导致了内存大量涨幅。
Future<void> _decodeNextFrameAndSchedule() async {
_nextFrame?.image.dispose();
_nextFrame = null;
try {
_nextFrame = await _codec!.getNextFrame();
} catch (exception, stack) {
reportError(
context: ErrorDescription('resolving an image frame'),
exception: exception,
stack: stack,
informationCollector: _informationCollector,
silent: true,
);
return;
}
if (_codec!.frameCount == 1) {
if (!hasListeners) {
return;
}
_emitFrame(ImageInfo(
image: _nextFrame!.image.clone(),
scale: _scale,
debugLabel: debugLabel,
));
_nextFrame!.image.dispose();
_nextFrame = null;
return;
}
_scheduleAppFrame();
}
继续跟踪,发现最终是调用“Codec::getNextFrame”映射方法,这个方法是在flutter engine中的c++实现的,接下来跟踪flutter engine代码。
我们可以看到getNextFrame方法是属于MultiFrameCodec的。其实方法里面看起来是很简单的,里面做了几个事情:
- 先获取UI Task线程。
- 获取当前Skia处理的Queue
- 获取上下文
- 切换到IO线程中运行GetNextFrameAndInvokeCallback
而在GetNextFrameAndInvokeCallback里做了两件事:
- GetNextFrameImage:做的事情大致就是获取下一帧的SkImage数据,并且保存了上一帧的关键数据。这里我们可以看到有多次Copy的操作,所以这些操作都是在IO线程中的。
- InvokeCallback:这里做的事情大概就是获取刚刚从GetNextFrameImage中拿到的SkImage,并且将它塞到了FrameInfo的结构体里面,并且改变下一帧的index。然后在UI线程中Callback回去。
sk_sp<DlImage> MultiFrameCodec::State::GetNextFrameImage(
fml::WeakPtr<GrDirectContext> resourceContext,
const std::shared_ptr<const fml::SyncSwitch>& gpu_disable_sync_switch,
std::shared_ptr<impeller::Context> impeller_context_,
fml::RefPtr<flutter::SkiaUnrefQueue> unref_queue) {
SkBitmap bitmap = SkBitmap();
SkImageInfo info = generator_->GetInfo().makeColorType(kN32_SkColorType);
if (info.alphaType() == kUnpremul_SkAlphaType) {
SkImageInfo updated = info.makeAlphaType(kPremul_SkAlphaType);
info = updated;
}
if (!bitmap.tryAllocPixels(info)) {
FML_LOG(ERROR) << "Failed to allocate memory for bitmap of size "
<< info.computeMinByteSize() << "B";
return nullptr;
}
ImageGenerator::FrameInfo frameInfo =
generator_->GetFrameInfo(nextFrameIndex_);
const int requiredFrameIndex =
frameInfo.required_frame.value_or(SkCodec::kNoFrame);
std::optional<unsigned int> prior_frame_index = std::nullopt;
if (requiredFrameIndex != SkCodec::kNoFrame) {
if (lastRequiredFrame_ == nullptr) {
FML_LOG(ERROR) << "Frame " << nextFrameIndex_ << " depends on frame "
<< requiredFrameIndex
<< " and no required frames are cached.";
return nullptr;
} else if (lastRequiredFrameIndex_ != requiredFrameIndex) {
FML_DLOG(INFO) << "Required frame " << requiredFrameIndex
<< " is not cached. Using " << lastRequiredFrameIndex_
<< " instead";
}
if (lastRequiredFrame_->getPixels() &&
CopyToBitmap(&bitmap, lastRequiredFrame_->colorType(),
*lastRequiredFrame_)) {
prior_frame_index = requiredFrameIndex;
}
}
if (!generator_->GetPixels(info, bitmap.getPixels(), bitmap.rowBytes(),
nextFrameIndex_, requiredFrameIndex)) {
FML_LOG(ERROR) << "Could not getPixels for frame " << nextFrameIndex_;
return nullptr;
}
// Hold onto this if we need it to decode future frames.
if (frameInfo.disposal_method == SkCodecAnimation::DisposalMethod::kKeep) {
lastRequiredFrame_ = std::make_unique<SkBitmap>(bitmap);
lastRequiredFrameIndex_ = nextFrameIndex_;
}
sk_sp<SkImage> skImage;
gpu_disable_sync_switch->Execute(
fml::SyncSwitch::Handlers()
.SetIfTrue([&skImage, &bitmap] {
skImage = SkImage::MakeFromBitmap(bitmap);
})
.SetIfFalse([&skImage, &resourceContext, &bitmap] {
if (resourceContext) {
SkPixmap pixmap(bitmap.info(), bitmap.pixelRef()->pixels(),
bitmap.pixelRef()->rowBytes());
skImage = SkImage::MakeCrossContextFromPixmap(
resourceContext.get(), pixmap, true);
} else {
skImage = SkImage::MakeFromBitmap(bitmap);
}
}));
return DlImageGPU::Make({skImage, std::move(unref_queue)});
}
结论
通过分析GetNextFrameImage方法,我们发现该方法额外保存了上一帧的数据,导致了内存多了一张图片的大小。
又发现缓存的SkBitmap是32位深的,而不是24位深,因此加载一张4k图片的内存为:3840x2160x4
约为32M,getNextFrame额外保留了一帧图片,因此需要占用64M内存。
因此我们可以知道,内存涨这么多的原因是,flutter在加载图片数据时,保存的是32位深数据,并且会额外缓存一帧图片数据。
优化方案
如果要正面解决这个问题,就需要更加熟悉和理解flutter的源码,然后再修改flutter源码,这个工作量和风险都是巨大的。
但是,如果仅仅只是为了解决加载4k背景图片,导致内存涨幅大
的问题,其实可以选择另一种方式去优化。
我们知道,flutter的程序只创建出了一个窗口,所有的ui操作都在这个窗口中进行。如果说flutter自己绘制背景图片的方式会大量增加内存消耗,那我们可以尝试自己创建背景窗口去绘制背景图片,这是否就能解决这个问题。
外部背景窗口
1. 创建背景窗口
背景窗口需要能够绘制背景图片,可以简单使用gdi+实现,例如:
auto image = Image(_bgImagePath.c_str());
if (image.GetLastStatus() == Status::Ok)
{
RECT rc;
GetClientRect(_hWnd, &rc);
Graphics graphics(hdc);
graphics.DrawImage(&image, 0, 0, rc.right - rc.left, rc.bottom - rc.top);
}
窗口绘制背景图片的方式有很多种,这里只是给个示例。
2.设置父窗口
为了保证背景窗口和flutter主窗口之间的层级关系,必须要将背景窗口设置为flutter主窗口的父窗口。
BackgroundWindow bgWnd;
bgWnd.Create(instance);
HWND bghwnd = bgWnd.GetHandle();
FlutterWindow window(project);
Win32Window::Point origin(10, 10);
Win32Window::Size size(1280, 720);
if (!window.Create(L"myapp", origin, size, bghwnd)) {
return EXIT_FAILURE;
}
3.主窗口设置背景透明
要显示背景窗口,就需要设置主窗口的背景透明。考虑到对主窗口设置属性,以及监听事件等功能,我们可以使用第三方插件window_manager来实现设置背景透明的功能。
import 'package:window_manager/window_manager.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await windowManager.ensureInitialized();
WindowOptions windowOptions = WindowOptions(
size: Size(800, 600),
);
windowManager.waitUntilReadyToShow(windowOptions, () async {
// 设置背景透明
await windowManager.setBackgroundColor(Colors.transparent);
});
runApp(const MyApp());
}
@override
Widget build(BuildContext context) {
return Scaffold(
// 设置背景透明
backgroundColor: Colors.transparent,
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
//...
)
);
}
4.主窗口和背景窗口通信
主窗口和背景窗口之间要进行通信,最简单的一种方式就是窗口消息通信。创建插件,查找背景窗口,使用SendMessage发送WM_COPYDATA指令进行通信。
HWND hwnd = FindWindow(kWindowClassName, kWindowTitle);
if (!hwnd)
return false;
COPYDATASTRUCT cpd;
SendMessage(hwnd, WM_COPYDATA, 0, (LPARAM)&cpd);
总结
通过使用外部背景窗口的方式,也能实现flutter内部绘制背景图片的效果,而且内存消耗远小于使用flutter绘制的方式。可以使用该方案,优化flutter绘制图片消耗内存大的问题。但是该方法也具有较多的局限性,例如:
- 只能替换绘制背景图片,如果flutter需要其他组件绘制图片,则不能使用该方案
- 代码维护复杂,flutter是一个整体框架,而该方案破坏了flutter的整体性,对于后期的维护也存在一定的成本
- 可能存在更多的异常,存在更多的窗口,就增加了异常的可能性
参考资料:
- https://www.jianshu.com/p/3a4d8ebf9932
- https://juejin.cn/post/6844904016225239047
作者:星黎 | 来源:公众号——好奇de悟空
版权声明:本文内容转自互联网,本文观点仅代表作者本人。本站仅提供信息存储空间服务,所有权归原作者所有。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至1393616908@qq.com 举报,一经查实,本站将立刻删除。