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

Commit 2ee8257

Browse files
author
Chris Yang
authored
[ios_platform_view] MaskView pool to reuse maskViews. (#38989)
* reuse maskView reuse mask view format format draft draft fixes test * comments * fix comment * test for releasing maskView * updates * fix
1 parent 7b68d71 commit 2ee8257

File tree

4 files changed

+242
-16
lines changed

4 files changed

+242
-16
lines changed

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

Lines changed: 30 additions & 16 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) {
@@ -451,6 +453,17 @@ static bool ClipRRectContainsPlatformViewBoundingRect(const SkRRect& clip_rrect,
451453
return clipCount;
452454
}
453455

456+
void FlutterPlatformViewsController::ClipViewSetMaskView(UIView* clipView) {
457+
if (clipView.maskView) {
458+
return;
459+
}
460+
UIView* flutterView = flutter_view_.get();
461+
CGRect frame =
462+
CGRectMake(-clipView.frame.origin.x, -clipView.frame.origin.y,
463+
CGRectGetWidth(flutterView.bounds), CGRectGetHeight(flutterView.bounds));
464+
clipView.maskView = [mask_view_pool_.get() getMaskViewWithFrame:frame];
465+
}
466+
454467
void FlutterPlatformViewsController::ApplyMutators(const MutatorsStack& mutators_stack,
455468
UIView* embedded_view,
456469
const SkRect& bounding_rect) {
@@ -461,18 +474,15 @@ static bool ClipRRectContainsPlatformViewBoundingRect(const SkRRect& clip_rrect,
461474
ResetAnchor(embedded_view.layer);
462475
ChildClippingView* clipView = (ChildClippingView*)embedded_view.superview;
463476

464-
CGFloat screenScale = [UIScreen mainScreen].scale;
465-
466-
UIView* flutter_view = flutter_view_.get();
467-
FlutterClippingMaskView* maskView = [[[FlutterClippingMaskView alloc]
468-
initWithFrame:CGRectMake(-clipView.frame.origin.x, -clipView.frame.origin.y,
469-
CGRectGetWidth(flutter_view.bounds),
470-
CGRectGetHeight(flutter_view.bounds))
471-
screenScale:screenScale] autorelease];
472-
473477
SkMatrix transformMatrix;
474478
NSMutableArray* blurFilters = [[[NSMutableArray alloc] init] autorelease];
475-
479+
FML_DCHECK(!clipView.maskView ||
480+
[clipView.maskView isKindOfClass:[FlutterClippingMaskView class]]);
481+
if (mask_view_pool_.get() == nil) {
482+
mask_view_pool_.reset([[FlutterClippingMaskViewPool alloc]
483+
initWithCapacity:kFlutterClippingMaskViewPoolCapacity]);
484+
}
485+
[mask_view_pool_.get() recycleMaskViews];
476486
clipView.maskView = nil;
477487
auto iter = mutators_stack.Begin();
478488
while (iter != mutators_stack.End()) {
@@ -486,25 +496,28 @@ static bool ClipRRectContainsPlatformViewBoundingRect(const SkRRect& clip_rrect,
486496
transformMatrix)) {
487497
break;
488498
}
489-
[maskView clipRect:(*iter)->GetRect() matrix:transformMatrix];
490-
clipView.maskView = maskView;
499+
ClipViewSetMaskView(clipView);
500+
[(FlutterClippingMaskView*)clipView.maskView clipRect:(*iter)->GetRect()
501+
matrix:transformMatrix];
491502
break;
492503
}
493504
case kClipRRect: {
494505
if (ClipRRectContainsPlatformViewBoundingRect((*iter)->GetRRect(), bounding_rect,
495506
transformMatrix)) {
496507
break;
497508
}
498-
[maskView clipRRect:(*iter)->GetRRect() matrix:transformMatrix];
499-
clipView.maskView = maskView;
509+
ClipViewSetMaskView(clipView);
510+
[(FlutterClippingMaskView*)clipView.maskView clipRRect:(*iter)->GetRRect()
511+
matrix:transformMatrix];
500512
break;
501513
}
502514
case kClipPath: {
503515
// TODO(cyanglaz): Find a way to pre-determine if path contains the PlatformView boudning
504516
// rect. See `ClipRRectContainsPlatformViewBoundingRect`.
505517
// https://github.com/flutter/flutter/issues/118650
506-
[maskView clipPath:(*iter)->GetPath() matrix:transformMatrix];
507-
clipView.maskView = maskView;
518+
ClipViewSetMaskView(clipView);
519+
[(FlutterClippingMaskView*)clipView.maskView clipPath:(*iter)->GetPath()
520+
matrix:transformMatrix];
508521
break;
509522
}
510523
case kOpacity:
@@ -551,6 +564,7 @@ static bool ClipRRectContainsPlatformViewBoundingRect(const SkRRect& clip_rrect,
551564
[clipView applyBlurBackdropFilters:blurFilters];
552565
}
553566

567+
CGFloat screenScale = [UIScreen mainScreen].scale;
554568
// The UIKit frame is set based on the logical resolution (points) instead of physical.
555569
// (https://developer.apple.com/library/archive/documentation/DeviceInformation/Reference/iOSDeviceCompatibility/Displays/Displays.html).
556570
// However, flow is based on the physical resolution. For example, 1000 pixels in flow equals

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

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2445,6 +2445,125 @@ - (void)testHasFirstResponderInViewHierarchySubtree_descendantViewBecomesFirstRe
24452445
XCTAssertFalse(view.flt_hasFirstResponderInViewHierarchySubtree);
24462446
}
24472447

2448+
- (void)testFlutterClippingMaskViewPoolReuseViewsAfterRecycle {
2449+
FlutterClippingMaskViewPool* pool =
2450+
[[[FlutterClippingMaskViewPool alloc] initWithCapacity:2] autorelease];
2451+
FlutterClippingMaskView* view1 = [pool getMaskViewWithFrame:CGRectZero];
2452+
FlutterClippingMaskView* view2 = [pool getMaskViewWithFrame:CGRectZero];
2453+
[pool recycleMaskViews];
2454+
CGRect newRect = CGRectMake(0, 0, 10, 10);
2455+
FlutterClippingMaskView* view3 = [pool getMaskViewWithFrame:newRect];
2456+
FlutterClippingMaskView* view4 = [pool getMaskViewWithFrame:newRect];
2457+
XCTAssertEqual(view1, view3);
2458+
XCTAssertEqual(view2, view4);
2459+
XCTAssertTrue(CGRectEqualToRect(view3.frame, newRect));
2460+
XCTAssertTrue(CGRectEqualToRect(view4.frame, newRect));
2461+
}
2462+
2463+
- (void)testFlutterClippingMaskViewPoolAllocsNewMaskViewsAfterReachingCapacity {
2464+
FlutterClippingMaskViewPool* pool =
2465+
[[[FlutterClippingMaskViewPool alloc] initWithCapacity:2] autorelease];
2466+
FlutterClippingMaskView* view1 = [pool getMaskViewWithFrame:CGRectZero];
2467+
FlutterClippingMaskView* view2 = [pool getMaskViewWithFrame:CGRectZero];
2468+
FlutterClippingMaskView* view3 = [pool getMaskViewWithFrame:CGRectZero];
2469+
XCTAssertNotEqual(view1, view3);
2470+
XCTAssertNotEqual(view2, view3);
2471+
}
2472+
2473+
- (void)testMaskViewsReleasedWhenPoolIsReleased {
2474+
UIView* retainedView;
2475+
@autoreleasepool {
2476+
FlutterClippingMaskViewPool* pool =
2477+
[[[FlutterClippingMaskViewPool alloc] initWithCapacity:2] autorelease];
2478+
FlutterClippingMaskView* view = [pool getMaskViewWithFrame:CGRectZero];
2479+
retainedView = [view retain];
2480+
XCTAssertGreaterThan(retainedView.retainCount, 1u);
2481+
}
2482+
// The only retain left is our manual retain called inside the autorelease pool, meaning the
2483+
// maskViews are dealloc'd.
2484+
XCTAssertEqual(retainedView.retainCount, 1u);
2485+
}
2486+
2487+
- (void)testClipMaskViewIsReused {
2488+
flutter::FlutterPlatformViewsTestMockPlatformViewDelegate mock_delegate;
2489+
auto thread_task_runner = CreateNewThread("FlutterPlatformViewsTest");
2490+
flutter::TaskRunners runners(/*label=*/self.name.UTF8String,
2491+
/*platform=*/thread_task_runner,
2492+
/*raster=*/thread_task_runner,
2493+
/*ui=*/thread_task_runner,
2494+
/*io=*/thread_task_runner);
2495+
auto flutterPlatformViewsController = std::make_shared<flutter::FlutterPlatformViewsController>();
2496+
auto platform_view = std::make_unique<flutter::PlatformViewIOS>(
2497+
/*delegate=*/mock_delegate,
2498+
/*rendering_api=*/flutter::IOSRenderingAPI::kSoftware,
2499+
/*platform_views_controller=*/flutterPlatformViewsController,
2500+
/*task_runners=*/runners);
2501+
2502+
FlutterPlatformViewsTestMockFlutterPlatformFactory* factory =
2503+
[[FlutterPlatformViewsTestMockFlutterPlatformFactory new] autorelease];
2504+
flutterPlatformViewsController->RegisterViewFactory(
2505+
factory, @"MockFlutterPlatformView",
2506+
FlutterPlatformViewGestureRecognizersBlockingPolicyEager);
2507+
FlutterResult result = ^(id result) {
2508+
};
2509+
flutterPlatformViewsController->OnMethodCall(
2510+
[FlutterMethodCall
2511+
methodCallWithMethodName:@"create"
2512+
arguments:@{@"id" : @1, @"viewType" : @"MockFlutterPlatformView"}],
2513+
result);
2514+
2515+
XCTAssertNotNil(gMockPlatformView);
2516+
UIView* mockFlutterView = [[[UIView alloc] initWithFrame:CGRectMake(0, 0, 10, 10)] autorelease];
2517+
flutterPlatformViewsController->SetFlutterView(mockFlutterView);
2518+
// Create embedded view params
2519+
flutter::MutatorsStack stack1;
2520+
// Layer tree always pushes a screen scale factor to the stack
2521+
SkMatrix screenScaleMatrix =
2522+
SkMatrix::Scale([UIScreen mainScreen].scale, [UIScreen mainScreen].scale);
2523+
stack1.PushTransform(screenScaleMatrix);
2524+
// Push a clip rect
2525+
SkRect rect = SkRect::MakeXYWH(2, 2, 3, 3);
2526+
stack1.PushClipRect(rect);
2527+
2528+
auto embeddedViewParams1 = std::make_unique<flutter::EmbeddedViewParams>(
2529+
screenScaleMatrix, SkSize::Make(10, 10), stack1);
2530+
2531+
flutter::MutatorsStack stack2;
2532+
auto embeddedViewParams2 = std::make_unique<flutter::EmbeddedViewParams>(
2533+
screenScaleMatrix, SkSize::Make(10, 10), stack2);
2534+
2535+
flutterPlatformViewsController->PrerollCompositeEmbeddedView(1, std::move(embeddedViewParams1));
2536+
flutterPlatformViewsController->CompositeEmbeddedView(1);
2537+
UIView* childClippingView1 = gMockPlatformView.superview.superview;
2538+
UIView* maskView1 = childClippingView1.maskView;
2539+
XCTAssertNotNil(maskView1);
2540+
2541+
// Composite a new frame.
2542+
auto embeddedViewParams3 = std::make_unique<flutter::EmbeddedViewParams>(
2543+
screenScaleMatrix, SkSize::Make(10, 10), stack2);
2544+
flutterPlatformViewsController->PrerollCompositeEmbeddedView(1, std::move(embeddedViewParams3));
2545+
flutterPlatformViewsController->CompositeEmbeddedView(1);
2546+
childClippingView1 = gMockPlatformView.superview.superview;
2547+
2548+
// This overrides gMockPlatformView to point to the newly created platform view.
2549+
flutterPlatformViewsController->OnMethodCall(
2550+
[FlutterMethodCall
2551+
methodCallWithMethodName:@"create"
2552+
arguments:@{@"id" : @2, @"viewType" : @"MockFlutterPlatformView"}],
2553+
result);
2554+
2555+
auto embeddedViewParams4 = std::make_unique<flutter::EmbeddedViewParams>(
2556+
screenScaleMatrix, SkSize::Make(10, 10), stack1);
2557+
flutterPlatformViewsController->PrerollCompositeEmbeddedView(2, std::move(embeddedViewParams4));
2558+
flutterPlatformViewsController->CompositeEmbeddedView(2);
2559+
UIView* childClippingView2 = gMockPlatformView.superview.superview;
2560+
2561+
UIView* maskView2 = childClippingView2.maskView;
2562+
XCTAssertEqual(maskView1, maskView2);
2563+
XCTAssertNotNil(childClippingView2.maskView);
2564+
XCTAssertNil(childClippingView1.maskView);
2565+
}
2566+
24482567
// Return true if a correct visual effect view is found. It also implies all the validation in this
24492568
// method passes.
24502569
//

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`.
@@ -268,6 +292,7 @@ class FlutterPlatformViewsController {
268292
// Traverse the `mutators_stack` and return the number of clip operations.
269293
int CountClips(const MutatorsStack& mutators_stack);
270294

295+
void ClipViewSetMaskView(UIView* clipView);
271296
// Applies the mutators in the mutators_stack to the UIView chain that was constructed by
272297
// `ReconstructClipViewsChain`
273298
//
@@ -328,6 +353,7 @@ class FlutterPlatformViewsController {
328353
fml::scoped_nsobject<FlutterMethodChannel> channel_;
329354
fml::scoped_nsobject<UIView> flutter_view_;
330355
fml::scoped_nsobject<UIViewController> flutter_view_controller_;
356+
fml::scoped_nsobject<FlutterClippingMaskViewPool> mask_view_pool_;
331357
std::map<std::string, fml::scoped_nsobject<NSObject<FlutterPlatformViewFactory>>> factories_;
332358
std::map<int64_t, fml::scoped_nsobject<NSObject<FlutterPlatformView>>> views_;
333359
std::map<int64_t, fml::scoped_nsobject<FlutterTouchInterceptingView>> touch_interceptors_;

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

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,10 @@ - (instancetype)initWithFrame:(CGRect)frame screenScale:(CGFloat)screenScale {
271271
return self;
272272
}
273273

274+
- (void)reset {
275+
paths_.clear();
276+
}
277+
274278
// In some scenarios, when we add this view as a maskView of the ChildClippingView, iOS added
275279
// this view as a subview of the ChildClippingView.
276280
// This results this view blocking touch events on the ChildClippingView.
@@ -447,3 +451,66 @@ - (void)clipPath:(const SkPath&)path matrix:(const SkMatrix&)matrix {
447451
}
448452

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

0 commit comments

Comments
 (0)