Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Commit 66fec20

Browse files
author
Chris Yang
authored
Revert "Revert "[ios_platform_view] MaskView pool to reuse maskViews." (#39608)" (#40456)
Reland "[ios_platform_view] MaskView pool to reuse maskViews" (#39608)"
1 parent 3b2bbfc commit 66fec20

File tree

4 files changed

+238
-15
lines changed

4 files changed

+238
-15
lines changed

shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterViewController_Internal.h"
1919
#import "flutter/shell/platform/darwin/ios/ios_surface.h"
2020

21+
static const NSUInteger kFlutterClippingMaskViewPoolCapacity = 5;
22+
2123
@implementation UIView (FirstResponder)
2224
- (BOOL)flt_hasFirstResponderInViewHierarchySubtree {
2325
if (self.isFirstResponder) {
@@ -434,6 +436,17 @@ static bool ClipRRectContainsPlatformViewBoundingRect(const SkRRect& clip_rrect,
434436
return clipCount;
435437
}
436438

439+
void FlutterPlatformViewsController::ClipViewSetMaskView(UIView* clipView) {
440+
if (clipView.maskView) {
441+
return;
442+
}
443+
UIView* flutterView = flutter_view_.get();
444+
CGRect frame =
445+
CGRectMake(-clipView.frame.origin.x, -clipView.frame.origin.y,
446+
CGRectGetWidth(flutterView.bounds), CGRectGetHeight(flutterView.bounds));
447+
clipView.maskView = [mask_view_pool_.get() getMaskViewWithFrame:frame];
448+
}
449+
437450
void FlutterPlatformViewsController::ApplyMutators(const MutatorsStack& mutators_stack,
438451
UIView* embedded_view,
439452
const SkRect& bounding_rect) {
@@ -444,18 +457,17 @@ static bool ClipRRectContainsPlatformViewBoundingRect(const SkRRect& clip_rrect,
444457
ResetAnchor(embedded_view.layer);
445458
ChildClippingView* clipView = (ChildClippingView*)embedded_view.superview;
446459

447-
CGFloat screenScale = [UIScreen mainScreen].scale;
448-
449-
UIView* flutter_view = flutter_view_.get();
450-
FlutterClippingMaskView* maskView = [[[FlutterClippingMaskView alloc]
451-
initWithFrame:CGRectMake(-clipView.frame.origin.x, -clipView.frame.origin.y,
452-
CGRectGetWidth(flutter_view.bounds),
453-
CGRectGetHeight(flutter_view.bounds))
454-
screenScale:screenScale] autorelease];
455-
456460
SkMatrix transformMatrix;
457461
NSMutableArray* blurFilters = [[[NSMutableArray alloc] init] autorelease];
462+
FML_DCHECK(!clipView.maskView ||
463+
[clipView.maskView isKindOfClass:[FlutterClippingMaskView class]]);
464+
if (mask_view_pool_.get() == nil) {
465+
mask_view_pool_.reset([[FlutterClippingMaskViewPool alloc]
466+
initWithCapacity:kFlutterClippingMaskViewPoolCapacity]);
467+
}
468+
[mask_view_pool_.get() recycleMaskViews];
458469
clipView.maskView = nil;
470+
CGFloat screenScale = [UIScreen mainScreen].scale;
459471
auto iter = mutators_stack.Begin();
460472
while (iter != mutators_stack.End()) {
461473
switch ((*iter)->GetType()) {
@@ -468,25 +480,28 @@ static bool ClipRRectContainsPlatformViewBoundingRect(const SkRRect& clip_rrect,
468480
transformMatrix)) {
469481
break;
470482
}
471-
[maskView clipRect:(*iter)->GetRect() matrix:transformMatrix];
472-
clipView.maskView = maskView;
483+
ClipViewSetMaskView(clipView);
484+
[(FlutterClippingMaskView*)clipView.maskView clipRect:(*iter)->GetRect()
485+
matrix:transformMatrix];
473486
break;
474487
}
475488
case kClipRRect: {
476489
if (ClipRRectContainsPlatformViewBoundingRect((*iter)->GetRRect(), bounding_rect,
477490
transformMatrix)) {
478491
break;
479492
}
480-
[maskView clipRRect:(*iter)->GetRRect() matrix:transformMatrix];
481-
clipView.maskView = maskView;
493+
ClipViewSetMaskView(clipView);
494+
[(FlutterClippingMaskView*)clipView.maskView clipRRect:(*iter)->GetRRect()
495+
matrix:transformMatrix];
482496
break;
483497
}
484498
case kClipPath: {
485499
// TODO(cyanglaz): Find a way to pre-determine if path contains the PlatformView boudning
486500
// rect. See `ClipRRectContainsPlatformViewBoundingRect`.
487501
// https://github.com/flutter/flutter/issues/118650
488-
[maskView clipPath:(*iter)->GetPath() matrix:transformMatrix];
489-
clipView.maskView = maskView;
502+
ClipViewSetMaskView(clipView);
503+
[(FlutterClippingMaskView*)clipView.maskView clipPath:(*iter)->GetPath()
504+
matrix:transformMatrix];
490505
break;
491506
}
492507
case kOpacity:

shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2644,6 +2644,125 @@ - (void)testHasFirstResponderInViewHierarchySubtree_descendantViewBecomesFirstRe
26442644
XCTAssertFalse(view.flt_hasFirstResponderInViewHierarchySubtree);
26452645
}
26462646

2647+
- (void)testFlutterClippingMaskViewPoolReuseViewsAfterRecycle {
2648+
FlutterClippingMaskViewPool* pool =
2649+
[[[FlutterClippingMaskViewPool alloc] initWithCapacity:2] autorelease];
2650+
FlutterClippingMaskView* view1 = [pool getMaskViewWithFrame:CGRectZero];
2651+
FlutterClippingMaskView* view2 = [pool getMaskViewWithFrame:CGRectZero];
2652+
[pool recycleMaskViews];
2653+
CGRect newRect = CGRectMake(0, 0, 10, 10);
2654+
FlutterClippingMaskView* view3 = [pool getMaskViewWithFrame:newRect];
2655+
FlutterClippingMaskView* view4 = [pool getMaskViewWithFrame:newRect];
2656+
XCTAssertEqual(view1, view3);
2657+
XCTAssertEqual(view2, view4);
2658+
XCTAssertTrue(CGRectEqualToRect(view3.frame, newRect));
2659+
XCTAssertTrue(CGRectEqualToRect(view4.frame, newRect));
2660+
}
2661+
2662+
- (void)testFlutterClippingMaskViewPoolAllocsNewMaskViewsAfterReachingCapacity {
2663+
FlutterClippingMaskViewPool* pool =
2664+
[[[FlutterClippingMaskViewPool alloc] initWithCapacity:2] autorelease];
2665+
FlutterClippingMaskView* view1 = [pool getMaskViewWithFrame:CGRectZero];
2666+
FlutterClippingMaskView* view2 = [pool getMaskViewWithFrame:CGRectZero];
2667+
FlutterClippingMaskView* view3 = [pool getMaskViewWithFrame:CGRectZero];
2668+
XCTAssertNotEqual(view1, view3);
2669+
XCTAssertNotEqual(view2, view3);
2670+
}
2671+
2672+
- (void)testMaskViewsReleasedWhenPoolIsReleased {
2673+
UIView* retainedView;
2674+
@autoreleasepool {
2675+
FlutterClippingMaskViewPool* pool =
2676+
[[[FlutterClippingMaskViewPool alloc] initWithCapacity:2] autorelease];
2677+
FlutterClippingMaskView* view = [pool getMaskViewWithFrame:CGRectZero];
2678+
retainedView = [view retain];
2679+
XCTAssertGreaterThan(retainedView.retainCount, 1u);
2680+
}
2681+
// The only retain left is our manual retain called inside the autorelease pool, meaning the
2682+
// maskViews are dealloc'd.
2683+
XCTAssertEqual(retainedView.retainCount, 1u);
2684+
}
2685+
2686+
- (void)testClipMaskViewIsReused {
2687+
flutter::FlutterPlatformViewsTestMockPlatformViewDelegate mock_delegate;
2688+
auto thread_task_runner = CreateNewThread("FlutterPlatformViewsTest");
2689+
flutter::TaskRunners runners(/*label=*/self.name.UTF8String,
2690+
/*platform=*/thread_task_runner,
2691+
/*raster=*/thread_task_runner,
2692+
/*ui=*/thread_task_runner,
2693+
/*io=*/thread_task_runner);
2694+
auto flutterPlatformViewsController = std::make_shared<flutter::FlutterPlatformViewsController>();
2695+
auto platform_view = std::make_unique<flutter::PlatformViewIOS>(
2696+
/*delegate=*/mock_delegate,
2697+
/*rendering_api=*/flutter::IOSRenderingAPI::kSoftware,
2698+
/*platform_views_controller=*/flutterPlatformViewsController,
2699+
/*task_runners=*/runners);
2700+
2701+
FlutterPlatformViewsTestMockFlutterPlatformFactory* factory =
2702+
[[FlutterPlatformViewsTestMockFlutterPlatformFactory new] autorelease];
2703+
flutterPlatformViewsController->RegisterViewFactory(
2704+
factory, @"MockFlutterPlatformView",
2705+
FlutterPlatformViewGestureRecognizersBlockingPolicyEager);
2706+
FlutterResult result = ^(id result) {
2707+
};
2708+
flutterPlatformViewsController->OnMethodCall(
2709+
[FlutterMethodCall
2710+
methodCallWithMethodName:@"create"
2711+
arguments:@{@"id" : @1, @"viewType" : @"MockFlutterPlatformView"}],
2712+
result);
2713+
2714+
XCTAssertNotNil(gMockPlatformView);
2715+
UIView* mockFlutterView = [[[UIView alloc] initWithFrame:CGRectMake(0, 0, 10, 10)] autorelease];
2716+
flutterPlatformViewsController->SetFlutterView(mockFlutterView);
2717+
// Create embedded view params
2718+
flutter::MutatorsStack stack1;
2719+
// Layer tree always pushes a screen scale factor to the stack
2720+
SkMatrix screenScaleMatrix =
2721+
SkMatrix::Scale([UIScreen mainScreen].scale, [UIScreen mainScreen].scale);
2722+
stack1.PushTransform(screenScaleMatrix);
2723+
// Push a clip rect
2724+
SkRect rect = SkRect::MakeXYWH(2, 2, 3, 3);
2725+
stack1.PushClipRect(rect);
2726+
2727+
auto embeddedViewParams1 = std::make_unique<flutter::EmbeddedViewParams>(
2728+
screenScaleMatrix, SkSize::Make(10, 10), stack1);
2729+
2730+
flutter::MutatorsStack stack2;
2731+
auto embeddedViewParams2 = std::make_unique<flutter::EmbeddedViewParams>(
2732+
screenScaleMatrix, SkSize::Make(10, 10), stack2);
2733+
2734+
flutterPlatformViewsController->PrerollCompositeEmbeddedView(1, std::move(embeddedViewParams1));
2735+
flutterPlatformViewsController->CompositeEmbeddedView(1);
2736+
UIView* childClippingView1 = gMockPlatformView.superview.superview;
2737+
UIView* maskView1 = childClippingView1.maskView;
2738+
XCTAssertNotNil(maskView1);
2739+
2740+
// Composite a new frame.
2741+
auto embeddedViewParams3 = std::make_unique<flutter::EmbeddedViewParams>(
2742+
screenScaleMatrix, SkSize::Make(10, 10), stack2);
2743+
flutterPlatformViewsController->PrerollCompositeEmbeddedView(1, std::move(embeddedViewParams3));
2744+
flutterPlatformViewsController->CompositeEmbeddedView(1);
2745+
childClippingView1 = gMockPlatformView.superview.superview;
2746+
2747+
// This overrides gMockPlatformView to point to the newly created platform view.
2748+
flutterPlatformViewsController->OnMethodCall(
2749+
[FlutterMethodCall
2750+
methodCallWithMethodName:@"create"
2751+
arguments:@{@"id" : @2, @"viewType" : @"MockFlutterPlatformView"}],
2752+
result);
2753+
2754+
auto embeddedViewParams4 = std::make_unique<flutter::EmbeddedViewParams>(
2755+
screenScaleMatrix, SkSize::Make(10, 10), stack1);
2756+
flutterPlatformViewsController->PrerollCompositeEmbeddedView(2, std::move(embeddedViewParams4));
2757+
flutterPlatformViewsController->CompositeEmbeddedView(2);
2758+
UIView* childClippingView2 = gMockPlatformView.superview.superview;
2759+
2760+
UIView* maskView2 = childClippingView2.maskView;
2761+
XCTAssertEqual(maskView1, maskView2);
2762+
XCTAssertNotNil(childClippingView2.maskView);
2763+
XCTAssertNil(childClippingView1.maskView);
2764+
}
2765+
26472766
// Return true if a correct visual effect view is found. It also implies all the validation in this
26482767
// method passes.
26492768
//

shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@
3030

3131
- (instancetype)initWithFrame:(CGRect)frame screenScale:(CGFloat)screenScale;
3232

33+
- (void)reset;
34+
3335
// Adds a clip rect operation to the queue.
3436
//
3537
// The `clipSkRect` is transformed with the `matrix` before adding to the queue.
@@ -47,6 +49,28 @@
4749

4850
@end
4951

52+
// A pool that provides |FlutterClippingMaskView|s.
53+
//
54+
// The pool has a capacity that can be set in the initializer.
55+
// When requesting a FlutterClippingMaskView, the pool will first try to reuse an available maskView
56+
// in the pool. If there are none available, a new FlutterClippingMaskView is constructed. If the
57+
// capacity is reached, the newly constructed FlutterClippingMaskView is not added to the pool.
58+
//
59+
// Call |recycleMaskViews| to mark all the FlutterClippingMaskViews in the pool available.
60+
@interface FlutterClippingMaskViewPool : NSObject
61+
62+
// Initialize the pool with `capacity`. When the `capacity` is reached, a FlutterClippingMaskView is
63+
// constructed when requested, and it is not added to the pool.
64+
- (instancetype)initWithCapacity:(NSInteger)capacity;
65+
66+
// Reuse a maskView from the pool, or allocate a new one.
67+
- (FlutterClippingMaskView*)getMaskViewWithFrame:(CGRect)frame;
68+
69+
// Mark all the maskViews available.
70+
- (void)recycleMaskViews;
71+
72+
@end
73+
5074
// An object represents a blur filter.
5175
//
5276
// This object produces a `backdropFilterView`.
@@ -266,6 +290,7 @@ class FlutterPlatformViewsController {
266290
// Traverse the `mutators_stack` and return the number of clip operations.
267291
int CountClips(const MutatorsStack& mutators_stack);
268292

293+
void ClipViewSetMaskView(UIView* clipView);
269294
// Applies the mutators in the mutators_stack to the UIView chain that was constructed by
270295
// `ReconstructClipViewsChain`
271296
//
@@ -326,6 +351,7 @@ class FlutterPlatformViewsController {
326351
fml::scoped_nsobject<FlutterMethodChannel> channel_;
327352
fml::scoped_nsobject<UIView> flutter_view_;
328353
fml::scoped_nsobject<UIViewController> flutter_view_controller_;
354+
fml::scoped_nsobject<FlutterClippingMaskViewPool> mask_view_pool_;
329355
std::map<std::string, fml::scoped_nsobject<NSObject<FlutterPlatformViewFactory>>> factories_;
330356
std::map<int64_t, fml::scoped_nsobject<NSObject<FlutterPlatformView>>> views_;
331357
std::map<int64_t, fml::scoped_nsobject<FlutterTouchInterceptingView>> touch_interceptors_;

shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -452,3 +452,66 @@ - (void)clipPath:(const SkPath&)path matrix:(const SkMatrix&)matrix {
452452
}
453453

454454
@end
455+
456+
@interface FlutterClippingMaskViewPool ()
457+
458+
// The maximum number of `FlutterClippingMaskView` the pool can contain.
459+
// This prevents the pool to grow infinately and limits the maximum memory a pool can use.
460+
@property(assign, nonatomic) NSUInteger capacity;
461+
@property(retain, nonatomic) NSMutableArray<FlutterClippingMaskView*>* pool;
462+
// The index points to the first available FlutterClippingMaskView in the `pool`.
463+
@property(assign, nonatomic) NSUInteger availableIndex;
464+
465+
@end
466+
467+
@implementation FlutterClippingMaskViewPool : NSObject
468+
469+
- (instancetype)initWithCapacity:(NSInteger)capacity {
470+
if (self = [super init]) {
471+
_pool = [[NSMutableArray alloc] initWithCapacity:capacity];
472+
_capacity = capacity;
473+
_availableIndex = 0;
474+
}
475+
return self;
476+
}
477+
478+
- (FlutterClippingMaskView*)getMaskViewWithFrame:(CGRect)frame {
479+
FML_DCHECK(self.availableIndex <= self.capacity);
480+
FlutterClippingMaskView* maskView;
481+
if (self.availableIndex == self.capacity) {
482+
// The pool is full, alloc a new one.
483+
maskView =
484+
[[[FlutterClippingMaskView alloc] initWithFrame:frame
485+
screenScale:[UIScreen mainScreen].scale] autorelease];
486+
return maskView;
487+
}
488+
489+
if (self.availableIndex >= self.pool.count) {
490+
// The pool doesn't have enough maskViews, alloc a new one and add to the pool.
491+
maskView =
492+
[[[FlutterClippingMaskView alloc] initWithFrame:frame
493+
screenScale:[UIScreen mainScreen].scale] autorelease];
494+
[self.pool addObject:maskView];
495+
FML_DCHECK(self.pool.count <= self.capacity);
496+
} else {
497+
// Reuse a maskView from the pool.
498+
maskView = [self.pool objectAtIndex:self.availableIndex];
499+
maskView.frame = frame;
500+
[maskView reset];
501+
}
502+
self.availableIndex++;
503+
return maskView;
504+
}
505+
506+
- (void)recycleMaskViews {
507+
self.availableIndex = 0;
508+
}
509+
510+
- (void)dealloc {
511+
[_pool release];
512+
_pool = nil;
513+
514+
[super dealloc];
515+
}
516+
517+
@end

0 commit comments

Comments
 (0)