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