Skip to content

Commit

Permalink
Optimize font handling on iOS (#31764)
Browse files Browse the repository at this point in the history
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:
<img width="1656" alt="Screen Shot 2021-06-23 at 7 40 06 AM" src="https://user-images.githubusercontent.com/2466893/123092882-f4f55100-d3f8-11eb-906f-d25086049a18.png">
<img width="1656" alt="Screen Shot 2021-06-23 at 7 41 30 AM" src="https://user-images.githubusercontent.com/2466893/123092886-f6267e00-d3f8-11eb-89f6-cfd2cae9f7b6.png">

After:
<img width="1455" alt="Screen Shot 2021-06-23 at 9 02 54 AM" src="https://user-images.githubusercontent.com/2466893/123101899-7d2c2400-d402-11eb-97f8-2ee97ee69ec4.png">
<img width="1455" alt="Screen Shot 2021-06-23 at 8 59 44 AM" src="https://user-images.githubusercontent.com/2466893/123101892-7bfaf700-d402-11eb-9a10-def46b37b87f.png">

Changelog:
[iOS][Changed] - Optimized font handling

Pull Request resolved: #31764

Reviewed By: appden

Differential Revision: D30241725

Pulled By: yungsters

fbshipit-source-id: 342e4f6e5492926acd2afc7d645e6878846369fc
  • Loading branch information
Adlai-Holler authored and facebook-github-bot committed Aug 27, 2021
1 parent ec92c85 commit 4ac42d8
Show file tree
Hide file tree
Showing 3 changed files with 56 additions and 39 deletions.
1 change: 1 addition & 0 deletions BUCK
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
92 changes: 54 additions & 38 deletions React/Views/RCTFont.mm
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,16 @@

#import <CoreText/CoreText.h>

#import <mutex>

typedef CGFloat RCTFontWeight;
static RCTFontWeight weightOfFont(UIFont *font)
{
static NSArray *fontNames;
static NSArray *fontWeights;
static NSArray<NSString *> *weightSuffixes;
static NSArray<NSNumber *> *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",
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -130,18 +129,16 @@ static inline BOOL CompareFontWeights(UIFontWeight firstWeight, UIFontWeight sec

static UIFont *cachedSystemFont(CGFloat size, RCTFontWeight weight)
{
static NSCache *fontCache;
static std::mutex *fontCacheMutex = new std::mutex;

NSString *cacheKey = [NSString stringWithFormat:@"%.1f/%.2f", size, weight];
UIFont *font;
{
std::lock_guard<std::mutex> lock(*fontCacheMutex);
if (!fontCache) {
fontCache = [NSCache new];
}
font = [fontCache objectForKey:cacheKey];
}
static NSCache<NSValue *, UIFont *> *fontCache = [NSCache new];

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) {
Expand All @@ -151,15 +148,36 @@ static inline BOOL CompareFontWeights(UIFontWeight firstWeight, UIFontWeight sec
font = [UIFont systemFontOfSize:size weight:weight];
}

{
std::lock_guard<std::mutex> lock(*fontCacheMutex);
[fontCache setObject:font forKey:cacheKey];
}
[fontCache setObject:font forKey:cacheKey];
}

return font;
}

// Caching wrapper around expensive +[UIFont fontNamesForFamilyName:]
static NSArray<NSString *> *fontNamesForFamilyName(NSString *familyName)
{
static NSCache<NSString *, NSArray<NSString *> *> *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
Expand Down Expand Up @@ -315,7 +333,7 @@ + (UIFont *)updateFont:(UIFont *)font

// 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,
Expand All @@ -339,7 +357,8 @@ + (UIFont *)updateFont:(UIFont *)font

// Get the closest font that matches the given weight for the fontFamily
CGFloat closestWeight = INFINITY;
for (NSString *name in [UIFont fontNamesForFamilyName:familyName]) {
NSArray<NSString *> *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);
Expand All @@ -352,11 +371,8 @@ + (UIFont *)updateFont:(UIFont *)font

// 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
Expand Down
2 changes: 1 addition & 1 deletion packages/rn-tester/RNTesterUnitTests/RCTFontTests.m
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ @implementation RCTFontTests
// 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); \
}

Expand Down

0 comments on commit 4ac42d8

Please sign in to comment.