From 4ac42d88ef60ae3fed7319851d47b93e98ac9afa Mon Sep 17 00:00:00 2001 From: Adlai Holler Date: Thu, 26 Aug 2021 22:40:03 -0700 Subject: [PATCH] Optimize font handling on iOS (#31764) Summary: Few issues I saw when profiling RNTester: - Repeatedly calling `-lowercaseString` during `weightOfFont` causes a TON of extra memory traffic, for no reason. - `NSCache` is thread-safe, so no need for a mutex. - Using `stringWithFormat:` for the cache key is slow. Use `NSValue` to store the data directly instead. - Calling `-fontDescriptor` in `isItalicFont` and `isCondensedFont` is overly expensive and unnecessary. - `+fontNamesForFamilyName:` is insanely expensive. Wrap it in a cache. Unscientific test on RNTester iPhone 11 Pro, memory & time. Before: Screen Shot 2021-06-23 at 7 40 06 AM Screen Shot 2021-06-23 at 7 41 30 AM After: Screen Shot 2021-06-23 at 9 02 54 AM Screen Shot 2021-06-23 at 8 59 44 AM Changelog: [iOS][Changed] - Optimized font handling Pull Request resolved: https://github.com/facebook/react-native/pull/31764 Reviewed By: appden Differential Revision: D30241725 Pulled By: yungsters fbshipit-source-id: 342e4f6e5492926acd2afc7d645e6878846369fc --- BUCK | 1 + React/Views/RCTFont.mm | 90 +++++++++++-------- .../RNTesterUnitTests/RCTFontTests.m | 2 +- 3 files changed, 55 insertions(+), 38 deletions(-) diff --git a/BUCK b/BUCK index b315295c97..8c53560e33 100644 --- a/BUCK +++ b/BUCK @@ -376,6 +376,7 @@ rn_xplat_cxx_library2( "$SDKROOT/System/Library/Frameworks/CFNetwork.framework", "$SDKROOT/System/Library/Frameworks/CoreGraphics.framework", "$SDKROOT/System/Library/Frameworks/CoreLocation.framework", + "$SDKROOT/System/Library/Frameworks/CoreText.framework", "$SDKROOT/System/Library/Frameworks/Foundation.framework", "$SDKROOT/System/Library/Frameworks/MapKit.framework", "$SDKROOT/System/Library/Frameworks/QuartzCore.framework", diff --git a/React/Views/RCTFont.mm b/React/Views/RCTFont.mm index 93e1a803c7..7a8cbbb19a 100644 --- a/React/Views/RCTFont.mm +++ b/React/Views/RCTFont.mm @@ -11,18 +11,16 @@ #import -#import - typedef CGFloat RCTFontWeight; static RCTFontWeight weightOfFont(UIFont *font) { - static NSArray *fontNames; - static NSArray *fontWeights; + static NSArray *weightSuffixes; + static NSArray *fontWeights; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ // We use two arrays instead of one map because // the order is important for suffix matching. - fontNames = @[ + weightSuffixes = @[ @"normal", @"ultralight", @"thin", @@ -54,28 +52,29 @@ static RCTFontWeight weightOfFont(UIFont *font) ]; }); - for (NSInteger i = 0; i < 0 || i < (unsigned)fontNames.count; i++) { - if ([font.fontName.lowercaseString hasSuffix:fontNames[i]]) { - return (RCTFontWeight)[fontWeights[i] doubleValue]; + NSString *fontName = font.fontName; + NSInteger i = 0; + for (NSString *suffix in weightSuffixes) { + // CFStringFind is much faster than any variant of rangeOfString: because it does not use a locale. + auto options = kCFCompareCaseInsensitive | kCFCompareAnchored | kCFCompareBackwards; + if (CFStringFind((CFStringRef)fontName, (CFStringRef)suffix, options).location != kCFNotFound) { + return (RCTFontWeight)fontWeights[i].doubleValue; } + i++; } - NSDictionary *traits = [font.fontDescriptor objectForKey:UIFontDescriptorTraitsAttribute]; + auto traits = (__bridge_transfer NSDictionary *)CTFontCopyTraits((CTFontRef)font); return (RCTFontWeight)[traits[UIFontWeightTrait] doubleValue]; } static BOOL isItalicFont(UIFont *font) { - NSDictionary *traits = [font.fontDescriptor objectForKey:UIFontDescriptorTraitsAttribute]; - UIFontDescriptorSymbolicTraits symbolicTraits = [traits[UIFontSymbolicTrait] unsignedIntValue]; - return (symbolicTraits & UIFontDescriptorTraitItalic) != 0; + return (CTFontGetSymbolicTraits((CTFontRef)font) & kCTFontTraitItalic) != 0; } static BOOL isCondensedFont(UIFont *font) { - NSDictionary *traits = [font.fontDescriptor objectForKey:UIFontDescriptorTraitsAttribute]; - UIFontDescriptorSymbolicTraits symbolicTraits = [traits[UIFontSymbolicTrait] unsignedIntValue]; - return (symbolicTraits & UIFontDescriptorTraitCondensed) != 0; + return (CTFontGetSymbolicTraits((CTFontRef)font) & kCTFontTraitCondensed) != 0; } static RCTFontHandler defaultFontHandler; @@ -130,18 +129,16 @@ static NSString *FontWeightDescriptionFromUIFontWeight(UIFontWeight fontWeight) static UIFont *cachedSystemFont(CGFloat size, RCTFontWeight weight) { - static NSCache *fontCache; - static std::mutex *fontCacheMutex = new std::mutex; + static NSCache *fontCache = [NSCache new]; - NSString *cacheKey = [NSString stringWithFormat:@"%.1f/%.2f", size, weight]; - UIFont *font; - { - std::lock_guard lock(*fontCacheMutex); - if (!fontCache) { - fontCache = [NSCache new]; - } - font = [fontCache objectForKey:cacheKey]; - } + struct __attribute__((__packed__)) CacheKey { + CGFloat size; + RCTFontWeight weight; + }; + + CacheKey key{size, weight}; + NSValue *cacheKey = [[NSValue alloc] initWithBytes:&key objCType:@encode(CacheKey)]; + UIFont *font = [fontCache objectForKey:cacheKey]; if (!font) { if (defaultFontHandler) { @@ -151,15 +148,36 @@ static UIFont *cachedSystemFont(CGFloat size, RCTFontWeight weight) font = [UIFont systemFontOfSize:size weight:weight]; } - { - std::lock_guard lock(*fontCacheMutex); - [fontCache setObject:font forKey:cacheKey]; - } + [fontCache setObject:font forKey:cacheKey]; } return font; } +// Caching wrapper around expensive +[UIFont fontNamesForFamilyName:] +static NSArray *fontNamesForFamilyName(NSString *familyName) +{ + static NSCache *> *cache; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + cache = [NSCache new]; + [NSNotificationCenter.defaultCenter + addObserverForName:(NSNotificationName)kCTFontManagerRegisteredFontsChangedNotification + object:nil + queue:nil + usingBlock:^(NSNotification *) { + [cache removeAllObjects]; + }]; + }); + + auto names = [cache objectForKey:familyName]; + if (!names) { + names = [UIFont fontNamesForFamilyName:familyName]; + [cache setObject:names forKey:familyName]; + } + return names; +} + @implementation RCTConvert (RCTFont) + (UIFont *)UIFont:(id)json @@ -315,7 +333,7 @@ RCT_ARRAY_CONVERTER(RCTFontVariantDescriptor) // Gracefully handle being given a font name rather than font family, for // example: "Helvetica Light Oblique" rather than just "Helvetica". - if (!didFindFont && [UIFont fontNamesForFamilyName:familyName].count == 0) { + if (!didFindFont && fontNamesForFamilyName(familyName).count == 0) { font = [UIFont fontWithName:familyName size:fontSize]; if (font) { // It's actually a font name, not a font family name, @@ -339,7 +357,8 @@ RCT_ARRAY_CONVERTER(RCTFontVariantDescriptor) // Get the closest font that matches the given weight for the fontFamily CGFloat closestWeight = INFINITY; - for (NSString *name in [UIFont fontNamesForFamilyName:familyName]) { + NSArray *names = fontNamesForFamilyName(familyName); + for (NSString *name in names) { UIFont *match = [UIFont fontWithName:name size:fontSize]; if (isItalic == isItalicFont(match) && isCondensed == isCondensedFont(match)) { CGFloat testWeight = weightOfFont(match); @@ -352,11 +371,8 @@ RCT_ARRAY_CONVERTER(RCTFontVariantDescriptor) // If we still don't have a match at least return the first font in the fontFamily // This is to support built-in font Zapfino and other custom single font families like Impact - if (!font) { - NSArray *names = [UIFont fontNamesForFamilyName:familyName]; - if (names.count > 0) { - font = [UIFont fontWithName:names[0] size:fontSize]; - } + if (!font && names.count > 0) { + font = [UIFont fontWithName:names[0] size:fontSize]; } // Apply font variants to font object diff --git a/packages/rn-tester/RNTesterUnitTests/RCTFontTests.m b/packages/rn-tester/RNTesterUnitTests/RCTFontTests.m index bb8f19b038..650b14f515 100644 --- a/packages/rn-tester/RNTesterUnitTests/RCTFontTests.m +++ b/packages/rn-tester/RNTesterUnitTests/RCTFontTests.m @@ -20,7 +20,7 @@ // will be different objects, but the same font, so this macro now explicitly // checks that fontName (which includes the style) and pointSize are equal. #define RCTAssertEqualFonts(font1, font2) { \ - XCTAssertTrue([font1.fontName isEqualToString:font2.fontName]); \ + XCTAssertEqualObjects(font1.fontName, font2.fontName); \ XCTAssertEqual(font1.pointSize,font2.pointSize); \ }