mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
596 lines
20 KiB
C++
596 lines
20 KiB
C++
// Copyright 2013 The Flutter Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style license that can be
|
|
// found in the LICENSE file.
|
|
|
|
#include "flutter/flow/raster_cache.h"
|
|
|
|
#include <vector>
|
|
|
|
#include "flutter/common/constants.h"
|
|
#include "flutter/flow/layers/container_layer.h"
|
|
#include "flutter/flow/layers/layer.h"
|
|
#include "flutter/flow/paint_utils.h"
|
|
#include "flutter/fml/logging.h"
|
|
#include "flutter/fml/trace_event.h"
|
|
#include "third_party/skia/include/core/SkCanvas.h"
|
|
#include "third_party/skia/include/core/SkColorSpace.h"
|
|
#include "third_party/skia/include/core/SkImage.h"
|
|
#include "third_party/skia/include/core/SkPicture.h"
|
|
#include "third_party/skia/include/core/SkSurface.h"
|
|
#include "third_party/skia/include/gpu/GrDirectContext.h"
|
|
|
|
namespace flutter {
|
|
|
|
RasterCacheResult::RasterCacheResult(sk_sp<SkImage> image,
|
|
const SkRect& logical_rect,
|
|
const char* type)
|
|
: image_(std::move(image)), logical_rect_(logical_rect), flow_(type) {}
|
|
|
|
void RasterCacheResult::draw(SkCanvas& canvas, const SkPaint* paint) const {
|
|
TRACE_EVENT0("flutter", "RasterCacheResult::draw");
|
|
SkAutoCanvasRestore auto_restore(&canvas, true);
|
|
SkIRect bounds =
|
|
RasterCache::GetDeviceBounds(logical_rect_, canvas.getTotalMatrix());
|
|
FML_DCHECK(
|
|
std::abs(bounds.size().width() - image_->dimensions().width()) <= 1 &&
|
|
std::abs(bounds.size().height() - image_->dimensions().height()) <= 1);
|
|
canvas.resetMatrix();
|
|
flow_.Step();
|
|
canvas.drawImage(image_, bounds.fLeft, bounds.fTop, SkSamplingOptions(),
|
|
paint);
|
|
}
|
|
|
|
RasterCache::RasterCache(size_t access_threshold,
|
|
size_t picture_and_display_list_cache_limit_per_frame)
|
|
: access_threshold_(access_threshold),
|
|
picture_and_display_list_cache_limit_per_frame_(
|
|
picture_and_display_list_cache_limit_per_frame),
|
|
checkerboard_images_(false) {}
|
|
|
|
static bool CanRasterizeRect(const SkRect& cull_rect) {
|
|
if (cull_rect.isEmpty()) {
|
|
// No point in ever rasterizing an empty display list.
|
|
return false;
|
|
}
|
|
|
|
if (!cull_rect.isFinite()) {
|
|
// Cannot attempt to rasterize into an infinitely large surface.
|
|
FML_LOG(INFO) << "Attempted to raster cache non-finite display list";
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static bool IsPictureWorthRasterizing(SkPicture* picture,
|
|
bool will_change,
|
|
bool is_complex) {
|
|
if (will_change) {
|
|
// If the picture is going to change in the future, there is no point in
|
|
// doing to extra work to rasterize.
|
|
return false;
|
|
}
|
|
|
|
if (picture == nullptr || !CanRasterizeRect(picture->cullRect())) {
|
|
// No point in deciding whether the picture is worth rasterizing if it
|
|
// cannot be rasterized at all.
|
|
return false;
|
|
}
|
|
|
|
if (is_complex) {
|
|
// The caller seems to have extra information about the picture and thinks
|
|
// the picture is always worth rasterizing.
|
|
return true;
|
|
}
|
|
|
|
// TODO(abarth): We should find a better heuristic here that lets us avoid
|
|
// wasting memory on trivial layers that are easy to re-rasterize every frame.
|
|
return picture->approximateOpCount(true) > 5;
|
|
}
|
|
|
|
static bool IsDisplayListWorthRasterizing(
|
|
DisplayList* display_list,
|
|
bool will_change,
|
|
bool is_complex,
|
|
DisplayListComplexityCalculator* complexity_calculator) {
|
|
if (will_change) {
|
|
// If the display list is going to change in the future, there is no point
|
|
// in doing to extra work to rasterize.
|
|
return false;
|
|
}
|
|
|
|
if (display_list == nullptr || !CanRasterizeRect(display_list->bounds())) {
|
|
// No point in deciding whether the display list is worth rasterizing if it
|
|
// cannot be rasterized at all.
|
|
return false;
|
|
}
|
|
|
|
if (is_complex) {
|
|
// The caller seems to have extra information about the display list and
|
|
// thinks the display list is always worth rasterizing.
|
|
return true;
|
|
}
|
|
|
|
unsigned int complexity_score = complexity_calculator->Compute(display_list);
|
|
return complexity_calculator->ShouldBeCached(complexity_score);
|
|
}
|
|
|
|
/// @note Procedure doesn't copy all closures.
|
|
static std::unique_ptr<RasterCacheResult> Rasterize(
|
|
GrDirectContext* context,
|
|
const SkMatrix& ctm,
|
|
SkColorSpace* dst_color_space,
|
|
bool checkerboard,
|
|
const SkRect& logical_rect,
|
|
const char* type,
|
|
const std::function<void(SkCanvas*)>& draw_function) {
|
|
TRACE_EVENT0("flutter", "RasterCachePopulate");
|
|
SkIRect cache_rect = RasterCache::GetDeviceBounds(logical_rect, ctm);
|
|
|
|
const SkImageInfo image_info = SkImageInfo::MakeN32Premul(
|
|
cache_rect.width(), cache_rect.height(), sk_ref_sp(dst_color_space));
|
|
|
|
sk_sp<SkSurface> surface =
|
|
context
|
|
? SkSurface::MakeRenderTarget(context, SkBudgeted::kYes, image_info)
|
|
: SkSurface::MakeRaster(image_info);
|
|
|
|
if (!surface) {
|
|
return nullptr;
|
|
}
|
|
|
|
SkCanvas* canvas = surface->getCanvas();
|
|
canvas->clear(SK_ColorTRANSPARENT);
|
|
canvas->translate(-cache_rect.left(), -cache_rect.top());
|
|
canvas->concat(ctm);
|
|
draw_function(canvas);
|
|
|
|
if (checkerboard) {
|
|
DrawCheckerboard(canvas, logical_rect);
|
|
}
|
|
|
|
return std::make_unique<RasterCacheResult>(surface->makeImageSnapshot(),
|
|
logical_rect, type);
|
|
}
|
|
|
|
std::unique_ptr<RasterCacheResult> RasterCache::RasterizePicture(
|
|
SkPicture* picture,
|
|
GrDirectContext* context,
|
|
const SkMatrix& ctm,
|
|
SkColorSpace* dst_color_space,
|
|
bool checkerboard) const {
|
|
return Rasterize(context, ctm, dst_color_space, checkerboard,
|
|
picture->cullRect(), "RasterCacheFlow::SkPicture",
|
|
[=](SkCanvas* canvas) { canvas->drawPicture(picture); });
|
|
}
|
|
|
|
std::unique_ptr<RasterCacheResult> RasterCache::RasterizeDisplayList(
|
|
DisplayList* display_list,
|
|
GrDirectContext* context,
|
|
const SkMatrix& ctm,
|
|
SkColorSpace* dst_color_space,
|
|
bool checkerboard) const {
|
|
return Rasterize(context, ctm, dst_color_space, checkerboard,
|
|
display_list->bounds(), "RasterCacheFlow::DisplayList",
|
|
[=](SkCanvas* canvas) { display_list->RenderTo(canvas); });
|
|
}
|
|
|
|
void RasterCache::Prepare(PrerollContext* context,
|
|
Layer* layer,
|
|
const SkMatrix& ctm,
|
|
RasterCacheLayerStrategy strategy) {
|
|
auto cache_key_optional =
|
|
TryToMakeRasterCacheKeyForLayer(layer, strategy, ctm);
|
|
if (!cache_key_optional) {
|
|
return;
|
|
}
|
|
Entry& entry = cache_[cache_key_optional.value()];
|
|
entry.access_count++;
|
|
entry.used_this_frame = true;
|
|
if (!entry.image) {
|
|
entry.image =
|
|
RasterizeLayer(context, layer, strategy, ctm, checkerboard_images_);
|
|
}
|
|
}
|
|
|
|
std::optional<RasterCacheKey> RasterCache::TryToMakeRasterCacheKeyForLayer(
|
|
const Layer* layer,
|
|
RasterCacheLayerStrategy strategy,
|
|
const SkMatrix& ctm) const {
|
|
switch (strategy) {
|
|
case RasterCacheLayerStrategy::kLayer:
|
|
return RasterCacheKey(layer->unique_id(), RasterCacheKeyType::kLayer,
|
|
ctm);
|
|
case RasterCacheLayerStrategy::kLayerChildren:
|
|
FML_DCHECK(layer->as_container_layer());
|
|
auto& children_layers = layer->as_container_layer()->layers();
|
|
auto children_count = children_layers.size();
|
|
if (children_count == 0) {
|
|
return std::nullopt;
|
|
}
|
|
std::vector<uint64_t> ids;
|
|
std::transform(children_layers.begin(), children_layers.end(),
|
|
std::back_inserter(ids), [](auto& layer) -> uint64_t {
|
|
return layer->unique_id();
|
|
});
|
|
return RasterCacheKey(RasterCacheKeyID(std::move(ids)),
|
|
RasterCacheKeyType::kLayerChildren, ctm);
|
|
}
|
|
}
|
|
|
|
std::unique_ptr<RasterCacheResult> RasterCache::RasterizeLayer(
|
|
PrerollContext* context,
|
|
Layer* layer,
|
|
RasterCacheLayerStrategy strategy,
|
|
const SkMatrix& ctm,
|
|
bool checkerboard) const {
|
|
const SkRect& paint_bounds = GetPaintBoundsFromLayer(layer, strategy);
|
|
|
|
return Rasterize(
|
|
context->gr_context, ctm, context->dst_color_space, checkerboard,
|
|
paint_bounds, "RasterCacheFlow::Layer",
|
|
[layer, context, strategy](SkCanvas* canvas) {
|
|
SkISize canvas_size = canvas->getBaseLayerSize();
|
|
SkNWayCanvas internal_nodes_canvas(canvas_size.width(),
|
|
canvas_size.height());
|
|
internal_nodes_canvas.setMatrix(canvas->getTotalMatrix());
|
|
internal_nodes_canvas.addCanvas(canvas);
|
|
Layer::PaintContext paintContext = {
|
|
/* internal_nodes_canvas= */ static_cast<SkCanvas*>(
|
|
&internal_nodes_canvas),
|
|
/* leaf_nodes_canvas= */ canvas,
|
|
/* gr_context= */ context->gr_context,
|
|
/* view_embedder= */ nullptr,
|
|
context->raster_time,
|
|
context->ui_time,
|
|
context->texture_registry,
|
|
context->has_platform_view ? nullptr : context->raster_cache,
|
|
context->checkerboard_offscreen_layers,
|
|
context->frame_device_pixel_ratio};
|
|
switch (strategy) {
|
|
case RasterCacheLayerStrategy::kLayer:
|
|
if (layer->needs_painting(paintContext)) {
|
|
layer->Paint(paintContext);
|
|
}
|
|
break;
|
|
case RasterCacheLayerStrategy::kLayerChildren:
|
|
FML_DCHECK(layer->as_container_layer());
|
|
layer->as_container_layer()->PaintChildren(paintContext);
|
|
break;
|
|
}
|
|
});
|
|
}
|
|
|
|
const SkRect& RasterCache::GetPaintBoundsFromLayer(
|
|
Layer* layer,
|
|
RasterCacheLayerStrategy strategy) const {
|
|
switch (strategy) {
|
|
case RasterCacheLayerStrategy::kLayer:
|
|
return layer->paint_bounds();
|
|
case RasterCacheLayerStrategy::kLayerChildren:
|
|
FML_DCHECK(layer->as_container_layer());
|
|
return layer->as_container_layer()->child_paint_bounds();
|
|
}
|
|
}
|
|
|
|
bool RasterCache::Prepare(PrerollContext* context,
|
|
SkPicture* picture,
|
|
bool is_complex,
|
|
bool will_change,
|
|
const SkMatrix& untranslated_matrix,
|
|
const SkPoint& offset) {
|
|
if (!GenerateNewCacheInThisFrame()) {
|
|
return false;
|
|
}
|
|
|
|
if (!IsPictureWorthRasterizing(picture, will_change, is_complex)) {
|
|
// We only deal with pictures that are worthy of rasterization.
|
|
return false;
|
|
}
|
|
|
|
SkMatrix transformation_matrix = untranslated_matrix;
|
|
transformation_matrix.preTranslate(offset.x(), offset.y());
|
|
|
|
if (!transformation_matrix.invert(nullptr)) {
|
|
// The matrix was singular. No point in going further.
|
|
return false;
|
|
}
|
|
|
|
RasterCacheKey cache_key(picture->uniqueID(), RasterCacheKeyType::kPicture,
|
|
transformation_matrix);
|
|
|
|
// Creates an entry, if not present prior.
|
|
Entry& entry = cache_[cache_key];
|
|
if (entry.access_count < access_threshold_) {
|
|
// Frame threshold has not yet been reached.
|
|
return false;
|
|
}
|
|
|
|
if (!entry.image) {
|
|
// GetIntegralTransCTM effect for matrix which only contains scale,
|
|
// translate, so it won't affect result of matrix decomposition and cache
|
|
// key.
|
|
#ifndef SUPPORT_FRACTIONAL_TRANSLATION
|
|
transformation_matrix = GetIntegralTransCTM(transformation_matrix);
|
|
#endif
|
|
entry.image =
|
|
RasterizePicture(picture, context->gr_context, transformation_matrix,
|
|
context->dst_color_space, checkerboard_images_);
|
|
picture_cached_this_frame_++;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool RasterCache::Prepare(PrerollContext* context,
|
|
DisplayList* display_list,
|
|
bool is_complex,
|
|
bool will_change,
|
|
const SkMatrix& untranslated_matrix,
|
|
const SkPoint& offset) {
|
|
if (!GenerateNewCacheInThisFrame()) {
|
|
return false;
|
|
}
|
|
|
|
DisplayListComplexityCalculator* complexity_calculator =
|
|
context->gr_context ? DisplayListComplexityCalculator::GetForBackend(
|
|
context->gr_context->backend())
|
|
: DisplayListComplexityCalculator::GetForSoftware();
|
|
|
|
if (!IsDisplayListWorthRasterizing(display_list, will_change, is_complex,
|
|
complexity_calculator)) {
|
|
// We only deal with display lists that are worthy of rasterization.
|
|
return false;
|
|
}
|
|
|
|
SkMatrix transformation_matrix = untranslated_matrix;
|
|
transformation_matrix.preTranslate(offset.x(), offset.y());
|
|
|
|
if (!transformation_matrix.invert(nullptr)) {
|
|
// The matrix was singular. No point in going further.
|
|
return false;
|
|
}
|
|
|
|
RasterCacheKey cache_key(display_list->unique_id(),
|
|
RasterCacheKeyType::kDisplayList,
|
|
transformation_matrix);
|
|
|
|
// Creates an entry, if not present prior.
|
|
Entry& entry = cache_[cache_key];
|
|
if (entry.access_count < access_threshold_) {
|
|
// Frame threshold has not yet been reached.
|
|
return false;
|
|
}
|
|
|
|
if (!entry.image) {
|
|
// GetIntegralTransCTM effect for matrix which only contains scale,
|
|
// translate, so it won't affect result of matrix decomposition and cache
|
|
// key.
|
|
#ifndef SUPPORT_FRACTIONAL_TRANSLATION
|
|
transformation_matrix = GetIntegralTransCTM(transformation_matrix);
|
|
#endif
|
|
entry.image = RasterizeDisplayList(
|
|
display_list, context->gr_context, transformation_matrix,
|
|
context->dst_color_space, checkerboard_images_);
|
|
display_list_cached_this_frame_++;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void RasterCache::Touch(Layer* layer,
|
|
const SkMatrix& ctm,
|
|
RasterCacheLayerStrategy strategey) {
|
|
auto cache_key_optional =
|
|
TryToMakeRasterCacheKeyForLayer(layer, strategey, ctm);
|
|
if (!cache_key_optional) {
|
|
return;
|
|
}
|
|
Touch(cache_key_optional.value());
|
|
}
|
|
|
|
void RasterCache::Touch(SkPicture* picture,
|
|
const SkMatrix& transformation_matrix) {
|
|
RasterCacheKey cache_key(picture->uniqueID(), RasterCacheKeyType::kPicture,
|
|
transformation_matrix);
|
|
Touch(cache_key);
|
|
}
|
|
|
|
void RasterCache::Touch(DisplayList* display_list,
|
|
const SkMatrix& transformation_matrix) {
|
|
RasterCacheKey cache_key(display_list->unique_id(),
|
|
RasterCacheKeyType::kDisplayList,
|
|
transformation_matrix);
|
|
Touch(cache_key);
|
|
}
|
|
|
|
void RasterCache::Touch(const RasterCacheKey& cache_key) {
|
|
auto it = cache_.find(cache_key);
|
|
if (it != cache_.end()) {
|
|
it->second.used_this_frame = true;
|
|
it->second.access_count++;
|
|
}
|
|
}
|
|
|
|
bool RasterCache::Draw(const SkPicture& picture,
|
|
SkCanvas& canvas,
|
|
const SkPaint* paint) const {
|
|
RasterCacheKey cache_key(picture.uniqueID(), RasterCacheKeyType::kPicture,
|
|
canvas.getTotalMatrix());
|
|
return Draw(cache_key, canvas, paint);
|
|
}
|
|
|
|
bool RasterCache::Draw(const DisplayList& display_list,
|
|
SkCanvas& canvas,
|
|
const SkPaint* paint) const {
|
|
RasterCacheKey cache_key(display_list.unique_id(),
|
|
RasterCacheKeyType::kDisplayList,
|
|
canvas.getTotalMatrix());
|
|
return Draw(cache_key, canvas, paint);
|
|
}
|
|
|
|
bool RasterCache::Draw(const Layer* layer,
|
|
SkCanvas& canvas,
|
|
RasterCacheLayerStrategy strategy,
|
|
const SkPaint* paint) const {
|
|
auto cache_key_optional =
|
|
TryToMakeRasterCacheKeyForLayer(layer, strategy, canvas.getTotalMatrix());
|
|
if (!cache_key_optional) {
|
|
return false;
|
|
}
|
|
return Draw(cache_key_optional.value(), canvas, paint);
|
|
}
|
|
|
|
bool RasterCache::Draw(const RasterCacheKey& cache_key,
|
|
SkCanvas& canvas,
|
|
const SkPaint* paint) const {
|
|
auto it = cache_.find(cache_key);
|
|
if (it == cache_.end()) {
|
|
return false;
|
|
}
|
|
|
|
Entry& entry = it->second;
|
|
entry.access_count++;
|
|
entry.used_this_frame = true;
|
|
|
|
if (entry.image) {
|
|
entry.image->draw(canvas, paint);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void RasterCache::PrepareNewFrame() {
|
|
picture_cached_this_frame_ = 0;
|
|
display_list_cached_this_frame_ = 0;
|
|
}
|
|
|
|
void RasterCache::SweepOneCacheAfterFrame(RasterCacheKey::Map<Entry>& cache,
|
|
RasterCacheMetrics& picture_metrics,
|
|
RasterCacheMetrics& layer_metrics) {
|
|
std::vector<RasterCacheKey::Map<Entry>::iterator> dead;
|
|
|
|
for (auto it = cache.begin(); it != cache.end(); ++it) {
|
|
Entry& entry = it->second;
|
|
|
|
if (!entry.used_this_frame) {
|
|
dead.push_back(it);
|
|
} else if (entry.image) {
|
|
RasterCacheKeyKind kind = it->first.kind();
|
|
switch (kind) {
|
|
case RasterCacheKeyKind::kPictureMetrics:
|
|
picture_metrics.in_use_count++;
|
|
picture_metrics.in_use_bytes += entry.image->image_bytes();
|
|
break;
|
|
case RasterCacheKeyKind::kLayerMetrics:
|
|
layer_metrics.in_use_count++;
|
|
layer_metrics.in_use_bytes += entry.image->image_bytes();
|
|
break;
|
|
}
|
|
}
|
|
entry.used_this_frame = false;
|
|
}
|
|
|
|
for (auto it : dead) {
|
|
if (it->second.image) {
|
|
RasterCacheKeyKind kind = it->first.kind();
|
|
switch (kind) {
|
|
case RasterCacheKeyKind::kPictureMetrics:
|
|
picture_metrics.eviction_count++;
|
|
picture_metrics.eviction_bytes += it->second.image->image_bytes();
|
|
break;
|
|
case RasterCacheKeyKind::kLayerMetrics:
|
|
layer_metrics.eviction_count++;
|
|
layer_metrics.eviction_bytes += it->second.image->image_bytes();
|
|
break;
|
|
}
|
|
}
|
|
cache.erase(it);
|
|
}
|
|
}
|
|
|
|
void RasterCache::CleanupAfterFrame() {
|
|
picture_metrics_ = {};
|
|
layer_metrics_ = {};
|
|
SweepOneCacheAfterFrame(cache_, picture_metrics_, layer_metrics_);
|
|
TraceStatsToTimeline();
|
|
}
|
|
|
|
void RasterCache::Clear() {
|
|
cache_.clear();
|
|
picture_metrics_ = {};
|
|
layer_metrics_ = {};
|
|
}
|
|
|
|
size_t RasterCache::GetCachedEntriesCount() const {
|
|
return cache_.size();
|
|
}
|
|
|
|
size_t RasterCache::GetLayerCachedEntriesCount() const {
|
|
size_t layer_cached_entries_count = 0;
|
|
for (const auto& item : cache_) {
|
|
if (item.first.kind() == RasterCacheKeyKind::kLayerMetrics) {
|
|
layer_cached_entries_count++;
|
|
}
|
|
}
|
|
return layer_cached_entries_count;
|
|
}
|
|
|
|
size_t RasterCache::GetPictureCachedEntriesCount() const {
|
|
size_t picture_cached_entries_count = 0;
|
|
for (const auto& item : cache_) {
|
|
if (item.first.kind() == RasterCacheKeyKind::kPictureMetrics) {
|
|
picture_cached_entries_count++;
|
|
}
|
|
}
|
|
return picture_cached_entries_count;
|
|
}
|
|
|
|
void RasterCache::SetCheckboardCacheImages(bool checkerboard) {
|
|
if (checkerboard_images_ == checkerboard) {
|
|
return;
|
|
}
|
|
|
|
checkerboard_images_ = checkerboard;
|
|
|
|
// Clear all existing entries so previously rasterized items (with or without
|
|
// a checkerboard) will be refreshed in subsequent passes.
|
|
Clear();
|
|
}
|
|
|
|
void RasterCache::TraceStatsToTimeline() const {
|
|
#if !FLUTTER_RELEASE
|
|
FML_TRACE_COUNTER(
|
|
"flutter", //
|
|
"RasterCache", reinterpret_cast<int64_t>(this), //
|
|
"LayerCount", layer_metrics_.total_count(), //
|
|
"LayerMBytes", layer_metrics_.total_bytes() / kMegaByteSizeInBytes, //
|
|
"PictureCount", picture_metrics_.total_count(), //
|
|
"PictureMBytes", picture_metrics_.total_bytes() / kMegaByteSizeInBytes);
|
|
|
|
#endif // !FLUTTER_RELEASE
|
|
}
|
|
|
|
size_t RasterCache::EstimateLayerCacheByteSize() const {
|
|
size_t layer_cache_bytes = 0;
|
|
for (const auto& item : cache_) {
|
|
if (item.first.kind() == RasterCacheKeyKind::kLayerMetrics &&
|
|
item.second.image) {
|
|
layer_cache_bytes += item.second.image->image_bytes();
|
|
}
|
|
}
|
|
return layer_cache_bytes;
|
|
}
|
|
|
|
size_t RasterCache::EstimatePictureCacheByteSize() const {
|
|
size_t picture_cache_bytes = 0;
|
|
for (const auto& item : cache_) {
|
|
if (item.first.kind() == RasterCacheKeyKind::kPictureMetrics &&
|
|
item.second.image) {
|
|
picture_cache_bytes += item.second.image->image_bytes();
|
|
}
|
|
}
|
|
return picture_cache_bytes;
|
|
}
|
|
|
|
} // namespace flutter
|