Skip to content

Commit 4ac42d8

Browse files
Adlai-Hollerfacebook-github-bot
authored andcommitted
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: <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
1 parent ec92c85 commit 4ac42d8

File tree

3 files changed

+56
-39
lines changed

3 files changed

+56
-39
lines changed

BUCK

+1
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,7 @@ rn_xplat_cxx_library2(
376376
"$SDKROOT/System/Library/Frameworks/CFNetwork.framework",
377377
"$SDKROOT/System/Library/Frameworks/CoreGraphics.framework",
378378
"$SDKROOT/System/Library/Frameworks/CoreLocation.framework",
379+
"$SDKROOT/System/Library/Frameworks/CoreText.framework",
379380
"$SDKROOT/System/Library/Frameworks/Foundation.framework",
380381
"$SDKROOT/System/Library/Frameworks/MapKit.framework",
381382
"$SDKROOT/System/Library/Frameworks/QuartzCore.framework",

React/Views/RCTFont.mm

+54-38
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,16 @@
1111

1212
#import <CoreText/CoreText.h>
1313

14-
#import <mutex>
15-
1614
typedef CGFloat RCTFontWeight;
1715
static RCTFontWeight weightOfFont(UIFont *font)
1816
{
19-
static NSArray *fontNames;
20-
static NSArray *fontWeights;
17+
static NSArray<NSString *> *weightSuffixes;
18+
static NSArray<NSNumber *> *fontWeights;
2119
static dispatch_once_t onceToken;
2220
dispatch_once(&onceToken, ^{
2321
// We use two arrays instead of one map because
2422
// the order is important for suffix matching.
25-
fontNames = @[
23+
weightSuffixes = @[
2624
@"normal",
2725
@"ultralight",
2826
@"thin",
@@ -54,28 +52,29 @@ static RCTFontWeight weightOfFont(UIFont *font)
5452
];
5553
});
5654

57-
for (NSInteger i = 0; i < 0 || i < (unsigned)fontNames.count; i++) {
58-
if ([font.fontName.lowercaseString hasSuffix:fontNames[i]]) {
59-
return (RCTFontWeight)[fontWeights[i] doubleValue];
55+
NSString *fontName = font.fontName;
56+
NSInteger i = 0;
57+
for (NSString *suffix in weightSuffixes) {
58+
// CFStringFind is much faster than any variant of rangeOfString: because it does not use a locale.
59+
auto options = kCFCompareCaseInsensitive | kCFCompareAnchored | kCFCompareBackwards;
60+
if (CFStringFind((CFStringRef)fontName, (CFStringRef)suffix, options).location != kCFNotFound) {
61+
return (RCTFontWeight)fontWeights[i].doubleValue;
6062
}
63+
i++;
6164
}
6265

63-
NSDictionary *traits = [font.fontDescriptor objectForKey:UIFontDescriptorTraitsAttribute];
66+
auto traits = (__bridge_transfer NSDictionary *)CTFontCopyTraits((CTFontRef)font);
6467
return (RCTFontWeight)[traits[UIFontWeightTrait] doubleValue];
6568
}
6669

6770
static BOOL isItalicFont(UIFont *font)
6871
{
69-
NSDictionary *traits = [font.fontDescriptor objectForKey:UIFontDescriptorTraitsAttribute];
70-
UIFontDescriptorSymbolicTraits symbolicTraits = [traits[UIFontSymbolicTrait] unsignedIntValue];
71-
return (symbolicTraits & UIFontDescriptorTraitItalic) != 0;
72+
return (CTFontGetSymbolicTraits((CTFontRef)font) & kCTFontTraitItalic) != 0;
7273
}
7374

7475
static BOOL isCondensedFont(UIFont *font)
7576
{
76-
NSDictionary *traits = [font.fontDescriptor objectForKey:UIFontDescriptorTraitsAttribute];
77-
UIFontDescriptorSymbolicTraits symbolicTraits = [traits[UIFontSymbolicTrait] unsignedIntValue];
78-
return (symbolicTraits & UIFontDescriptorTraitCondensed) != 0;
77+
return (CTFontGetSymbolicTraits((CTFontRef)font) & kCTFontTraitCondensed) != 0;
7978
}
8079

8180
static RCTFontHandler defaultFontHandler;
@@ -130,18 +129,16 @@ static inline BOOL CompareFontWeights(UIFontWeight firstWeight, UIFontWeight sec
130129

131130
static UIFont *cachedSystemFont(CGFloat size, RCTFontWeight weight)
132131
{
133-
static NSCache *fontCache;
134-
static std::mutex *fontCacheMutex = new std::mutex;
135-
136-
NSString *cacheKey = [NSString stringWithFormat:@"%.1f/%.2f", size, weight];
137-
UIFont *font;
138-
{
139-
std::lock_guard<std::mutex> lock(*fontCacheMutex);
140-
if (!fontCache) {
141-
fontCache = [NSCache new];
142-
}
143-
font = [fontCache objectForKey:cacheKey];
144-
}
132+
static NSCache<NSValue *, UIFont *> *fontCache = [NSCache new];
133+
134+
struct __attribute__((__packed__)) CacheKey {
135+
CGFloat size;
136+
RCTFontWeight weight;
137+
};
138+
139+
CacheKey key{size, weight};
140+
NSValue *cacheKey = [[NSValue alloc] initWithBytes:&key objCType:@encode(CacheKey)];
141+
UIFont *font = [fontCache objectForKey:cacheKey];
145142

146143
if (!font) {
147144
if (defaultFontHandler) {
@@ -151,15 +148,36 @@ static inline BOOL CompareFontWeights(UIFontWeight firstWeight, UIFontWeight sec
151148
font = [UIFont systemFontOfSize:size weight:weight];
152149
}
153150

154-
{
155-
std::lock_guard<std::mutex> lock(*fontCacheMutex);
156-
[fontCache setObject:font forKey:cacheKey];
157-
}
151+
[fontCache setObject:font forKey:cacheKey];
158152
}
159153

160154
return font;
161155
}
162156

157+
// Caching wrapper around expensive +[UIFont fontNamesForFamilyName:]
158+
static NSArray<NSString *> *fontNamesForFamilyName(NSString *familyName)
159+
{
160+
static NSCache<NSString *, NSArray<NSString *> *> *cache;
161+
static dispatch_once_t onceToken;
162+
dispatch_once(&onceToken, ^{
163+
cache = [NSCache new];
164+
[NSNotificationCenter.defaultCenter
165+
addObserverForName:(NSNotificationName)kCTFontManagerRegisteredFontsChangedNotification
166+
object:nil
167+
queue:nil
168+
usingBlock:^(NSNotification *) {
169+
[cache removeAllObjects];
170+
}];
171+
});
172+
173+
auto names = [cache objectForKey:familyName];
174+
if (!names) {
175+
names = [UIFont fontNamesForFamilyName:familyName];
176+
[cache setObject:names forKey:familyName];
177+
}
178+
return names;
179+
}
180+
163181
@implementation RCTConvert (RCTFont)
164182

165183
+ (UIFont *)UIFont:(id)json
@@ -315,7 +333,7 @@ + (UIFont *)updateFont:(UIFont *)font
315333

316334
// Gracefully handle being given a font name rather than font family, for
317335
// example: "Helvetica Light Oblique" rather than just "Helvetica".
318-
if (!didFindFont && [UIFont fontNamesForFamilyName:familyName].count == 0) {
336+
if (!didFindFont && fontNamesForFamilyName(familyName).count == 0) {
319337
font = [UIFont fontWithName:familyName size:fontSize];
320338
if (font) {
321339
// It's actually a font name, not a font family name,
@@ -339,7 +357,8 @@ + (UIFont *)updateFont:(UIFont *)font
339357

340358
// Get the closest font that matches the given weight for the fontFamily
341359
CGFloat closestWeight = INFINITY;
342-
for (NSString *name in [UIFont fontNamesForFamilyName:familyName]) {
360+
NSArray<NSString *> *names = fontNamesForFamilyName(familyName);
361+
for (NSString *name in names) {
343362
UIFont *match = [UIFont fontWithName:name size:fontSize];
344363
if (isItalic == isItalicFont(match) && isCondensed == isCondensedFont(match)) {
345364
CGFloat testWeight = weightOfFont(match);
@@ -352,11 +371,8 @@ + (UIFont *)updateFont:(UIFont *)font
352371

353372
// If we still don't have a match at least return the first font in the fontFamily
354373
// This is to support built-in font Zapfino and other custom single font families like Impact
355-
if (!font) {
356-
NSArray *names = [UIFont fontNamesForFamilyName:familyName];
357-
if (names.count > 0) {
358-
font = [UIFont fontWithName:names[0] size:fontSize];
359-
}
374+
if (!font && names.count > 0) {
375+
font = [UIFont fontWithName:names[0] size:fontSize];
360376
}
361377

362378
// Apply font variants to font object

packages/rn-tester/RNTesterUnitTests/RCTFontTests.m

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ @implementation RCTFontTests
2020
// will be different objects, but the same font, so this macro now explicitly
2121
// checks that fontName (which includes the style) and pointSize are equal.
2222
#define RCTAssertEqualFonts(font1, font2) { \
23-
XCTAssertTrue([font1.fontName isEqualToString:font2.fontName]); \
23+
XCTAssertEqualObjects(font1.fontName, font2.fontName); \
2424
XCTAssertEqual(font1.pointSize,font2.pointSize); \
2525
}
2626

0 commit comments

Comments
 (0)