Flutter 字体生效原理解析
一、说明Flutter作为跨平台UI框架其核心能力在于实现一套代码的多端一致性渲染。字体文本渲染作为保障应用UI视觉统一和文案展示规范的基础能力在开发中具有关键作用。开发人员通常通过Dart层的TextStyle和全局ThemeData配置字体家族、字重和样式即可实现Android/iOS多平台的正常文本展示。二、字体生效原理我们在创建 MaterialApp 的时候会传入 ThemeData示例MaterialApp( theme: ThemeData( fontFamily: CustomFont, useMaterial3: true, ), );在构造函数中如果你配置了自己的字体则会用自己的如果没有配置Flutter 会为你配置一套主题字体。theme_data.dart我们再看一下 typography.dart 干了什么根据不同的平台配置不同的字体Android 默认主题iOS 默认主题所以在 Framework 层其实已经做了兜底策略你如果不手动设置的话它会配置一套系统的默认字体。源码中提到 iOS 的字体使用了 San Francisco 的字体主题。A Material Design text theme with dark glyphs based on San Francisco.这儿的 CupertinoSystemDisplay 和 CupertinoSystemText 名称是怎样去找到系统的 SF 字体的呢首先Flutter 引擎层会通过不同的字体管理器去获取不同的字体。引擎层源码font_collection.ccstd::vectorsk_spSkTypeface FontCollection::findTypefaces(const std::vectorSkString familyNames, SkFontStyle fontStyle, const std::optionalFontArguments fontArgs) { std::vectorsk_spSkTypeface typefaces; for (const SkString familyName : familyNames) { // 匹配字体 sk_spSkTypeface match matchTypeface(familyName, fontStyle); if (match fontArgs) { match fontArgs-CloneTypeface(match); } if (match) { typefaces.emplace_back(std::move(match)); } } return typefaces; } // 查找字体 sk_spSkTypeface FontCollection::matchTypeface(const SkString familyName, SkFontStyle fontStyle) { for (const auto manager : this-getFontManagerOrder()) { // 匹配字体 sk_spSkFontStyleSet set(manager-matchFamily(familyName.c_str())); // 没找到字体找下一个 if (!set || set-count() 0) { continue; } // 如果找到了就匹配样式字重、宽度、斜体等 sk_spSkTypeface match(set-matchStyle(fontStyle)); if (match) { return match; } } return nullptr; } // 获取字体管理器数组 std::vectorsk_spSkFontMgr FontCollection::getFontManagerOrder() const { std::vectorsk_spSkFontMgr order; if (fDynamicFontManager) { order.push_back(fDynamicFontManager); } if (fAssetFontManager) { order.push_back(fAssetFontManager); } if (fTestFontManager) { order.push_back(fTestFontManager); } if (fDefaultFontManager fEnableFontFallback) { order.push_back(fDefaultFontManager); } return order; }flutter 会维护 4 种不同的字体管理器查找字体的时候会根据顺序依次查找DynamicFontManager 系统特定字体如iOS的SF Pro Display和动态注册的字体。AssetFontManager pubspec注册的应用字体自定义字体TestFontManager 仅用于测试DefaultFontManager 平台默认字体如iOS的SF Pro Text当使用 CupertinoSystemText 时 先在动态字体管理器中查找找不到再在资源字体管理器中查找找不到最终在默认字体管理器(CoreText)中找到SF Pro TextCoreText根据获取到 SF Pro TextCupertinoSystemDisplay 对应的 SF Pro Display 对应的加载方式我们继续查看源码FontCollection::RegisterFonts( const std::shared_ptrAssetManager asset_manager) { #if FML_OS_MACOSX || FML_OS_IOS RegisterSystemFonts(*dynamic_font_manager_); #endif进一步查看 platform_mac.mm// Apple system font larger than size 29 returns SFProDisplay typeface. static const CGFloat kSFProDisplayBreakPoint 29; // Font name represents the SF Pro Display system font on Apple platforms. static const std::string kSFProDisplayName CupertinoSystemDisplay; void RegisterSystemFonts(const DynamicFontManager dynamic_font_manager) { auto register_weighted_font [dynamic_font_manager](const int weight) { sk_spSkTypeface large_system_font_weighted SkMakeTypefaceFromCTFont(MatchSystemUIFont(weight, kSFProDisplayBreakPoint)); if (large_system_font_weighted) { dynamic_font_manager.font_provider().RegisterTypeface(large_system_font_weighted, kSFProDisplayName); } }; for (int i 0; i 8; i) { const int font_weight i * 100; register_weighted_font(font_weight); } // The value 780 returns a font weight of 800. register_weighted_font(780); // The value of 810 returns a font weight of 900. register_weighted_font(810); }使用 MatchSystemUIFont 函数通过CoreText的 CTFontCreateUIFontForLanguage 获取系统字体。由于 CoreText 是 Apple 开发的闭源系统库用于文本渲染和排版。Apple 只提供该函数的声明头文件 和 二进制库 不开放实现源码我们只能看到这一层。指定字体大小为 kSFProDisplayBreakPoint 29确保获取的是SF Pro Display字体。通过 dynamic_font_manager.font_provider().RegisterTypeface 将获取到的SF Pro Display字体注册为 CupertinoSystemDisplay 名称这样Flutter框架就可以通过这个名称使用SF字体。而安卓主题注释信息中有提到A Material Design text theme with dark glyphs based on Roboto.它是基于 robot 字体的主题。以上的两种字体只是英文的为什么没有提到中文呢进一步查看 text_style.dart 中的说明/// The fallback order is:////// * [fontFamily]/// * [fontFamilyFallback] in order of first to last./// * System fallback fonts which will vary depending on platform. Flutter通过智能字体回退机制来解决找不到字体的问题。回退顺序为fontFamily (主要字体 - Roboto)fontFamilyFallback (自定义回退字体列表 - Android 默认为空)System fallback fonts (系统回退字体 - 关键)我们从源码中可以看到底层并未设置 fontFamilyFallback所以它主要是通过系统层的回退机制去查找的。安卓系统的回退机制为Roboto (主要字体) → Noto Sans CJK (中文) → Noto Color Emoji (表情) → 其他系统字体 iOS 的回退机制为San Francisco → PingFang SC → 系统默认中文字体举例Flutter 在安卓端显示混合文本Hello 你好Text(Hello 你好)字符级别的字体查找H → Roboto ✅ (使用 Roboto)e → Roboto ✅ (使用 Roboto)l → Roboto ✅ (使用 Roboto)l → Roboto ✅ (使用 Roboto)o → Roboto ✅ (使用 Roboto) → Roboto ✅ (使用 Roboto)你 → Roboto ❌ → 系统回退 → Noto Sans CJK ✅ (使用 Noto Sans CJK)好 → Roboto ❌ → 系统回退 → Noto Sans CJK ✅ (使用 Noto Sans CJK) → Roboto ✅ (使用 Roboto) → Roboto ❌ → 系统回退 → Noto Color Emoji ✅ (使用 Noto Color Emoji)所以在不手动设置 Flutter 字体的情况下其使用的是系统默认的字体。三、字重生效原理Flutter 会先找到字体后再去尝试查找字重。// 查找字体 sk_spSkTypeface FontCollection::matchTypeface(const SkString familyName, SkFontStyle fontStyle) { for (const auto manager : this-getFontManagerOrder()) { // 匹配字体 sk_spSkFontStyleSet set(manager-matchFamily(familyName.c_str())); // 没找到字体找下一个 if (!set || set-count() 0) { continue; } // 如果找到了就匹配样式字重、宽度、斜体等 sk_spSkTypeface match(set-matchStyle(fontStyle)); if (match) { return match; } } return nullptr; }matchStyle 部分又做了什么呢typeface_font_asset_provider.ccsk_spSkTypeface TypefaceFontStyleSet::matchStyle(const SkFontStyle pattern) { return matchStyleCSS3(pattern); }SKFontMgr.cpp/** * Width has the greatest priority. * If the value of pattern.width is 5 (normal) or less, * narrower width values are checked first, then wider values. * If the value of pattern.width is greater than 5 (normal), * wider values are checked first, followed by narrower values. * * Italic/Oblique has the next highest priority. * If italic requested and there is some italic font, use it. * If oblique requested and there is some oblique font, use it. * If italic requested and there is some oblique font, use it. * If oblique requested and there is some italic font, use it. * * Exact match. * If pattern.weight 400, weights below pattern.weight are checked * in descending order followed by weights above pattern.weight * in ascending order until a match is found. * If pattern.weight 500, weights above pattern.weight are checked * in ascending order followed by weights below pattern.weight * in descending order until a match is found. * If pattern.weight is 400, 500 is checked first * and then the rule for pattern.weight 400 is used. * If pattern.weight is 500, 400 is checked first * and then the rule for pattern.weight 400 is used. */ sk_spSkTypeface SkFontStyleSet::matchStyleCSS3(const SkFontStyle pattern) { int count this-count(); if (0 count) { return nullptr; } struct Score { int score; int index; Score operator (int rhs) { this-score rhs; return *this; } Score operator (int rhs) { this-score rhs; return *this; } bool operator (const Score that) { return this-score that.score; } }; Score maxScore { 0, 0 }; for (int i 0; i count; i) { SkFontStyle current; this-getStyle(i, current, nullptr); Score currentScore { 0, i }; // CSS weight / SkFontStyle::Weight // The closer to the target weight, the higher the score. // 1000 is the heaviest recognized weight if (pattern.weight() current.weight()) { currentScore 1000; // less than 400 prefer lighter weights } else if (pattern.weight() 400) { if (current.weight() pattern.weight()) { currentScore 1000 - pattern.weight() current.weight(); } else { currentScore 1000 - current.weight(); } // between 400 and 500 prefer heavier up to 500, then lighter weights } else if (pattern.weight() 500) { if (current.weight() pattern.weight() current.weight() 500) { currentScore 1000 pattern.weight() - current.weight(); } else if (current.weight() pattern.weight()) { currentScore 500 current.weight(); } else { currentScore 1000 - current.weight(); } // greater than 500 prefer heavier weights } else if (pattern.weight() 500) { if (current.weight() pattern.weight()) { currentScore 1000 pattern.weight() - current.weight(); } else { currentScore current.weight(); } } if (maxScore currentScore) { maxScore currentScore; } } return this-createTypeface(maxScore.index); }这个时候会按照 CSS Fonts Module Level 3 规范中的规则去匹配字重匹配算法遵循标准评分系统 为每种字体样式计算一个匹配得分字重匹配 根据目标字重与当前字体字重的差异计算得分按照上面的逻辑来看如果某个字体只有一种字重的话应该是不管设置多大的字重都只会使用自带的字重比如 thin 的字重是 300应该是将字重设置为 100 到 900它都只会使用 300 的字重。我们搞个 demo 实践一下从 demo 中发现 2 个问题如果设置的字重比它自身的字重小的话以字体的实际字重兜底。举例thin 是 300 的字重你给它设置 100 的时候它也是 300。当大到一定程度它会自动加粗但加粗的效果好像不如真实的。举例thin 虽然只有 300 的字重但是从 600 开始变得粗了但它这儿的 600 不如真实的 600 字重粗。问题为什么会造成这样的情况呢skia/modules/skparagraph/src/OneLineShaper.cpp在 matchResolvedFonts 的调用中我们发现这样一段逻辑// Apply fake bold and/or italic settings to the font if the typefaces attributes do not match the intended font style. int wantedWeight block.fStyle.getFontStyle().weight(); bool fakeBold wantedWeight 600 wantedWeight - font.getTypeface()-fontStyle().weight() 200; font.setEmbolden(fakeBold);判断逻辑为目标字重大于等于 600并且与找到的字重相差大于等于200才会触发模拟加粗否则直接用找到的字重去绘制。 到这儿也就能解释上面的 demo 中为什么会有加粗的效果了。底层是怎样模拟加粗的呢skia/src/ports/SkFontHost_FreeType.cpp #ifndef SK_OUTLINE_EMBOLDEN_DIVISOR #ifdef __ANDROID__ #define SK_OUTLINE_EMBOLDEN_DIVISOR 34 #else #define SK_OUTLINE_EMBOLDEN_DIVISOR 24 #endif #endif // 加粗强度 const FT_Pos strength FT_MulFix(face-units_per_EM, face-size-metrics.y_scale) / SK_OUTLINE_EMBOLDEN_DIVISOR; // 应用合成加粗 return 0 FT_Outline_Embolden(glyph-outline, strength);FT_Pos FreeType 的位置类型通常是 32 位整数使用 16.16 定点数格式高 16 位整数部分低 16 位小数部分face-units_per_EM 字体的设计大小通常固定为 1000 或 2048单位是字体设计单位face-size-metrics.y_scale 当前字体大小的 Y 轴缩放因子将字体设计单位转换为设备像素FT_MulFix(a, b) FreeType 提供的定点数乘法函数SK_OUTLINE_EMBOLDEN_DIVISOR 控制加粗强度的除数根据平台不同为 24 或 34小字体 加粗效果适中避免笔画粘连大字体 加粗效果明显保持视觉一致性跨平台 根据不同平台的显示特性调整强度举例// 示例 112pt 字体units_per_EM 1000y_scale 0.012 strength FT_MulFix(1000, 0.012 * 65536) / 24 (1000 * 786.432) / 24 786432 / 24 32768 (约等于 0.5 像素) // 示例 224pt 字体units_per_EM 1000y_scale 0.024 strength FT_MulFix(1000, 0.024 * 65536) / 24 (1000 * 1572.864) / 24 1572864 / 24 65536 (约等于 1 像素)所以字体越大加粗效果越明显。所以字重的整体匹配逻辑如下四、总结Flutter作为跨平台框架通过统一的字体文本渲染机制确保多端UI一致性。开发人员可通过TextStyle和ThemeData配置字体家族、字重等属性框架默认提供Android的Roboto和iOS的San Francisco作为兜底字体。字体加载流程依赖引擎层的动态管理器、资源管理器等四类管理器按序查找。iOS通过CoreText闭源库获取系统字体例如CupertinoSystemDisplay对应SF Pro Display安卓则基于Roboto主题。对于中文等非默认字体Flutter采用智能回退机制优先匹配主字体失败后依次尝试自定义回退列表和系统回退字体如安卓的Noto Sans CJK。这种字符级匹配策略保障了混合文本如英文中文表情的正确渲染。