1. iOS字体替换方案概述
在iOS应用开发中,全局字体替换是一个常见的需求。许多设计师会为应用定制专属字体,但UIKit默认使用系统字体,手动修改每个UILabel的font属性既繁琐又容易遗漏。通过Runtime的Method Swizzling技术,我们可以实现一键替换所有UILabel字体的效果。
这个方案的核心思路是:在UILabel被添加到视图层级时(willMoveToSuperview:),自动将其字体替换为自定义字体。我们通过Category扩展UILabel,利用Objective-C的运行时特性交换原始方法和自定义方法,实现无侵入式的字体替换。
注意:Method Swizzling是强大的运行时技术,但不当使用可能导致难以调试的问题。务必确保交换的方法具有相同的参数和返回类型,并在+load方法中安全地执行交换操作。
2. 实现步骤详解
2.1 准备字体资源文件
首先需要将字体文件(.ttf或.otf格式)添加到项目中:
- 将字体文件拖入Xcode工程,确保勾选"Copy items if needed"和对应的target
- 在Info.plist中添加Fonts provided by application数组项
- 在数组中添加字体文件名(如loveway.ttf)
objective-c复制// 示例Info.plist配置
<key>UIAppFonts</key>
<array>
<string>loveway.ttf</string>
</array>
2.2 创建UILabel分类
我们通过Category扩展UILabel,实现字体替换逻辑:
objective-c复制// UILabel+FontChange.h
#import <UIKit/UIKit.h>
@interface UILabel (FontChange)
@end
对应的实现文件中,我们需要:
- 在+load方法中安全地执行方法交换
- 实现自定义的willMoveToSuperview:方法
- 处理字体替换逻辑
2.3 方法交换实现
方法交换的核心代码:
objective-c复制+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
SEL originalSel = @selector(willMoveToSuperview:);
SEL overrideSel = @selector(myWillMoveToSuperview:);
MethodSwizzle([self class], originalSel, overrideSel);
});
}
void MethodSwizzle(Class cls, SEL originalName, SEL overrideName) {
Method originalMethod = class_getInstanceMethod(cls, originalName);
Method overrideMethod = class_getInstanceMethod(cls, overrideName);
BOOL result = class_addMethod(cls, originalName,
method_getImplementation(overrideMethod),
method_getTypeEncoding(overrideMethod));
if (result) {
class_replaceMethod(cls, overrideName,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, overrideMethod);
}
}
3. 方法交换原理深度解析
3.1 方法交换的两种场景
Method Swizzling需要处理两种情况:
- 目标类未实现原方法:使用class_addMethod添加新实现,再替换方法实现
- 目标类已实现原方法:直接交换两个方法的实现
这种区分是为了避免意外修改父类的方法实现。class_addMethod会先检查方法是否已存在,如果返回YES表示我们添加了新方法,此时需要将交换后的方法指向原始实现;如果返回NO则表示方法已存在,可以直接交换。
3.2 willMoveToSuperview:的选择
我们选择交换willMoveToSuperview:方法是因为:
- 它在视图被添加到视图层级时调用,确保字体替换时机正确
- 比awakeFromNib更通用,适用于代码和xib创建的UILabel
- 比layoutSubviews性能更好,避免重复设置字体
3.3 自定义字体替换逻辑
在替换后的方法中,我们添加字体处理代码:
objective-c复制- (void)myWillMoveToSuperview:(UIView *)newSuperview {
[self myWillMoveToSuperview:newSuperview];
// 跳过UIButton的内部标签
if ([self isKindOfClass:NSClassFromString(@"UIButtonLabel")]) {
return;
}
if (self) {
// 特殊标签保留系统字体
if (self.tag == 10086) {
self.font = [UIFont systemFontOfSize:self.font.pointSize];
} else {
// 使用自定义字体
NSString *customFontName = @"FZLBJW--GB1-0";
if ([UIFont fontNamesForFamilyName:customFontName]) {
self.font = [UIFont fontWithName:customFontName size:self.font.pointSize];
}
}
}
}
4. 字体名称获取工具方法
4.1 读取TTF/OTF字体
objective-c复制- (UIFont *)customFontWithPath:(NSString *)path size:(CGFloat)size {
NSURL *fontUrl = [NSURL fileURLWithPath:path];
CGDataProviderRef fontDataProvider = CGDataProviderCreateWithURL((__bridge CFURLRef)fontUrl);
CGFontRef fontRef = CGFontCreateWithDataProvider(fontDataProvider);
CGDataProviderRelease(fontDataProvider);
CTFontManagerRegisterGraphicsFont(fontRef, NULL);
NSString *fontName = CFBridgingRelease(CGFontCopyPostScriptName(fontRef));
UIFont *font = [UIFont fontWithName:fontName size:size];
CGFontRelease(fontRef);
return font;
}
4.2 读取TTC字体集合
objective-c复制- (NSArray *)customFontArrayWithPath:(NSString *)path size:(CGFloat)size {
CFStringRef fontPath = CFStringCreateWithCString(NULL, [path UTF8String], kCFStringEncodingUTF8);
CFURLRef fontUrl = CFURLCreateWithFileSystemPath(NULL, fontPath, kCFURLPOSIXPathStyle, 0);
CFArrayRef fontArray = CTFontManagerCreateFontDescriptorsFromURL(fontUrl);
CTFontManagerRegisterFontsForURL(fontUrl, kCTFontManagerScopeNone, NULL);
NSMutableArray *customFontArray = [NSMutableArray array];
for (CFIndex i = 0; i < CFArrayGetCount(fontArray); i++) {
CTFontDescriptorRef descriptor = CFArrayGetValueAtIndex(fontArray, i);
CTFontRef fontRef = CTFontCreateWithFontDescriptor(descriptor, size, NULL);
NSString *fontName = CFBridgingRelease(CTFontCopyName(fontRef, kCTFontPostScriptNameKey));
UIFont *font = [UIFont fontWithName:fontName size:size];
[customFontArray addObject:font];
}
return customFontArray;
}
5. 实际应用中的注意事项
5.1 性能优化建议
- 字体缓存:将字体名称缓存起来,避免重复查询
- 避免重复设置:检查当前字体是否已经是目标字体
- 线程安全:确保字体操作在主线程执行
objective-c复制static NSString *cachedFontName = nil;
+ (NSString *)customFontName {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSArray *fontNames = [UIFont fontNamesForFamilyName:@"YourFontFamily"];
cachedFontName = fontNames.firstObject;
});
return cachedFontName;
}
5.2 常见问题排查
-
字体不生效:
- 检查Info.plist配置是否正确
- 确认字体文件名和实际文件完全一致(包括大小写)
- 使用[UIFont familyNames]打印可用字体列表验证
-
控制台警告:
- "Could not load the "XXX" font." 通常表示字体文件未正确添加到bundle
- "Font doesn't have the requested style." 可能使用了错误的字体名称
-
特殊视图处理:
- UIButton的标题需要使用UIButtonLabel类判断
- UITextField/UITextView需要单独处理
5.3 进阶使用技巧
- 动态字体支持:
objective-c复制// 监听动态字体大小变化
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(contentSizeCategoryDidChange:)
name:UIContentSizeCategoryDidChangeNotification
object:nil];
- 多主题字体支持:
objective-c复制// 根据主题使用不同字体
- (void)updateFontForTheme:(Theme)theme {
NSString *fontName = (theme == ThemeDark) ? @"DarkThemeFont" : @"LightThemeFont";
self.font = [UIFont fontWithName:fontName size:self.font.pointSize];
}
- Swift兼容:
swift复制// 在Swift中调用Objective-C的Method Swizzling
extension UILabel {
@objc static func swizzleMethods() {
let originalSelector = #selector(willMove(toSuperview:))
let swizzledSelector = #selector(swizzled_willMove(toSuperview:))
guard let originalMethod = class_getInstanceMethod(self, originalSelector),
let swizzledMethod = class_getInstanceMethod(self, swizzledSelector) else {
return
}
method_exchangeImplementations(originalMethod, swizzledMethod)
}
}
6. 替代方案比较
6.1 方法交换 vs 子类化
| 特性 | Method Swizzling | 子类化 |
|---|---|---|
| 侵入性 | 低,通过Category实现 | 高,需要修改所有UILabel创建代码 |
| 维护性 | 需要小心处理交换逻辑 | 更符合常规OOP实践 |
| 灵活性 | 可以保留特定标签的原字体 | 所有实例统一行为 |
| 适用场景 | 已有项目全局修改 | 新项目或需要严格控制的场景 |
6.2 其他实现方式
- 外观代理:
objective-c复制[[UILabel appearance] setFont:[UIFont fontWithName:@"CustomFont" size:17]];
局限:无法覆盖所有情况,如代码直接设置font属性会覆盖外观设置
- KVO观察:
objective-c复制[self addObserver:self forKeyPath:@"font" options:NSKeyValueObservingOptionNew context:nil];
问题:性能开销大,需要谨慎处理观察者生命周期
- 消息转发:
objective-c复制- (void)forwardInvocation:(NSInvocation *)anInvocation {
if ([NSStringFromSelector(anInvocation.selector) isEqualToString:@"setFont:"]) {
// 拦截字体设置
}
}
复杂度高,一般不推荐用于简单字体替换
在实际项目中,Method Swizzling提供了最佳的平衡点:足够的灵活性和较低的实施成本,同时保持代码的整洁性。