From b70709dbc27c75c69e9fd2b082ffa27e7e8db7fd Mon Sep 17 00:00:00 2001 From: Saad Najmi Date: Wed, 9 Oct 2024 16:42:17 -0700 Subject: [PATCH] fix(iOS): Replace uses of `CGColorRef` with UIColor to avoid manual memory management (#46847) Summary: Update React Native on iOS to do less manual memory management, by replacing uses of `CGColorRef` with `UIColor`, and only calling `UIColor.CGColor` when needed. This results in much less manual memory management, which is probably a good thing in 2024 :D. The downside is there is a breaking change: the signature of a method in `RCTBorderDrawing` changes. This is a followup to https://github.com/facebook/react-native/issues/46797 . After that PR merged and I tested merging React Native macOS to the `0.76-stable` branch cut commit again, I saw even more places I needed to manually call `CGColorRetain` / `CGColorRelease`. The reason is due to React Native macOS specifics (explained below) and I could update React Native macOS to not need these changes, but I thought I would at least throw up a PR to propose the changes, as it may be good to move away from using Core Graphics' C base API as much as possible. ## Longer Explanation With https://github.com/microsoft/react-native-macos/pull/2209 , I wrote a shim of [UIGraphicsImageRenderer](https://developer.apple.com/documentation/uikit/uigraphicsimagerenderer) for macOS. The main difference is my shim calls the NSImage API [imageWithSize:flipped:drawingHandler:](https://developer.apple.com/documentation/appkit/nsimage/1519860-imagewithsize?language=objc) to shim the UIGraphicsImageRenderer API [imageWithData:](https://developer.apple.com/documentation/uikit/uiimage/1624137-imagewithdata). The difference between the two is that the macOS API copies the block, and executes it when Appkit is about to draw the image, while the iOS API executes the block right away. Because of this, I think I am hitting way more places where `CGColorRef` variables needed to be retained / released. Additionally, I hit more of them when I merge to the 0.76 branch cut commit (this is why I didn't catch it with https://github.com/facebook/react-native/issues/46797). Given this constraint, I have a couple of options: 1. Refactor my macOS shim to use the deprecated API [[NSImage lockFocus]](https://developer.apple.com/documentation/appkit/nsimage/1519891-lockfocus) - I am not a fan of this because `lockFocus` was deprecated for `imageWithSize:flipped:drawingHandler:` 2. Refactor my macOS shim to do what we used to do: Create a CGContext manually and write it to an image - This is probably OK. Relies on less Appkit specifics, and potentially gives more control to RN on the rendering layer, which I recall lenaic told me is better for Fabric) 3. Refactor React Native to avoid CGColorRef altogether, and use `UIColor` (which ARC will memory manage for us) as much as possible. - This is the approach of this PR and my preferred approach. The downside is this changes the signature of some of the methods in `RCTBorderDrawing` which is a breaking change. I've seen other PRs do this, so maybe its OK? ## Changelog: [IOS] [BREAKING] - Replace uses of `CGColorRef` with UIColor to avoid manual memory management Pull Request resolved: https://github.com/facebook/react-native/pull/46847 Test Plan: Launching RNTester's View example (which tests a lot of the border / outline / shadow rendering) does not crash for me on both iOS and macOS. Reviewed By: NickGerleman Differential Revision: D63989547 Pulled By: joevilches fbshipit-source-id: 5e85e17567e3dd8b4b0388452398954ad2e02464 --- .../View/RCTViewComponentView.mm | 45 +++++++------------ .../React/Fabric/RCTConversions.h | 6 --- .../React/Views/RCTBorderDrawing.h | 10 ++--- .../React/Views/RCTBorderDrawing.m | 42 +++++++++-------- packages/react-native/React/Views/RCTView.m | 19 ++++---- 5 files changed, 49 insertions(+), 73 deletions(-) diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm index 04b2ede4f96c96..87983b0dbc1315 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm @@ -242,9 +242,8 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared & // `shadowColor` if (oldViewProps.shadowColor != newViewProps.shadowColor) { - CGColorRef shadowColor = RCTCreateCGColorRefFromSharedColor(newViewProps.shadowColor); - self.layer.shadowColor = shadowColor; - CGColorRelease(shadowColor); + UIColor *shadowColor = RCTUIColorFromSharedColor(newViewProps.shadowColor); + self.layer.shadowColor = shadowColor.CGColor; needsInvalidateLayer = YES; } @@ -682,7 +681,7 @@ static void RCTAddContourEffectToLayer( const RCTBorderStyle &contourStyle) { UIImage *image = RCTGetBorderImage( - contourStyle, layer.bounds.size, cornerRadii, contourInsets, contourColors, [UIColor clearColor].CGColor, NO); + contourStyle, layer.bounds.size, cornerRadii, contourInsets, contourColors, [UIColor clearColor], NO); if (image == nil) { layer.contents = nil; @@ -711,18 +710,10 @@ static void RCTAddContourEffectToLayer( static RCTBorderColors RCTCreateRCTBorderColorsFromBorderColors(BorderColors borderColors) { return RCTBorderColors{ - .top = RCTCreateCGColorRefFromSharedColor(borderColors.top), - .left = RCTCreateCGColorRefFromSharedColor(borderColors.left), - .bottom = RCTCreateCGColorRefFromSharedColor(borderColors.bottom), - .right = RCTCreateCGColorRefFromSharedColor(borderColors.right)}; -} - -static void RCTReleaseRCTBorderColors(RCTBorderColors borderColors) -{ - CGColorRelease(borderColors.top); - CGColorRelease(borderColors.left); - CGColorRelease(borderColors.bottom); - CGColorRelease(borderColors.right); + .top = RCTUIColorFromSharedColor(borderColors.top), + .left = RCTUIColorFromSharedColor(borderColors.left), + .bottom = RCTUIColorFromSharedColor(borderColors.bottom), + .right = RCTUIColorFromSharedColor(borderColors.right)}; } static CALayerCornerCurve CornerCurveFromBorderCurve(BorderCurve borderCurve) @@ -870,7 +861,7 @@ - (void)invalidateLayer (*borderMetrics.borderColors.left).getUIColor() != nullptr)); // background color - CGColorRef backgroundColor = [_backgroundColor resolvedColorWithTraitCollection:self.traitCollection].CGColor; + UIColor *backgroundColor = [_backgroundColor resolvedColorWithTraitCollection:self.traitCollection]; // The reason we sometimes do not set self.layer's backgroundColor is because // we want to support non-uniform border radii, which apple does not natively // support. To get this behavior we need to create a CGPath in the shape that @@ -880,7 +871,7 @@ - (void)invalidateLayer if (useCoreAnimationBorderRendering) { [_backgroundColorLayer removeFromSuperlayer]; _backgroundColorLayer = nil; - layer.backgroundColor = backgroundColor; + layer.backgroundColor = backgroundColor.CGColor; } else { layer.backgroundColor = nil; if (!_backgroundColorLayer) { @@ -890,7 +881,7 @@ - (void)invalidateLayer [self.layer addSublayer:_backgroundColorLayer]; } - _backgroundColorLayer.backgroundColor = backgroundColor; + _backgroundColorLayer.backgroundColor = backgroundColor.CGColor; if (borderMetrics.borderRadii.isUniform()) { _backgroundColorLayer.mask = nil; _backgroundColorLayer.cornerRadius = borderMetrics.borderRadii.topLeft.horizontal; @@ -911,9 +902,8 @@ - (void)invalidateLayer _borderLayer = nil; layer.borderWidth = (CGFloat)borderMetrics.borderWidths.left; - CGColorRef borderColor = RCTCreateCGColorRefFromSharedColor(borderMetrics.borderColors.left); - layer.borderColor = borderColor; - CGColorRelease(borderColor); + UIColor *borderColor = RCTUIColorFromSharedColor(borderMetrics.borderColors.left); + layer.borderColor = borderColor.CGColor; layer.cornerRadius = (CGFloat)borderMetrics.borderRadii.topLeft.horizontal; layer.cornerCurve = CornerCurveFromBorderCurve(borderMetrics.borderCurves.topLeft); } else { @@ -938,8 +928,6 @@ - (void)invalidateLayer borderColors, RCTUIEdgeInsetsFromEdgeInsets(borderMetrics.borderWidths), RCTBorderStyleFromBorderStyle(borderMetrics.borderStyles.left)); - - RCTReleaseRCTBorderColors(borderColors); } // outline @@ -958,12 +946,11 @@ - (void)invalidateLayer layer.bounds, -_props->outlineOffset - _props->outlineWidth, -_props->outlineOffset - _props->outlineWidth); if (borderMetrics.borderRadii.isUniform() && borderMetrics.borderRadii.topLeft.horizontal == 0) { - CGColorRef outlineColor = RCTCreateCGColorRefFromSharedColor(_props->outlineColor); + UIColor *outlineColor = RCTUIColorFromSharedColor(_props->outlineColor); _outlineLayer.borderWidth = _props->outlineWidth; - _outlineLayer.borderColor = outlineColor; - CGColorRelease(outlineColor); + _outlineLayer.borderColor = outlineColor.CGColor; } else { - CGColorRef outlineColor = RCTCreateCGColorRefFromSharedColor(_props->outlineColor); + UIColor *outlineColor = RCTUIColorFromSharedColor(_props->outlineColor); RCTAddContourEffectToLayer( _outlineLayer, @@ -971,8 +958,6 @@ - (void)invalidateLayer RCTBorderColors{outlineColor, outlineColor, outlineColor, outlineColor}, UIEdgeInsets{_props->outlineWidth, _props->outlineWidth, _props->outlineWidth, _props->outlineWidth}, RCTBorderStyleFromOutlineStyle(_props->outlineStyle)); - - CGColorRelease(outlineColor); } } diff --git a/packages/react-native/React/Fabric/RCTConversions.h b/packages/react-native/React/Fabric/RCTConversions.h index 204c54686322ba..f93d8937ea0255 100644 --- a/packages/react-native/React/Fabric/RCTConversions.h +++ b/packages/react-native/React/Fabric/RCTConversions.h @@ -40,12 +40,6 @@ inline UIColor *_Nullable RCTUIColorFromSharedColor(const facebook::react::Share return RCTPlatformColorFromColor(*sharedColor); } -inline CF_RETURNS_RETAINED CGColorRef _Nullable RCTCreateCGColorRefFromSharedColor( - const facebook::react::SharedColor &sharedColor) -{ - return CGColorRetain(RCTUIColorFromSharedColor(sharedColor).CGColor); -} - inline CGPoint RCTCGPointFromPoint(const facebook::react::Point &point) { return {point.x, point.y}; diff --git a/packages/react-native/React/Views/RCTBorderDrawing.h b/packages/react-native/React/Views/RCTBorderDrawing.h index 39105666f63daf..b13ffdf6fbadeb 100644 --- a/packages/react-native/React/Views/RCTBorderDrawing.h +++ b/packages/react-native/React/Views/RCTBorderDrawing.h @@ -29,10 +29,10 @@ typedef struct { } RCTCornerInsets; typedef struct { - CGColorRef top; - CGColorRef left; - CGColorRef bottom; - CGColorRef right; + UIColor *top; + UIColor *left; + UIColor *bottom; + UIColor *right; } RCTBorderColors; /** @@ -67,5 +67,5 @@ RCT_EXTERN UIImage *RCTGetBorderImage( RCTCornerRadii cornerRadii, UIEdgeInsets borderInsets, RCTBorderColors borderColors, - CGColorRef backgroundColor, + UIColor *backgroundColor, BOOL drawToEdge); diff --git a/packages/react-native/React/Views/RCTBorderDrawing.m b/packages/react-native/React/Views/RCTBorderDrawing.m index 67bf10cba935b0..b51499af33d093 100644 --- a/packages/react-native/React/Views/RCTBorderDrawing.m +++ b/packages/react-native/React/Views/RCTBorderDrawing.m @@ -30,9 +30,9 @@ BOOL RCTCornerRadiiAreEqualAndSymmetrical(RCTCornerRadii cornerRadii) BOOL RCTBorderColorsAreEqual(RCTBorderColors borderColors) { - return CGColorEqualToColor(borderColors.left, borderColors.right) && - CGColorEqualToColor(borderColors.left, borderColors.top) && - CGColorEqualToColor(borderColors.left, borderColors.bottom); + return CGColorEqualToColor(borderColors.left.CGColor, borderColors.right.CGColor) && + CGColorEqualToColor(borderColors.left.CGColor, borderColors.top.CGColor) && + CGColorEqualToColor(borderColors.left.CGColor, borderColors.bottom.CGColor); } RCTCornerInsets RCTGetCornerInsets(RCTCornerRadii cornerRadii, UIEdgeInsets edgeInsets) @@ -182,9 +182,9 @@ static CGPathRef RCTPathCreateOuterOutline(BOOL drawToEdge, CGRect rect, RCTCorn } static UIGraphicsImageRenderer * -RCTMakeUIGraphicsImageRenderer(CGSize size, CGColorRef backgroundColor, BOOL hasCornerRadii, BOOL drawToEdge) +RCTMakeUIGraphicsImageRenderer(CGSize size, UIColor *backgroundColor, BOOL hasCornerRadii, BOOL drawToEdge) { - const CGFloat alpha = CGColorGetAlpha(backgroundColor); + const CGFloat alpha = CGColorGetAlpha(backgroundColor.CGColor); const BOOL opaque = (drawToEdge || !hasCornerRadii) && alpha == 1.0; UIGraphicsImageRendererFormat *const rendererFormat = [UIGraphicsImageRendererFormat defaultFormat]; rendererFormat.opaque = opaque; @@ -197,7 +197,7 @@ static CGPathRef RCTPathCreateOuterOutline(BOOL drawToEdge, CGRect rect, RCTCorn CGSize viewSize, UIEdgeInsets borderInsets, RCTBorderColors borderColors, - CGColorRef backgroundColor, + UIColor *backgroundColor, BOOL drawToEdge) { const BOOL hasCornerRadii = RCTCornerRadiiAreAboveThreshold(cornerRadii); @@ -233,18 +233,16 @@ static CGPathRef RCTPathCreateOuterOutline(BOOL drawToEdge, CGRect rect, RCTCorn UIGraphicsImageRenderer *const imageRenderer = RCTMakeUIGraphicsImageRenderer(size, backgroundColor, hasCornerRadii, drawToEdge); - CGColorRetain(backgroundColor); UIImage *image = [imageRenderer imageWithActions:^(UIGraphicsImageRendererContext *_Nonnull rendererContext) { const CGContextRef context = rendererContext.CGContext; const CGRect rect = {.size = size}; CGPathRef path = RCTPathCreateOuterOutline(drawToEdge, rect, cornerRadii); if (backgroundColor) { - CGContextSetFillColorWithColor(context, backgroundColor); + CGContextSetFillColorWithColor(context, backgroundColor.CGColor); CGContextAddPath(context, path); CGContextFillPath(context); } - CGColorRelease(backgroundColor); CGContextAddPath(context, path); CGPathRelease(path); @@ -256,7 +254,7 @@ static CGPathRef RCTPathCreateOuterOutline(BOOL drawToEdge, CGRect rect, RCTCorn BOOL hasEqualColors = RCTBorderColorsAreEqual(borderColors); if ((drawToEdge || !hasCornerRadii) && hasEqualColors) { - CGContextSetFillColorWithColor(context, borderColors.left); + CGContextSetFillColorWithColor(context, borderColors.left.CGColor); CGContextAddRect(context, rect); CGContextAddPath(context, insetPath); CGContextEOFillPath(context); @@ -321,7 +319,7 @@ static CGPathRef RCTPathCreateOuterOutline(BOOL drawToEdge, CGRect rect, RCTCorn } } - CGColorRef currentColor = NULL; + UIColor *currentColor = nil; // RIGHT if (borderInsets.right > 0) { @@ -345,8 +343,8 @@ static CGPathRef RCTPathCreateOuterOutline(BOOL drawToEdge, CGRect rect, RCTCorn (CGPoint){size.width, size.height}, }; - if (!CGColorEqualToColor(currentColor, borderColors.bottom)) { - CGContextSetFillColorWithColor(context, currentColor); + if (!CGColorEqualToColor(currentColor.CGColor, borderColors.bottom.CGColor)) { + CGContextSetFillColorWithColor(context, currentColor.CGColor); CGContextFillPath(context); currentColor = borderColors.bottom; } @@ -362,8 +360,8 @@ static CGPathRef RCTPathCreateOuterOutline(BOOL drawToEdge, CGRect rect, RCTCorn (CGPoint){0, size.height}, }; - if (!CGColorEqualToColor(currentColor, borderColors.left)) { - CGContextSetFillColorWithColor(context, currentColor); + if (!CGColorEqualToColor(currentColor.CGColor, borderColors.left.CGColor)) { + CGContextSetFillColorWithColor(context, currentColor.CGColor); CGContextFillPath(context); currentColor = borderColors.left; } @@ -379,15 +377,15 @@ static CGPathRef RCTPathCreateOuterOutline(BOOL drawToEdge, CGRect rect, RCTCorn (CGPoint){size.width, 0}, }; - if (!CGColorEqualToColor(currentColor, borderColors.top)) { - CGContextSetFillColorWithColor(context, currentColor); + if (!CGColorEqualToColor(currentColor.CGColor, borderColors.top.CGColor)) { + CGContextSetFillColorWithColor(context, currentColor.CGColor); CGContextFillPath(context); currentColor = borderColors.top; } CGContextAddLines(context, points, sizeof(points) / sizeof(*points)); } - CGContextSetFillColorWithColor(context, currentColor); + CGContextSetFillColorWithColor(context, currentColor.CGColor); CGContextFillPath(context); } @@ -467,7 +465,7 @@ static CGPathRef RCTPathCreateOuterOutline(BOOL drawToEdge, CGRect rect, RCTCorn CGSize viewSize, UIEdgeInsets borderInsets, RCTBorderColors borderColors, - CGColorRef backgroundColor, + UIColor *backgroundColor, BOOL drawToEdge) { NSCParameterAssert(borderStyle == RCTBorderStyleDashed || borderStyle == RCTBorderStyleDotted); @@ -494,7 +492,7 @@ static CGPathRef RCTPathCreateOuterOutline(BOOL drawToEdge, CGRect rect, RCTCorn CGContextAddPath(context, outerPath); CGPathRelease(outerPath); - CGContextSetFillColorWithColor(context, backgroundColor); + CGContextSetFillColorWithColor(context, backgroundColor.CGColor); CGContextFillPath(context); } @@ -513,7 +511,7 @@ static CGPathRef RCTPathCreateOuterOutline(BOOL drawToEdge, CGRect rect, RCTCorn CGContextSetStrokeColorWithColor(context, [UIColor yellowColor].CGColor); CGContextAddPath(context, path); - CGContextSetStrokeColorWithColor(context, borderColors.top); + CGContextSetStrokeColorWithColor(context, borderColors.top.CGColor); CGContextStrokePath(context); CGPathRelease(path); @@ -526,7 +524,7 @@ static CGPathRef RCTPathCreateOuterOutline(BOOL drawToEdge, CGRect rect, RCTCorn RCTCornerRadii cornerRadii, UIEdgeInsets borderInsets, RCTBorderColors borderColors, - CGColorRef backgroundColor, + UIColor *backgroundColor, BOOL drawToEdge) { switch (borderStyle) { diff --git a/packages/react-native/React/Views/RCTView.m b/packages/react-native/React/Views/RCTView.m index 8602dedc50dfc2..b07e7ef9e54b92 100644 --- a/packages/react-native/React/Views/RCTView.m +++ b/packages/react-native/React/Views/RCTView.m @@ -774,10 +774,10 @@ - (RCTBorderColors)borderColorsWithTraitCollection:(UITraitCollection *)traitCol [directionAwareBorderRightColor resolvedColorWithTraitCollection:self.traitCollection]; return (RCTBorderColors){ - (borderTopColor ?: borderColor).CGColor, - (directionAwareBorderLeftColor ?: borderColor).CGColor, - (borderBottomColor ?: borderColor).CGColor, - (directionAwareBorderRightColor ?: borderColor).CGColor, + (borderTopColor ?: borderColor), + (directionAwareBorderLeftColor ?: borderColor), + (borderBottomColor ?: borderColor), + (directionAwareBorderRightColor ?: borderColor), }; } @@ -814,21 +814,20 @@ - (void)displayLayer:(CALayer *)layer // the content. For this reason, only use iOS border drawing when clipping // or when the border is hidden. - (borderInsets.top == 0 || (borderColors.top && CGColorGetAlpha(borderColors.top) == 0) || self.clipsToBounds); + (borderInsets.top == 0 || (borderColors.top && CGColorGetAlpha(borderColors.top.CGColor) == 0) || + self.clipsToBounds); // iOS clips to the outside of the border, but CSS clips to the inside. To // solve this, we'll need to add a container view inside the main view to // correctly clip the subviews. - CGColorRef backgroundColor; - - backgroundColor = [_backgroundColor resolvedColorWithTraitCollection:self.traitCollection].CGColor; + UIColor *backgroundColor = [_backgroundColor resolvedColorWithTraitCollection:self.traitCollection]; if (useIOSBorderRendering) { layer.cornerRadius = cornerRadii.topLeftHorizontal; - layer.borderColor = borderColors.left; + layer.borderColor = borderColors.left.CGColor; layer.borderWidth = borderInsets.left; - layer.backgroundColor = backgroundColor; + layer.backgroundColor = backgroundColor.CGColor; layer.contents = nil; layer.needsDisplayOnBoundsChange = NO; layer.mask = nil;