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

Commit 727b441

Browse files
author
Chris Yang
authored
Reland "[ios_platform_view] only recycle maskView when the view is applying mutators #42115" (#42823)
Relands #42115, which was reverted in #42231 due to a crash in the framework test. The crash is due to a memory management issue that I fixed it in this PR, I also added a scenario test to catch the crash. fixes: flutter/flutter#125620 See also: orignal PR #41573 for more details. [C++, Objective-C, Java style guides]: https://github.com/flutter/engine/blob/main/CONTRIBUTING.md#style
1 parent 20b4eab commit 727b441

13 files changed

+519
-70
lines changed

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

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,6 @@
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-
2321
@implementation UIView (FirstResponder)
2422
- (BOOL)flt_hasFirstResponderInViewHierarchySubtree {
2523
if (self.isFirstResponder) {
@@ -447,6 +445,8 @@ static bool ClipRRectContainsPlatformViewBoundingRect(const SkRRect& clip_rrect,
447445
clipView.maskView = [mask_view_pool_.get() getMaskViewWithFrame:frame];
448446
}
449447

448+
// This method is only called when the `embedded_view` needs to be re-composited at the current
449+
// frame. See: `CompositeWithParams` for details.
450450
void FlutterPlatformViewsController::ApplyMutators(const MutatorsStack& mutators_stack,
451451
UIView* embedded_view,
452452
const SkRect& bounding_rect) {
@@ -461,12 +461,10 @@ static bool ClipRRectContainsPlatformViewBoundingRect(const SkRRect& clip_rrect,
461461
NSMutableArray* blurFilters = [[[NSMutableArray alloc] init] autorelease];
462462
FML_DCHECK(!clipView.maskView ||
463463
[clipView.maskView isKindOfClass:[FlutterClippingMaskView class]]);
464-
if (mask_view_pool_.get() == nil) {
465-
mask_view_pool_.reset([[FlutterClippingMaskViewPool alloc]
466-
initWithCapacity:kFlutterClippingMaskViewPoolCapacity]);
464+
if (clipView.maskView) {
465+
[mask_view_pool_.get() insertViewToPoolIfNeeded:(FlutterClippingMaskView*)(clipView.maskView)];
466+
clipView.maskView = nil;
467467
}
468-
[mask_view_pool_.get() recycleMaskViews];
469-
clipView.maskView = nil;
470468
CGFloat screenScale = [UIScreen mainScreen].scale;
471469
auto iter = mutators_stack.Begin();
472470
while (iter != mutators_stack.End()) {
@@ -570,6 +568,14 @@ static bool ClipRRectContainsPlatformViewBoundingRect(const SkRRect& clip_rrect,
570568
embedded_view.layer.transform = flutter::GetCATransform3DFromSkMatrix(transformMatrix);
571569
}
572570

571+
// Composite the PlatformView with `view_id`.
572+
//
573+
// Every frame, during the paint traversal of the layer tree, this method is called for all
574+
// the PlatformViews in `views_to_recomposite_`.
575+
//
576+
// Note that `views_to_recomposite_` does not represent all the views in the view hierarchy,
577+
// if a PlatformView does not change its composition parameter from last frame, it is not
578+
// included in the `views_to_recomposite_`.
573579
void FlutterPlatformViewsController::CompositeWithParams(int64_t view_id,
574580
const EmbeddedViewParams& params) {
575581
CGRect frame = CGRectMake(0, 0, params.sizePoints().width(), params.sizePoints().height());

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

Lines changed: 83 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2703,12 +2703,15 @@ - (void)testFlutterClippingMaskViewPoolReuseViewsAfterRecycle {
27032703
[[[FlutterClippingMaskViewPool alloc] initWithCapacity:2] autorelease];
27042704
FlutterClippingMaskView* view1 = [pool getMaskViewWithFrame:CGRectZero];
27052705
FlutterClippingMaskView* view2 = [pool getMaskViewWithFrame:CGRectZero];
2706-
[pool recycleMaskViews];
2706+
[pool insertViewToPoolIfNeeded:view1];
2707+
[pool insertViewToPoolIfNeeded:view2];
27072708
CGRect newRect = CGRectMake(0, 0, 10, 10);
27082709
FlutterClippingMaskView* view3 = [pool getMaskViewWithFrame:newRect];
27092710
FlutterClippingMaskView* view4 = [pool getMaskViewWithFrame:newRect];
2710-
XCTAssertEqual(view1, view3);
2711-
XCTAssertEqual(view2, view4);
2711+
// view3 and view4 should randomly get either of view1 and view2.
2712+
NSSet* set1 = [NSSet setWithObjects:view1, view2, nil];
2713+
NSSet* set2 = [NSSet setWithObjects:view3, view4, nil];
2714+
XCTAssertEqualObjects(set1, set2);
27122715
XCTAssertTrue(CGRectEqualToRect(view3.frame, newRect));
27132716
XCTAssertTrue(CGRectEqualToRect(view4.frame, newRect));
27142717
}
@@ -2783,17 +2786,17 @@ - (void)testClipMaskViewIsReused {
27832786
auto embeddedViewParams1 = std::make_unique<flutter::EmbeddedViewParams>(
27842787
screenScaleMatrix, SkSize::Make(10, 10), stack1);
27852788

2786-
flutter::MutatorsStack stack2;
2787-
auto embeddedViewParams2 = std::make_unique<flutter::EmbeddedViewParams>(
2788-
screenScaleMatrix, SkSize::Make(10, 10), stack2);
2789-
27902789
flutterPlatformViewsController->PrerollCompositeEmbeddedView(1, std::move(embeddedViewParams1));
27912790
flutterPlatformViewsController->CompositeEmbeddedView(1);
27922791
UIView* childClippingView1 = gMockPlatformView.superview.superview;
27932792
UIView* maskView1 = childClippingView1.maskView;
27942793
XCTAssertNotNil(maskView1);
27952794

27962795
// Composite a new frame.
2796+
flutterPlatformViewsController->BeginFrame(SkISize::Make(100, 100));
2797+
flutter::MutatorsStack stack2;
2798+
auto embeddedViewParams2 = std::make_unique<flutter::EmbeddedViewParams>(
2799+
screenScaleMatrix, SkSize::Make(10, 10), stack2);
27972800
auto embeddedViewParams3 = std::make_unique<flutter::EmbeddedViewParams>(
27982801
screenScaleMatrix, SkSize::Make(10, 10), stack2);
27992802
flutterPlatformViewsController->PrerollCompositeEmbeddedView(1, std::move(embeddedViewParams3));
@@ -2819,6 +2822,79 @@ - (void)testClipMaskViewIsReused {
28192822
XCTAssertNil(childClippingView1.maskView);
28202823
}
28212824

2825+
- (void)testDifferentClipMaskViewIsUsedForEachView {
2826+
flutter::FlutterPlatformViewsTestMockPlatformViewDelegate mock_delegate;
2827+
auto thread_task_runner = CreateNewThread("FlutterPlatformViewsTest");
2828+
flutter::TaskRunners runners(/*label=*/self.name.UTF8String,
2829+
/*platform=*/thread_task_runner,
2830+
/*raster=*/thread_task_runner,
2831+
/*ui=*/thread_task_runner,
2832+
/*io=*/thread_task_runner);
2833+
auto flutterPlatformViewsController = std::make_shared<flutter::FlutterPlatformViewsController>();
2834+
auto platform_view = std::make_unique<flutter::PlatformViewIOS>(
2835+
/*delegate=*/mock_delegate,
2836+
/*rendering_api=*/flutter::IOSRenderingAPI::kSoftware,
2837+
/*platform_views_controller=*/flutterPlatformViewsController,
2838+
/*task_runners=*/runners,
2839+
/*worker_task_runner=*/nil,
2840+
/*is_gpu_disabled_sync_switch=*/nil);
2841+
2842+
FlutterPlatformViewsTestMockFlutterPlatformFactory* factory =
2843+
[[FlutterPlatformViewsTestMockFlutterPlatformFactory new] autorelease];
2844+
flutterPlatformViewsController->RegisterViewFactory(
2845+
factory, @"MockFlutterPlatformView",
2846+
FlutterPlatformViewGestureRecognizersBlockingPolicyEager);
2847+
FlutterResult result = ^(id result) {
2848+
};
2849+
2850+
flutterPlatformViewsController->OnMethodCall(
2851+
[FlutterMethodCall
2852+
methodCallWithMethodName:@"create"
2853+
arguments:@{@"id" : @1, @"viewType" : @"MockFlutterPlatformView"}],
2854+
result);
2855+
UIView* view1 = gMockPlatformView;
2856+
2857+
// This overwrites `gMockPlatformView` to another view.
2858+
flutterPlatformViewsController->OnMethodCall(
2859+
[FlutterMethodCall
2860+
methodCallWithMethodName:@"create"
2861+
arguments:@{@"id" : @2, @"viewType" : @"MockFlutterPlatformView"}],
2862+
result);
2863+
UIView* view2 = gMockPlatformView;
2864+
2865+
XCTAssertNotNil(gMockPlatformView);
2866+
UIView* mockFlutterView = [[[UIView alloc] initWithFrame:CGRectMake(0, 0, 10, 10)] autorelease];
2867+
flutterPlatformViewsController->SetFlutterView(mockFlutterView);
2868+
// Create embedded view params
2869+
flutter::MutatorsStack stack1;
2870+
// Layer tree always pushes a screen scale factor to the stack
2871+
SkMatrix screenScaleMatrix =
2872+
SkMatrix::Scale([UIScreen mainScreen].scale, [UIScreen mainScreen].scale);
2873+
stack1.PushTransform(screenScaleMatrix);
2874+
// Push a clip rect
2875+
SkRect rect = SkRect::MakeXYWH(2, 2, 3, 3);
2876+
stack1.PushClipRect(rect);
2877+
2878+
auto embeddedViewParams1 = std::make_unique<flutter::EmbeddedViewParams>(
2879+
screenScaleMatrix, SkSize::Make(10, 10), stack1);
2880+
2881+
flutter::MutatorsStack stack2;
2882+
stack2.PushClipRect(rect);
2883+
auto embeddedViewParams2 = std::make_unique<flutter::EmbeddedViewParams>(
2884+
screenScaleMatrix, SkSize::Make(10, 10), stack2);
2885+
2886+
flutterPlatformViewsController->PrerollCompositeEmbeddedView(1, std::move(embeddedViewParams1));
2887+
flutterPlatformViewsController->CompositeEmbeddedView(1);
2888+
UIView* childClippingView1 = view1.superview.superview;
2889+
2890+
flutterPlatformViewsController->PrerollCompositeEmbeddedView(2, std::move(embeddedViewParams2));
2891+
flutterPlatformViewsController->CompositeEmbeddedView(2);
2892+
UIView* childClippingView2 = view2.superview.superview;
2893+
UIView* maskView1 = childClippingView1.maskView;
2894+
UIView* maskView2 = childClippingView2.maskView;
2895+
XCTAssertNotEqual(maskView1, maskView2);
2896+
}
2897+
28222898
// Return true if a correct visual effect view is found. It also implies all the validation in this
28232899
// method passes.
28242900
//

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

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@
5656
// in the pool. If there are none available, a new FlutterClippingMaskView is constructed. If the
5757
// capacity is reached, the newly constructed FlutterClippingMaskView is not added to the pool.
5858
//
59-
// Call |recycleMaskViews| to mark all the FlutterClippingMaskViews in the pool available.
59+
// Call |insertViewToPoolIfNeeded:| to return a maskView to the pool.
6060
@interface FlutterClippingMaskViewPool : NSObject
6161

6262
// Initialize the pool with `capacity`. When the `capacity` is reached, a FlutterClippingMaskView is
@@ -66,8 +66,8 @@
6666
// Reuse a maskView from the pool, or allocate a new one.
6767
- (FlutterClippingMaskView*)getMaskViewWithFrame:(CGRect)frame;
6868

69-
// Mark all the maskViews available.
70-
- (void)recycleMaskViews;
69+
// Insert the `maskView` into the pool.
70+
- (void)insertViewToPoolIfNeeded:(FlutterClippingMaskView*)maskView;
7171

7272
@end
7373

@@ -291,27 +291,20 @@ class FlutterPlatformViewsController {
291291
int CountClips(const MutatorsStack& mutators_stack);
292292

293293
void ClipViewSetMaskView(UIView* clipView);
294+
294295
// Applies the mutators in the mutators_stack to the UIView chain that was constructed by
295296
// `ReconstructClipViewsChain`
296297
//
297-
// Clips are applied to the super view with a CALayer mask. Transforms are applied to the
298-
// current view that's at the head of the chain. For example the following mutators stack [T_1,
299-
// C_2, T_3, T_4, C_5, T_6] where T denotes a transform and C denotes a clip, will result in the
300-
// following UIView tree:
301-
//
302-
// C_2 -> C_5 -> PLATFORM_VIEW
303-
// (PLATFORM_VIEW is a subview of C_5 which is a subview of C_2)
304-
//
305-
// T_1 is applied to C_2, T_3 and T_4 are applied to C_5, and T_6 is applied to PLATFORM_VIEW.
306-
//
307-
// After each clip operation, we update the head to the super view of the current head.
298+
// Clips are applied to the `embedded_view`'s super view(|ChildClippingView|) using a
299+
// |FlutterClippingMaskView|. Transforms are applied to `embedded_view`
308300
//
309301
// The `bounding_rect` is the final bounding rect of the PlatformView
310302
// (EmbeddedViewParams::finalBoundingRect). If a clip mutator's rect contains the final bounding
311303
// rect of the PlatformView, the clip mutator is not applied for performance optimization.
312304
void ApplyMutators(const MutatorsStack& mutators_stack,
313305
UIView* embedded_view,
314306
const SkRect& bounding_rect);
307+
315308
void CompositeWithParams(int64_t view_id, const EmbeddedViewParams& params);
316309

317310
// Allocates a new FlutterPlatformViewLayer if needed, draws the pixels within the rect from

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

Lines changed: 27 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
#import "flutter/shell/platform/darwin/ios/ios_surface.h"
1010

1111
static int kMaxPointsInVerb = 4;
12+
static const NSUInteger kFlutterClippingMaskViewPoolCapacity = 5;
1213

1314
namespace flutter {
1415

@@ -26,7 +27,10 @@
2627

2728
FlutterPlatformViewsController::FlutterPlatformViewsController()
2829
: layer_pool_(std::make_unique<FlutterPlatformViewLayerPool>()),
29-
weak_factory_(std::make_unique<fml::WeakPtrFactory<FlutterPlatformViewsController>>(this)){};
30+
weak_factory_(std::make_unique<fml::WeakPtrFactory<FlutterPlatformViewsController>>(this)) {
31+
mask_view_pool_.reset(
32+
[[FlutterClippingMaskViewPool alloc] initWithCapacity:kFlutterClippingMaskViewPoolCapacity]);
33+
};
3034

3135
FlutterPlatformViewsController::~FlutterPlatformViewsController() = default;
3236

@@ -458,58 +462,51 @@ @interface FlutterClippingMaskViewPool ()
458462
// The maximum number of `FlutterClippingMaskView` the pool can contain.
459463
// This prevents the pool to grow infinately and limits the maximum memory a pool can use.
460464
@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;
465+
466+
// The pool contains the views that are available to use.
467+
// The number of items in the pool must not excceds `capacity`.
468+
@property(retain, nonatomic) NSMutableSet<FlutterClippingMaskView*>* pool;
464469

465470
@end
466471

467472
@implementation FlutterClippingMaskViewPool : NSObject
468473

469474
- (instancetype)initWithCapacity:(NSInteger)capacity {
470475
if (self = [super init]) {
471-
_pool = [[NSMutableArray alloc] initWithCapacity:capacity];
476+
// Most of cases, there are only one PlatformView in the scene.
477+
// Thus init with the capacity of 1.
478+
_pool = [[NSMutableSet alloc] initWithCapacity:1];
472479
_capacity = capacity;
473-
_availableIndex = 0;
474480
}
475481
return self;
476482
}
477483

478484
- (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 =
485+
FML_DCHECK(self.pool.count <= self.capacity);
486+
if (self.pool.count == 0) {
487+
// The pool is empty, alloc a new one.
488+
return
484489
[[[FlutterClippingMaskView alloc] initWithFrame:frame
485490
screenScale:[UIScreen mainScreen].scale] autorelease];
486-
return maskView;
487491
}
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++;
492+
FlutterClippingMaskView* maskView = [[[self.pool anyObject] retain] autorelease];
493+
maskView.frame = frame;
494+
[maskView reset];
495+
[self.pool removeObject:maskView];
503496
return maskView;
504497
}
505498

506-
- (void)recycleMaskViews {
507-
self.availableIndex = 0;
499+
- (void)insertViewToPoolIfNeeded:(FlutterClippingMaskView*)maskView {
500+
FML_DCHECK(![self.pool containsObject:maskView]);
501+
FML_DCHECK(self.pool.count <= self.capacity);
502+
if (self.pool.count == self.capacity) {
503+
return;
504+
}
505+
[self.pool addObject:maskView];
508506
}
509507

510508
- (void)dealloc {
511509
[_pool release];
512-
_pool = nil;
513510

514511
[super dealloc];
515512
}

0 commit comments

Comments
 (0)