引子
前几天刷知乎,刷到字节前端的一篇文章 深入了解魔性的CSS字体,一阵感概,大部分前端工程师的字体知识都是一片空白,现在终于逐渐有人注意到这些问题。我在文章下面进行了评论,尝试回答文章开头提出的问题:
第一个问题。
PingFangSC-Regular
为什么不支持 500 字重呢?因为它就是 Regular 字重(对应 400),自然不存在把 400 字重的字体指定成 500 这种事。PingFangSC-Regular
是字体全称,在 CSS 中应该使用字体族(font family)而不是字体全称(full font name),所以 CSS 的那个属性叫font-family
。第二个问题。为什么有的 Android 手机不支持粗体呢?因为中文字体文件体积太大,所以 Android 的 AOSP 只附带了 Regular 字重的
Noto Sans CJK SC Regular
和祖传的回退字体Droid Sans Fallback
(也只有一款字重)。那为什么有的又支持粗体呢?其一,软件层面实现的伪粗体,其二,小米、华为等厂商在系统中附带了支持多款字重的字体,比如小米兰亭、鸿蒙黑体,甚至用上最新的可变字体,比如小米的小米兰亭Pro VF,允许开发者放飞自我,定义[1, 1000]
之间的任意字重。
总的来说因为这篇文章让我决定补一补几年前写博客时留下的坑。
这里先补 React Native 的坑。
从 React Native 字体示例开始
昨天在 GitHub 上建了一个仓库 https://github.com/keqingrong/font-example 用于验证字体设置在 iOS 和 Android 上是否生效。黑盒验证总是不如白盒验证来得彻底,所以本文直接看 React Native 的源码一探究竟。
React Native 中一般这样设置字体属性:
import { Text } from 'react-native'
// ...
<Text style={{ fontFamily: 'PingFang SC', fontWeight: 'bold' }} >
// ...
所以先看 Text
组件从哪儿来的。
<Text>
的 JS 实现
Text
模块由 react-native
包导出,react-native@0.65.1
的 package.json
没有定义 main
字段,入口直接是根目录下的 index.js
。
// react-native/index.js
import typeof Text from './Libraries/Text/Text';
module.exports = {
// ...
get Text(): Text {
return require('./Libraries/Text/Text');
},
}
有两段涉及 Text
的,第一段是 Flow 类型定义,第二段是引入和导出具体模块(定义成 getter
形式是为了在组件和 API 变更时加迁移提示)。
Libraries
下是传说中的原生模块(Native Modules)和原生组件(Native Components),这里我们只关心 Text
文件夹。
# react-native/Libraries/Text/
.
├── BaseText/
├── RawText/
├── Text/
├── TextInput/
├── VirtualText/
├── RCTConvert+Text.h
├── RCTConvert+Text.m
├── RCTTextAttributes.h
├── RCTTextAttributes.m
├── RCTTextTransform.h
├── React-RCTText.podspec
├── Text.js
├── TextAncestor.js
├── TextInjection.js
├── TextNativeComponent.js
└── TextProps.js
先看 Text.js
,删减后的代码如下:
// react-native/Libraries/Text/Text.js
import TextAncestor from './TextAncestor';
import {NativeText, NativeVirtualText} from './TextNativeComponent';
const Text = () => {
const hasTextAncestor = useContext(TextAncestor);
return hasTextAncestor ? (
<NativeVirtualText />
) : (
<TextAncestor.Provider value={true}>
<NativeText />
</TextAncestor.Provider>
);
}
TextAncestor
直译为“文本祖先”,因为 <Text>
组件可以嵌套,类似:
<View>
<Text testID="parent">
<Text testID="child">Hello</Text>
</Text>
</View>
child
有文本祖先,parent
没有,有的话需要继承祖先元素的文本样式,没有的话自己成为祖先。
再看 TextNativeComponent.js
,删减后的代码如下:
// react-native/Libraries/Text/TextNativeComponent.js
import createReactNativeComponentClass from '../Renderer/shims/createReactNativeComponentClass';
const NativeText = createReactNativeComponentClass('RCTText', () => ({}));
const NativeVirtualText = createReactNativeComponentClass('RCTVirtualText', () => ({}));
通过调用 createReactNativeComponentClass()
创建原生组件类。
react-native
对外的创建原生组件的 APIrequireNativeComponent()
内部也是调用的该方法:// react-native/Libraries/ReactNative/requireNativeComponent.js const requireNativeComponent = (uiViewClassName) => createReactNativeComponentClass( uiViewClassName, () => getNativeComponentAttributes(uiViewClassName) ); module.exports = requireNativeComponent;
createReactNativeComponentClass()
内部又调用了 ReactNativeViewConfigRegistry.register()
,ReactNativeViewConfigRegistry
的代码如下,所谓的注册就是将组件名和获取组件视图配置的回调函数添加到 Map
中。
// react-native/Libraries/Renderer/shims/ReactNativeViewConfigRegistry.js
const viewConfigCallbacks = new Map();
const viewConfigs = new Map();
exports.register = function(name, callback) {
viewConfigCallbacks.set(name, callback);
return name;
};
exports.get = function(name) {
let viewConfig;
if (!viewConfigs.has(name)) {
const callback = viewConfigCallbacks.get(name);
viewConfig = callback();
processEventTypes(viewConfig);
viewConfigs.set(name, viewConfig);
viewConfigCallbacks.set(name, null);
} else {
viewConfig = viewConfigs.get(name);
}
return viewConfig;
}
React Native 的渲染器(Renderer)在渲染时会为 React 组件创建实例,根据组件类型获取对应视图配置。
// react-native/Libraries/Renderer/implementations/ReactNativeRenderer-dev.js
var ReactNativePrivateInterface = require("react-native/Libraries/ReactPrivate/ReactNativePrivateInterface");
var getViewConfigForType = ReactNativePrivateInterface.ReactNativeViewConfigRegistry.get;
function createInstance(
type,
props,
rootContainerInstance,
hostContext,
internalInstanceHandle
) {
var tag = allocateTag();
var viewConfig = getViewConfigForType(type);
{
for (var key in viewConfig.validAttributes) {
if (props.hasOwnProperty(key)) {
ReactNativePrivateInterface.deepFreezeAndThrowOnMutationInDev(
props[key]
);
}
}
}
var updatePayload = create(props, viewConfig.validAttributes);
ReactNativePrivateInterface.UIManager.createView(
tag, // reactTag
viewConfig.uiViewClassName, // viewName
rootContainerInstance, // rootTag
updatePayload // props
);
var component = new ReactNativeFiberHostComponent(
tag,
viewConfig,
internalInstanceHandle
);
precacheFiberNode(internalInstanceHandle, tag);
updateFiberProps(tag, props); // Not sure how to avoid this cast. Flow is okay if the component is defined
// in the same file but if it's external it can't see the types.
return component;
}
React Native 的渲染器实现都在
Libraries/Renderer
文件夹下,主要包括ReactNativeRenderer
和ReactFabric
。
JS 部分暂时只看到这里,毕竟主线任务是看原生如何处理文字的字体,下面继续看 react-native/Libraries/Text/
目录下的原生代码:
<Text>
的 Native 实现
# react-native/Libraries/Text/
.
├── BaseText/
├── RawText/
├── Text/
├── TextInput/
├── VirtualText/
├── RCTConvert+Text.h
├── RCTConvert+Text.m
├── RCTTextAttributes.h
├── RCTTextAttributes.m
├── RCTTextTransform.h
├── React-RCTText.podspec
RCTTextAttributes.h
和 RCTTextAttributes.m
定义了一个 RCTTextAttributes
类,它的属性和 React Native 文档 Text Style Props 中定义的 props
对应(当然也同样是 <Text>
组件支持的 props
)。
Objective-C 的语法中,
@interface
虽然叫interface
,但实际用于定义类。@protocol
才相当于我们正常理解的接口。Objective-C 中的类除了在头文件中定义@interface
,还需要在源文件中通过@implementation
进行详细实现。 可以参考官方文档 Programming with Objective-C:
RCTTextAttributes.m
本身没有包含复杂的字体设置逻辑,其中使用了位于 react-native/React/Views/
目录的 RCTFont
类(RCTFont.h
、RCTFont.mm
)过于细节暂时忽略。
除了 TextInput
文件夹,其他主要包括如下 View
和相应的 ViewManager
(用于创建、修改、销毁 View
):
RCTTextView
/RCTTextViewManager
RCTTextShadowView
RCTBaseTextShadowView
/RCTBaseTextViewManager
RCTRawTextShadowView
/RCTRawTextViewManager
RCTVirtualTextShadowView
/RCTVirtualTextViewManager
其中
RCTBaseTextShadowView
引用了RCTTextAttributes
。
RCTTextViewManager
管理的就是 <Text>
组件对应的视图 RCTTextView
(对应 TextNativeComponent.js
中的 RCTText
),可以看到其代码中使用了 RCT_EXPORT_MODULE()
、RCT_EXPORT_VIEW_PROPERTY()
等宏来对外导出模块、属性:
// react-native/Libraries/Text/Text/RCTTextViewManager.m
RCT_EXPORT_MODULE(RCTText)
RCT_REMAP_SHADOW_PROPERTY(numberOfLines, maximumNumberOfLines, NSInteger)
RCT_REMAP_SHADOW_PROPERTY(ellipsizeMode, lineBreakMode, NSLineBreakMode)
RCT_REMAP_SHADOW_PROPERTY(adjustsFontSizeToFit, adjustsFontSizeToFit, BOOL)
RCT_REMAP_SHADOW_PROPERTY(minimumFontScale, minimumFontScale, CGFloat)
RCT_EXPORT_SHADOW_PROPERTY(onTextLayout, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(selectable, BOOL)
React Native 的 <Text>
没有直接使用 iOS 中的 UILabel
,而是使用 Apple 提供的 TextKit 来自定义文本内容布局。具体来说包括以下三个类:
NSTextStorage
NSLayoutManager
NSTextContainer
在 RCTTextView
和 RCTTextShadowView
中都有体现。React Native 的 TextLayoutManager
在不同平台有着不同的实现,代码在 react-native/ReactCommon/react/renderer/textlayoutmanager/platform
目录下:
# react-native/ReactCommon/react/renderer/textlayoutmanager/platform
.
├── android
│ └── react
│ └── renderer
│ └── textlayoutmanager
│ ├── TextLayoutManager.cpp
│ └── TextLayoutManager.h
├── cxx
│ ├── TextLayoutManager.cpp
│ └── TextLayoutManager.h
└── ios
├── NSTextStorage+FontScaling.h
├── NSTextStorage+FontScaling.m
├── RCTAttributedTextUtils.h
├── RCTAttributedTextUtils.mm
├── RCTFontProperties.h
├── RCTFontUtils.h
├── RCTFontUtils.mm
├── RCTTextLayoutManager.h
├── RCTTextLayoutManager.mm
├── RCTTextPrimitivesConversions.h
├── TextLayoutManager.h
└── TextLayoutManager.mm
我们主要看 RCTTextLayoutManager.h
和 RCTTextLayoutManager.mm
,通过 @interface
定义的 6 个方法中有 5 个都调用了同一个方法 _nsAttributedStringFromAttributedString()
:
measureAttributedString()
drawAttributedString()
getLinesForAttributedString()
getEventEmitterWithAttributeString()
getRectWithAttributedString()
如官方文档的 Nested text 所说,<Text>
在 iOS 上的实现基于 NSAttributedString
,_nsAttributedStringFromAttributedString()
的方法名正好验证这个说法。
// react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/RCTTextLayoutManager.mm
- (NSAttributedString *)_nsAttributedStringFromAttributedString:(AttributedString)attributedString
{
auto sharedNSAttributedString = _cache.get(attributedString, [](AttributedString attributedString) {
return wrapManagedObject(RCTNSAttributedStringFromAttributedString(attributedString));
});
return unwrapManagedObject(sharedNSAttributedString);
}
_nsAttributedStringFromAttributedString()
内部调用了 RCTAttributedTextUtils.mm
中的 RCTNSAttributedStringFromAttributedString()
,其代码大致如下:
NSAttributedString *RCTNSAttributedStringFromAttributedString(const AttributedString &attributedString)
{
// ...
NSMutableAttributedString *nsAttributedString = [[NSMutableAttributedString alloc] init];
[nsAttributedString beginEditing];
for (auto fragment : attributedString.getFragments()) {
NSMutableAttributedString *nsAttributedStringFragment;
if (fragment.isAttachment()) {
// ...
nsAttributedStringFragment = [[NSMutableAttributedString attributedStringWithAttachment:attachment] mutableCopy];
} else {
NSString *string = [NSString stringWithCString:fragment.string.c_str() encoding:NSUTF8StringEncoding];
nsAttributedStringFragment = [[NSMutableAttributedString alloc]
initWithString:string
attributes:RCTNSTextAttributesFromTextAttributes(fragment.textAttributes)];
}
// ...
[nsAttributedString appendAttributedString:nsAttributedStringFragment];
}
[nsAttributedString endEditing];
return nsAttributedString;
}
通过迭代 AttributedString
来生成 NSAttributedString
,其中出现了上文提及的文本样式属性字段 textAttributes
。RCTNSTextAttributesFromTextAttributes(textAttributes)
的部分代码:
// react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/RCTAttributedTextUtils.mm
NSDictionary<NSAttributedStringKey, id> *RCTNSTextAttributesFromTextAttributes(TextAttributes const &textAttributes)
{
NSMutableDictionary<NSAttributedStringKey, id> *attributes = [NSMutableDictionary dictionaryWithCapacity:10];
// Font
UIFont *font = RCTEffectiveFontFromTextAttributes(textAttributes);
if (font) {
attributes[NSFontAttributeName] = font;
}
// Colors
UIColor *effectiveForegroundColor = RCTEffectiveForegroundColorFromTextAttributes(textAttributes);
// ...
}
RCTEffectiveFontFromTextAttributes(textAttributes)
方法,部分代码如下:
// react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/RCTAttributedTextUtils.mm
inline static UIFont *RCTEffectiveFontFromTextAttributes(const TextAttributes &textAttributes)
{
NSString *fontFamily = [NSString stringWithCString:textAttributes.fontFamily.c_str() encoding:NSUTF8StringEncoding];
RCTFontProperties fontProperties;
fontProperties.family = fontFamily;
fontProperties.size = textAttributes.fontSize;
fontProperties.weight = textAttributes.fontWeight.hasValue()
? RCTUIFontWeightFromInteger((NSInteger)textAttributes.fontWeight.value())
: NAN;
// ...
return RCTFontWithFontProperties(fontProperties);
}
再看 RCTFontWithFontProperties()
方法,代码大致如下:
// react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/RCTFontUtils.mm
UIFont *RCTFontWithFontProperties(RCTFontProperties fontProperties)
{
RCTFontProperties defaultFontProperties = RCTDefaultFontProperties();
fontProperties = RCTResolveFontProperties(fontProperties, defaultFontProperties);
// ...
}
调用静态方法 RCTDefaultFontProperties()
获取默认属性,然后又调用静态方法 RCTResolveFontProperties()
解析传入的 fontProperties
和默认属性。两个方法的代码如下:
// react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/RCTFontUtils.mm
static RCTFontProperties RCTDefaultFontProperties()
{
static RCTFontProperties defaultFontProperties;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
defaultFontProperties.family = [UIFont systemFontOfSize:defaultFontProperties.size].familyName;
defaultFontProperties.size = 14;
defaultFontProperties.weight = UIFontWeightRegular;
defaultFontProperties.style = RCTFontStyleNormal;
defaultFontProperties.variant = RCTFontVariantDefault;
defaultFontProperties.sizeMultiplier = 1.0;
});
return defaultFontProperties;
}
static RCTFontProperties RCTResolveFontProperties(
RCTFontProperties fontProperties,
RCTFontProperties baseFontProperties)
{
fontProperties.family = fontProperties.family.length ? fontProperties.family : baseFontProperties.family;
fontProperties.size = !isnan(fontProperties.size) ? fontProperties.size : baseFontProperties.size;
fontProperties.weight = !isnan(fontProperties.weight) ? fontProperties.weight : baseFontProperties.weight;
fontProperties.style =
fontProperties.style != RCTFontStyleUndefined ? fontProperties.style : baseFontProperties.style;
fontProperties.variant =
fontProperties.variant != RCTFontVariantUndefined ? fontProperties.variant : baseFontProperties.variant;
return fontProperties;
}
没有设置 fontFamily
的情况下展示系统默认字体,fontSize
默认为 14,fontWeight
默认为 UIFontWeightRegular
。
简单总结
React Native 上设置的 fontFamily
等属性最终会直接传递赋给原生组件,和 CSS 截然不同,这也导致了 fontFamily
只能限定某一款字体族名称,无法写成 "PingFang SC", "Noto Sans CJK SC", "Microsoft YaHei", sans-serif
形式。
相关链接
- Text https://reactnative.dev/docs/text
- Text Style Props https://reactnative.dev/docs/text-style-props
- iOS Native Modules https://reactnative.dev/docs/native-modules-ios
- iOS Native UI Components https://reactnative.dev/docs/native-components-ios