blob: d29db98b0d9a83010029381d7b2afbf850bcba6d [file] [log] [blame]
/*
Copyright 2015-present Google Inc.. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#import "MDFFontDiskLoader.h"
#import <CoreText/CoreText.h>
@interface MDFFontDiskLoader ()
@property(nonatomic) BOOL disableSanityChecks; // For tests to turn off asserts
@end
// Use |isFontURLLoaded:| and |setFontURL:loaded:| for access to loaded fonts.
static NSMutableSet<NSURL *> *gLoadedFonts;
// The queue that ensures |gLoadedFonts| is accessed in a thread safe manner.
static dispatch_queue_t gLoadedFontsQueue;
/*
This class caches the registered state of fileURL because it is much faster than calling the
CoreText api.
Benchmark testing results for a iPod touch Model A1421 running 9.2.1 (13D11) on 7/21/2016:
==============================
first load runtime: 6.729903 ms
iterations: 1000
single instance of FontDiskLoader
unoptimizedLoad Avg. Runtime: 1.312834 ms
load Avg. Runtime: 0.006540 ms
ratio first/unoptimized: 5.126241
ratio first/optimized: 1029.037109
ratio unoptimized/optimized: 200.739151
new instances of FontDiskLoader
unoptimizedLoad Avg. Runtime: 1.384893 ms
load Avg. Runtime: 0.096831 ms
ratio first/unoptimized: 4.859511
ratio first/optimized: 69.501534
ratio unoptimized/optimized: 14.302166
==============================
The unoptimizedLoad was calling just the CoreText API. As you can see the unoptimizedLoad was on
average 1.3 ms. This is unacceptable in situations in which you may want to display more than 20
different labels as is the case for a scrolling collection view because it would cause frame drops.
*/
@implementation MDFFontDiskLoader
@synthesize loadFailed = _loadFailed;
+ (void)initialize {
static dispatch_once_t once;
dispatch_once(&once, ^{
gLoadedFonts = [[NSMutableSet alloc] init];
gLoadedFontsQueue =
dispatch_queue_create("com.google.MDF.FontDiskLoaderQueue", DISPATCH_QUEUE_SERIAL);
});
}
// A font's loaded state is shared accross all fontloaders that share the same URL. This class
// method ensures that the collection of URLs that are loaded are set in a thread safe manner.
// By using a serial queue we ensure that reads and writes do not occur at the same time.
+ (void)setFontURL:(NSURL *)fontURL loaded:(BOOL)loaded {
if (!fontURL) {
return;
}
dispatch_async(gLoadedFontsQueue, ^{
// Defensive bool cast because https://www.bignerdranch.com/blog/bools-sharp-corners
if ((bool)loaded == (bool)[gLoadedFonts containsObject:fontURL]) {
return; // Already in the correct state;
}
if (loaded) {
[gLoadedFonts addObject:fontURL];
} else {
[gLoadedFonts removeObject:fontURL];
}
});
}
// A font's loaded state is shared accross all fontloaders that share the same URL. This class
// method ensures that the collection of URLs that are loaded are gotten in a thread safe manner.
// By using a serial queue we ensure that reads and writes do not occur at the same time.
+ (BOOL)isFontURLLoaded:(NSURL *)fontURL {
if (!fontURL) {
return NO;
}
__block BOOL isLoaded = NO;
dispatch_sync(gLoadedFontsQueue, ^{
isLoaded = [gLoadedFonts containsObject:fontURL];
});
return isLoaded;
}
- (instancetype)initWithFontName:(NSString *)fontName fontURL:(NSURL *)fontURL {
self = [super init];
if (self) {
_fontName = [fontName copy];
_fontURL = [fontURL copy];
}
return self;
}
- (instancetype)initWithFontName:(NSString *)fontName
filename:(NSString *)filename
bundleFileName:(NSString *)bundleFilename
baseBundle:(NSBundle *)baseBundle {
NSURL *fontURL =
[baseBundle URLForResource:filename withExtension:nil subdirectory:bundleFilename];
if (!fontURL) {
NSLog(@"Failed to locate: %@ in bundle: %@ %@.", filename, baseBundle, bundleFilename);
return nil;
}
return [self initWithFontName:fontName fontURL:fontURL];
}
#pragma mark - NSObject overridden methods
- (NSString *)description {
NSMutableString *description = [super.description mutableCopy];
[description appendFormat:@" font name: %@;", _fontName];
if (self.loaded) {
[description appendString:@" loaded = YES;"];
} else if (_loadFailed) {
[description appendString:@" failed registration = YES;"];
}
[description appendFormat:@" font url: %@;", _fontURL];
return [description copy];
}
- (BOOL)isEqual:(id)object {
if (!object || ![object isKindOfClass:[self class]]) {
return NO;
}
MDFFontDiskLoader *otherObject = (MDFFontDiskLoader *)object;
BOOL fontNamesAreEqual = [otherObject.fontName isEqualToString:_fontName];
return fontNamesAreEqual && [otherObject.fontURL isEqual:_fontURL];
}
- (NSUInteger)hash {
return _fontName.hash ^ _fontURL.hash;
}
- (id)copyWithZone:(NSZone *)zone {
return self;
}
#pragma mark - Public methods
- (BOOL)load {
@synchronized(self) {
BOOL loaded = [MDFFontDiskLoader isFontURLLoaded:_fontURL];
if (loaded) {
return YES;
}
if (_loadFailed) {
return NO;
}
CFErrorRef error = NULL;
loaded = CTFontManagerRegisterFontsForURL((__bridge CFURLRef)_fontURL,
kCTFontManagerScopeProcess, &error);
if (!loaded) {
if (error && CFErrorGetCode(error) == kCTFontManagerErrorAlreadyRegistered) {
// If it's already been loaded by somebody else, we don't care.
// We do not check the error domain to make sure they match because
// kCTFontManagerErrorDomain is not defined in the iOS 8 SDK.
// Radar 18651170 iOS 8 SDK missing definition for kCTFontManagerErrorDomain
loaded = YES;
} else {
NSLog(@"Failed to load font: %@", error);
_loadFailed = YES;
}
}
if (error) {
CFRelease(error);
}
[MDFFontDiskLoader setFontURL:_fontURL loaded:loaded];
return loaded;
}
}
- (BOOL)unload {
@synchronized(self) {
BOOL loaded = [MDFFontDiskLoader isFontURLLoaded:_fontURL];
_loadFailed = NO;
if (!loaded) {
return YES;
}
CFErrorRef error = NULL;
loaded = !CTFontManagerUnregisterFontsForURL((__bridge CFURLRef)_fontURL,
kCTFontManagerScopeProcess, &error);
if (loaded || error) {
NSLog(@"Failed to unload font: %@", error);
}
[MDFFontDiskLoader setFontURL:_fontURL loaded:loaded];
return !loaded;
}
}
- (UIFont *)fontOfSize:(CGFloat)fontSize {
[self load];
UIFont *font = [UIFont fontWithName:_fontName size:fontSize];
NSAssert(_disableSanityChecks || font, @"Failed to find font: %@ in file at %@", _fontName,
_fontURL);
return font;
}
- (BOOL)isLoaded {
return [MDFFontDiskLoader isFontURLLoaded:_fontURL];
}
- (BOOL)hasLoadFailed {
@synchronized(self) {
return _loadFailed;
}
}
@end