Revert "Revert "[ios_platform_view] MaskView pool to reuse maskViews. (#38989)" (#39490)" (#39498)
This reverts commit 6a2de773e2932e4514f4e2f5df0eeb8d67b9842a.
diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm
index 69a96fe..8df862e 100644
--- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm
+++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm
@@ -18,6 +18,8 @@
#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterViewController_Internal.h"
#import "flutter/shell/platform/darwin/ios/ios_surface.h"
+static const NSUInteger kFlutterClippingMaskViewPoolCapacity = 5;
+
@implementation UIView (FirstResponder)
- (BOOL)flt_hasFirstResponderInViewHierarchySubtree {
if (self.isFirstResponder) {
@@ -451,6 +453,17 @@
return clipCount;
}
+void FlutterPlatformViewsController::ClipViewSetMaskView(UIView* clipView) {
+ if (clipView.maskView) {
+ return;
+ }
+ UIView* flutterView = flutter_view_.get();
+ CGRect frame =
+ CGRectMake(-clipView.frame.origin.x, -clipView.frame.origin.y,
+ CGRectGetWidth(flutterView.bounds), CGRectGetHeight(flutterView.bounds));
+ clipView.maskView = [mask_view_pool_.get() getMaskViewWithFrame:frame];
+}
+
void FlutterPlatformViewsController::ApplyMutators(const MutatorsStack& mutators_stack,
UIView* embedded_view,
const SkRect& bounding_rect) {
@@ -461,18 +474,15 @@
ResetAnchor(embedded_view.layer);
ChildClippingView* clipView = (ChildClippingView*)embedded_view.superview;
- CGFloat screenScale = [UIScreen mainScreen].scale;
-
- UIView* flutter_view = flutter_view_.get();
- FlutterClippingMaskView* maskView = [[[FlutterClippingMaskView alloc]
- initWithFrame:CGRectMake(-clipView.frame.origin.x, -clipView.frame.origin.y,
- CGRectGetWidth(flutter_view.bounds),
- CGRectGetHeight(flutter_view.bounds))
- screenScale:screenScale] autorelease];
-
SkMatrix transformMatrix;
NSMutableArray* blurFilters = [[[NSMutableArray alloc] init] autorelease];
-
+ FML_DCHECK(!clipView.maskView ||
+ [clipView.maskView isKindOfClass:[FlutterClippingMaskView class]]);
+ if (mask_view_pool_.get() == nil) {
+ mask_view_pool_.reset([[FlutterClippingMaskViewPool alloc]
+ initWithCapacity:kFlutterClippingMaskViewPoolCapacity]);
+ }
+ [mask_view_pool_.get() recycleMaskViews];
clipView.maskView = nil;
auto iter = mutators_stack.Begin();
while (iter != mutators_stack.End()) {
@@ -486,8 +496,9 @@
transformMatrix)) {
break;
}
- [maskView clipRect:(*iter)->GetRect() matrix:transformMatrix];
- clipView.maskView = maskView;
+ ClipViewSetMaskView(clipView);
+ [(FlutterClippingMaskView*)clipView.maskView clipRect:(*iter)->GetRect()
+ matrix:transformMatrix];
break;
}
case kClipRRect: {
@@ -495,16 +506,18 @@
transformMatrix)) {
break;
}
- [maskView clipRRect:(*iter)->GetRRect() matrix:transformMatrix];
- clipView.maskView = maskView;
+ ClipViewSetMaskView(clipView);
+ [(FlutterClippingMaskView*)clipView.maskView clipRRect:(*iter)->GetRRect()
+ matrix:transformMatrix];
break;
}
case kClipPath: {
// TODO(cyanglaz): Find a way to pre-determine if path contains the PlatformView boudning
// rect. See `ClipRRectContainsPlatformViewBoundingRect`.
// https://github.com/flutter/flutter/issues/118650
- [maskView clipPath:(*iter)->GetPath() matrix:transformMatrix];
- clipView.maskView = maskView;
+ ClipViewSetMaskView(clipView);
+ [(FlutterClippingMaskView*)clipView.maskView clipPath:(*iter)->GetPath()
+ matrix:transformMatrix];
break;
}
case kOpacity:
@@ -551,6 +564,7 @@
[clipView applyBlurBackdropFilters:blurFilters];
}
+ CGFloat screenScale = [UIScreen mainScreen].scale;
// The UIKit frame is set based on the logical resolution (points) instead of physical.
// (https://developer.apple.com/library/archive/documentation/DeviceInformation/Reference/iOSDeviceCompatibility/Displays/Displays.html).
// However, flow is based on the physical resolution. For example, 1000 pixels in flow equals
diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm
index 9b8ee71..945e5b4 100644
--- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm
+++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm
@@ -2445,6 +2445,125 @@
XCTAssertFalse(view.flt_hasFirstResponderInViewHierarchySubtree);
}
+- (void)testFlutterClippingMaskViewPoolReuseViewsAfterRecycle {
+ FlutterClippingMaskViewPool* pool =
+ [[[FlutterClippingMaskViewPool alloc] initWithCapacity:2] autorelease];
+ FlutterClippingMaskView* view1 = [pool getMaskViewWithFrame:CGRectZero];
+ FlutterClippingMaskView* view2 = [pool getMaskViewWithFrame:CGRectZero];
+ [pool recycleMaskViews];
+ CGRect newRect = CGRectMake(0, 0, 10, 10);
+ FlutterClippingMaskView* view3 = [pool getMaskViewWithFrame:newRect];
+ FlutterClippingMaskView* view4 = [pool getMaskViewWithFrame:newRect];
+ XCTAssertEqual(view1, view3);
+ XCTAssertEqual(view2, view4);
+ XCTAssertTrue(CGRectEqualToRect(view3.frame, newRect));
+ XCTAssertTrue(CGRectEqualToRect(view4.frame, newRect));
+}
+
+- (void)testFlutterClippingMaskViewPoolAllocsNewMaskViewsAfterReachingCapacity {
+ FlutterClippingMaskViewPool* pool =
+ [[[FlutterClippingMaskViewPool alloc] initWithCapacity:2] autorelease];
+ FlutterClippingMaskView* view1 = [pool getMaskViewWithFrame:CGRectZero];
+ FlutterClippingMaskView* view2 = [pool getMaskViewWithFrame:CGRectZero];
+ FlutterClippingMaskView* view3 = [pool getMaskViewWithFrame:CGRectZero];
+ XCTAssertNotEqual(view1, view3);
+ XCTAssertNotEqual(view2, view3);
+}
+
+- (void)testMaskViewsReleasedWhenPoolIsReleased {
+ UIView* retainedView;
+ @autoreleasepool {
+ FlutterClippingMaskViewPool* pool =
+ [[[FlutterClippingMaskViewPool alloc] initWithCapacity:2] autorelease];
+ FlutterClippingMaskView* view = [pool getMaskViewWithFrame:CGRectZero];
+ retainedView = [view retain];
+ XCTAssertGreaterThan(retainedView.retainCount, 1u);
+ }
+ // The only retain left is our manual retain called inside the autorelease pool, meaning the
+ // maskViews are dealloc'd.
+ XCTAssertEqual(retainedView.retainCount, 1u);
+}
+
+- (void)testClipMaskViewIsReused {
+ flutter::FlutterPlatformViewsTestMockPlatformViewDelegate mock_delegate;
+ auto thread_task_runner = CreateNewThread("FlutterPlatformViewsTest");
+ flutter::TaskRunners runners(/*label=*/self.name.UTF8String,
+ /*platform=*/thread_task_runner,
+ /*raster=*/thread_task_runner,
+ /*ui=*/thread_task_runner,
+ /*io=*/thread_task_runner);
+ auto flutterPlatformViewsController = std::make_shared<flutter::FlutterPlatformViewsController>();
+ auto platform_view = std::make_unique<flutter::PlatformViewIOS>(
+ /*delegate=*/mock_delegate,
+ /*rendering_api=*/flutter::IOSRenderingAPI::kSoftware,
+ /*platform_views_controller=*/flutterPlatformViewsController,
+ /*task_runners=*/runners);
+
+ FlutterPlatformViewsTestMockFlutterPlatformFactory* factory =
+ [[FlutterPlatformViewsTestMockFlutterPlatformFactory new] autorelease];
+ flutterPlatformViewsController->RegisterViewFactory(
+ factory, @"MockFlutterPlatformView",
+ FlutterPlatformViewGestureRecognizersBlockingPolicyEager);
+ FlutterResult result = ^(id result) {
+ };
+ flutterPlatformViewsController->OnMethodCall(
+ [FlutterMethodCall
+ methodCallWithMethodName:@"create"
+ arguments:@{@"id" : @1, @"viewType" : @"MockFlutterPlatformView"}],
+ result);
+
+ XCTAssertNotNil(gMockPlatformView);
+ UIView* mockFlutterView = [[[UIView alloc] initWithFrame:CGRectMake(0, 0, 10, 10)] autorelease];
+ flutterPlatformViewsController->SetFlutterView(mockFlutterView);
+ // Create embedded view params
+ flutter::MutatorsStack stack1;
+ // Layer tree always pushes a screen scale factor to the stack
+ SkMatrix screenScaleMatrix =
+ SkMatrix::Scale([UIScreen mainScreen].scale, [UIScreen mainScreen].scale);
+ stack1.PushTransform(screenScaleMatrix);
+ // Push a clip rect
+ SkRect rect = SkRect::MakeXYWH(2, 2, 3, 3);
+ stack1.PushClipRect(rect);
+
+ auto embeddedViewParams1 = std::make_unique<flutter::EmbeddedViewParams>(
+ screenScaleMatrix, SkSize::Make(10, 10), stack1);
+
+ flutter::MutatorsStack stack2;
+ auto embeddedViewParams2 = std::make_unique<flutter::EmbeddedViewParams>(
+ screenScaleMatrix, SkSize::Make(10, 10), stack2);
+
+ flutterPlatformViewsController->PrerollCompositeEmbeddedView(1, std::move(embeddedViewParams1));
+ flutterPlatformViewsController->CompositeEmbeddedView(1);
+ UIView* childClippingView1 = gMockPlatformView.superview.superview;
+ UIView* maskView1 = childClippingView1.maskView;
+ XCTAssertNotNil(maskView1);
+
+ // Composite a new frame.
+ auto embeddedViewParams3 = std::make_unique<flutter::EmbeddedViewParams>(
+ screenScaleMatrix, SkSize::Make(10, 10), stack2);
+ flutterPlatformViewsController->PrerollCompositeEmbeddedView(1, std::move(embeddedViewParams3));
+ flutterPlatformViewsController->CompositeEmbeddedView(1);
+ childClippingView1 = gMockPlatformView.superview.superview;
+
+ // This overrides gMockPlatformView to point to the newly created platform view.
+ flutterPlatformViewsController->OnMethodCall(
+ [FlutterMethodCall
+ methodCallWithMethodName:@"create"
+ arguments:@{@"id" : @2, @"viewType" : @"MockFlutterPlatformView"}],
+ result);
+
+ auto embeddedViewParams4 = std::make_unique<flutter::EmbeddedViewParams>(
+ screenScaleMatrix, SkSize::Make(10, 10), stack1);
+ flutterPlatformViewsController->PrerollCompositeEmbeddedView(2, std::move(embeddedViewParams4));
+ flutterPlatformViewsController->CompositeEmbeddedView(2);
+ UIView* childClippingView2 = gMockPlatformView.superview.superview;
+
+ UIView* maskView2 = childClippingView2.maskView;
+ XCTAssertEqual(maskView1, maskView2);
+ XCTAssertNotNil(childClippingView2.maskView);
+ XCTAssertNil(childClippingView1.maskView);
+}
+
// Return true if a correct visual effect view is found. It also implies all the validation in this
// method passes.
//
diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h
index 7ec51e3..1f767b3 100644
--- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h
+++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h
@@ -30,6 +30,8 @@
- (instancetype)initWithFrame:(CGRect)frame screenScale:(CGFloat)screenScale;
+- (void)reset;
+
// Adds a clip rect operation to the queue.
//
// The `clipSkRect` is transformed with the `matrix` before adding to the queue.
@@ -47,6 +49,28 @@
@end
+// A pool that provides |FlutterClippingMaskView|s.
+//
+// The pool has a capacity that can be set in the initializer.
+// When requesting a FlutterClippingMaskView, the pool will first try to reuse an available maskView
+// in the pool. If there are none available, a new FlutterClippingMaskView is constructed. If the
+// capacity is reached, the newly constructed FlutterClippingMaskView is not added to the pool.
+//
+// Call |recycleMaskViews| to mark all the FlutterClippingMaskViews in the pool available.
+@interface FlutterClippingMaskViewPool : NSObject
+
+// Initialize the pool with `capacity`. When the `capacity` is reached, a FlutterClippingMaskView is
+// constructed when requested, and it is not added to the pool.
+- (instancetype)initWithCapacity:(NSInteger)capacity;
+
+// Reuse a maskView from the pool, or allocate a new one.
+- (FlutterClippingMaskView*)getMaskViewWithFrame:(CGRect)frame;
+
+// Mark all the maskViews available.
+- (void)recycleMaskViews;
+
+@end
+
// An object represents a blur filter.
//
// This object produces a `backdropFilterView`.
@@ -268,6 +292,7 @@
// Traverse the `mutators_stack` and return the number of clip operations.
int CountClips(const MutatorsStack& mutators_stack);
+ void ClipViewSetMaskView(UIView* clipView);
// Applies the mutators in the mutators_stack to the UIView chain that was constructed by
// `ReconstructClipViewsChain`
//
@@ -328,6 +353,7 @@
fml::scoped_nsobject<FlutterMethodChannel> channel_;
fml::scoped_nsobject<UIView> flutter_view_;
fml::scoped_nsobject<UIViewController> flutter_view_controller_;
+ fml::scoped_nsobject<FlutterClippingMaskViewPool> mask_view_pool_;
std::map<std::string, fml::scoped_nsobject<NSObject<FlutterPlatformViewFactory>>> factories_;
std::map<int64_t, fml::scoped_nsobject<NSObject<FlutterPlatformView>>> views_;
std::map<int64_t, fml::scoped_nsobject<FlutterTouchInterceptingView>> touch_interceptors_;
diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm
index da7f156..c7df351 100644
--- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm
+++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm
@@ -271,6 +271,10 @@
return self;
}
+- (void)reset {
+ paths_.clear();
+}
+
// In some scenarios, when we add this view as a maskView of the ChildClippingView, iOS added
// this view as a subview of the ChildClippingView.
// This results this view blocking touch events on the ChildClippingView.
@@ -447,3 +451,66 @@
}
@end
+
+@interface FlutterClippingMaskViewPool ()
+
+// The maximum number of `FlutterClippingMaskView` the pool can contain.
+// This prevents the pool to grow infinately and limits the maximum memory a pool can use.
+@property(assign, nonatomic) NSUInteger capacity;
+@property(retain, nonatomic) NSMutableArray<FlutterClippingMaskView*>* pool;
+// The index points to the first available FlutterClippingMaskView in the `pool`.
+@property(assign, nonatomic) NSUInteger availableIndex;
+
+@end
+
+@implementation FlutterClippingMaskViewPool : NSObject
+
+- (instancetype)initWithCapacity:(NSInteger)capacity {
+ if (self = [super init]) {
+ _pool = [[NSMutableArray alloc] initWithCapacity:capacity];
+ _capacity = capacity;
+ _availableIndex = 0;
+ }
+ return self;
+}
+
+- (FlutterClippingMaskView*)getMaskViewWithFrame:(CGRect)frame {
+ FML_DCHECK(self.availableIndex <= self.capacity);
+ FlutterClippingMaskView* maskView;
+ if (self.availableIndex == self.capacity) {
+ // The pool is full, alloc a new one.
+ maskView =
+ [[[FlutterClippingMaskView alloc] initWithFrame:frame
+ screenScale:[UIScreen mainScreen].scale] autorelease];
+ return maskView;
+ }
+
+ if (self.availableIndex >= self.pool.count) {
+ // The pool doesn't have enough maskViews, alloc a new one and add to the pool.
+ maskView =
+ [[[FlutterClippingMaskView alloc] initWithFrame:frame
+ screenScale:[UIScreen mainScreen].scale] autorelease];
+ [self.pool addObject:maskView];
+ FML_DCHECK(self.pool.count <= self.capacity);
+ } else {
+ // Reuse a maskView from the pool.
+ maskView = [self.pool objectAtIndex:self.availableIndex];
+ maskView.frame = frame;
+ [maskView reset];
+ }
+ self.availableIndex++;
+ return maskView;
+}
+
+- (void)recycleMaskViews {
+ self.availableIndex = 0;
+}
+
+- (void)dealloc {
+ [_pool release];
+ _pool = nil;
+
+ [super dealloc];
+}
+
+@end