逆向分析CoreText中的字体级联/Font Fallback机制

news/2025/10/19 14:31:18/文章来源:https://www.cnblogs.com/Con-Tch-LLYF/p/19150834

逆向分析CoreText中的字体级联/Font Fallback机制

完整内容也可以在公众号「非专业程序员Ping」查看

一、引言

本文基于Xcode 16.4,iOS 18.5模拟器分析,不同系统版本可能有区别。

前面我们介绍了自定义文字排版引擎的原理,其中有一个复杂部分是字体Fallback,本文将通过逆向手段分析CoreText中CTFontCopyDefaultCascadeListForLanguages的实现,通过了解系统的字体回退实现,可以帮助我们实现更好的生产级别的文字排版引擎。

在开始之前,先介绍下CTFontCopyDefaultCascadeListForLanguages API,其完整的函数签名如下:

官方文档:https://developer.apple.com/documentation/coretext/ctfontcopydefaultcascadelistforlanguages(:😃

func CTFontCopyDefaultCascadeListForLanguages(_ font: CTFont,_ languagePrefList: CFArray?
) -> CFArray?

一个字体不可能支持所有的Unicode,比如Helvetica不支持中文,PingFang不支持韩文,在实际渲染时,往往是多个字体共同参与完成的,另外不同字体支持的Unicode有交集,那最终选择哪个字体也是有优先级的;CTFontCopyDefaultCascadeListForLanguages的作用就是:给定一个字体和语言列表,返回系统默认的Fallback列表(也叫级联列表,CascadeList),简单理解就是系统会按这个Fallabck列表进行优先级选择Fallback字体。

在macOS/iOS中,我们也可以通过kCTFontCascadeListAttribute显示指定Fallback链(如下),这样就能自定义Fallback,当然,如果不指定的话会系统也会启用默认Fallback,来尽量保证文本渲染正确。

func makeAttributedStringWithFallback(text: String,baseFontName: String = "Helvetica",size: CGFloat = 16,languages: [String] = ["zh-Hans", "ja", "ko"]
) -> NSAttributedString {let baseFont = CTFontCreateWithName(baseFontName as CFString, size, nil)let fallbacks = CTFontCopyDefaultCascadeListForLanguages(baseFont, languages as CFArray)as? [CTFontDescriptor] ?? []var attributes: [CFString: Any] = [kCTFontNameAttribute: baseFontName,kCTFontSizeAttribute: size]// 可以在这里修改fallbacks,来自定义回退if !fallbacks.isEmpty {attributes[kCTFontCascadeListAttribute] = fallbacks}let newDescriptor = CTFontDescriptorCreateWithAttributes(attributes as CFDictionary)let finalFont = CTFontCreateWithFontDescriptor(newDescriptor, size, nil)let attributesDict: [NSAttributedString.Key: Any] = [.font: finalFont]return NSAttributedString(string: text, attributes: attributesDict)
}

下面,我们按如下调用Demo来实际研究下:

let ctFont = UIFont.systemFont(ofSize: 16)
let languages: [String] = ["zh-Hans"]
let cascadeList = CTFontCopyDefaultCascadeListForLanguages(ctFont, languages as CFArray)

二、调用链路

在这里插入图片描述

如上是CTFontCopyDefaultCascadeListForLanguages的调用链路,可以看出大致分为两条处理链路:

  • Preset Fallbacks:系统预设Fallback,这是一个“快速通道”,系统内部维护了一个针对特定字体(如系统UI字体)的硬编码Fallback列表,如果请求的主字体在这个预设列表中,系统会直接使用这个列表,速度非常快。
  • System Default Fallbacks:系统默认Fallback,这是一个“通用通道”,如果预设列表没有命中,系统会启动默认Fallback流程,该流程会加载一个全局的、定义了完整回退规则的配置文件,根据用户的语言偏好设置,动态地为请求的字体生成一个Fallback列表,并进行缓存以提高后续调用效率。

后文我们也将按这两个流程分开分析。

完整的反汇编逻辑和注释可以参考:https://github.com/HusterYP/FontFallback

三、TBaseFont::CreateFallbacks

/**
* 核心分发函数,决定是使用预设Fallback还是系统默认Fallback。
*
* @param result@<X0> (TBaseFont*) TBaseFont 实例。
* @param a2@<X1>     (int) 标志位,可能表示是否为系统UI字体。
* @param a3@<X2>     (int) 字体属性。
* @param a4@<X3>     (_QWORD*) 未知参数,可能是字符集。
* @param a5@<X4>     (CFArrayRef) 语言列表。
* @param a6@<X8>     (_QWORD*) 用于接收结果的输出指针。
*
* @return __int64 无实际意义。*/
__int64 __usercall TBaseFont::CreateFallbacks@<X0>(__int64 result@<X0>, __int64 a2@<X1>, __int64 a3@<X2>, __int64 a4@<X3>, __int64 a5@<X4>, _QWORD *a6@<X8>)
{...// 保存参数v6 = a3;  // 字体特性标志v7 = a5;  // 语言数组指针v8 = a2;  // 系统UI字体标志v9 = (TBaseFont *)result;  // 基础字体对象...// 如果系统UI字体标志不为 0,尝试创建预设字体回退if ( (_DWORD)a2 ){v11 = (_QWORD *)a4;// 从字体对象中获取字体名,如.SFUI-Regularv12 = (*(__int64 (**)(void))(*(_QWORD *)result + 560LL))();if ( v12 ){v13 = v12;// 初始化字体描述符源对象TDescriptorSource::TDescriptorSource((TDescriptorSource *)&v33);_X26 = &v34;// 创建预设字体回退列表_X0 = TDescriptorSource::CreatePresetFallbacks(v13, v11, v7, v6, &v34);...}}// 检查预设字体回退是否成功创建v24 = objc_retain(_X0);if ( v24 ){v25 = v24;v26 = CFArrayGetCount(v24);result = objc_release(v25);// 如果预设字体回退不为空,直接返回if ( v26 )return result;}...// 如果预设字体回退为空,创建系统默认字体回退v27 = TBaseFont::GetCSSFamily(v9);_X23 = &v34;// 创建系统默认字体回退列表_X0 = TBaseFont::CreateSystemDefaultFallbacks((__int64)v9, v27, v7, v8, &v34);...return result;
}

这是处理预设Fallback和默认Fallback的入口函数。

1)result@<X0>参数是什么

首先我们主要关注的是第一个入参result@<X0>,我们先尝试反汇编x0,发现它其实指向的是类 TTenuousComponentFont (CoreText 内部的一个私有类,继承自 TBaseFont)的虚函数表,如下,下面的udf 其实是因为LLDB尝试将数据当代码解读,但其实它是一个指针表,所以识别成了未定义。

在这里插入图片描述

CoreText 是由 C++ 和 Objective-C 混合实现的,C++类对象的方法调用是通过虚函数表(vtable)实现的,C++ 虚表是一个函数指针数组,对象里保存着一个 vptr(虚表指针),指向它所属类的 vtable。

下面我们尝试将result@<X0>按虚表指针解析,主要是dis -c 5 -s xxx,可以通过这种方式索引各方法。

在这里插入图片描述

继续往上追溯,result@<X0>其实来自原始入参CTFont中的一个属性。

2)什么情况下会触发Preset Fallbacks

提取主要控制逻辑如下:

// 如果系统UI字体标志不为 0,尝试创建预设字体回退
if ( (_DWORD)a2 )
{v11 = (_QWORD *)a4;// 从字体对象中获取字体名,如.SFUI-Regularv12 = (*(__int64 (**)(void))(*(_QWORD *)result + 560LL))();if ( v12 ){...}
}

可以发现当a2非0时会触发Preset Fallbacks,继续往上追溯a2来自于TFont::IsSystemUIFontAndForShaping((TFont *)v5, &v14)IsSystemUIFontAndForShaping不在本文重点,简单理解就是如果是系统UI字体且用于文本塑形的字体则返回true,比如典型的UIFont.systemFont.SFUI-Regular:San Francisco (SF)字体家族中的字体)判定为true。

Q:为什么只有系统UI字体才有预设Fallback

简单理解就是只有系统UI字体是系统完全可控可感知的,所以可以提前构建Fallback列表

3)什么情况下会触发System Default Fallbacks

从上面反汇编逻辑比较容易看出,当Preset Fallbacks的结果为空时,会继续走System Default Fallbacks兜底。

四、Preset Fallbacks

4.1 获取全局预设Fallback列表CTPresetFallbacks

在分析系统是如何为特定字体构建预设Fallback(字体的级联列表)之前,我们需要先知道预设列表是从哪里读取的。

系统是通过GetCTPresetFallbacksDictionary获取预设列表的,继续往下追溯预设列表最终来自GSFontCacheGetData

/** 函数: GSFontCacheGetData* -------------------------* @brief  从图形服务(GraphicsServices)的字体缓存中根据键名获取数据。* @param  a1 (void*)      String入参,实际是对应plist名称,比如预设列表的plist名称CTPresetFallbacks.plist* @param  a2 (const char*) 在此反汇编中未使用,可能是寄存器传参的残留。* @return (void*)         返回一个指向缓存数据的指针,如果找不到则可能返回NULL。*/
void *__fastcall GSFontCacheGetData(void *a1, const char *a2)
{// =================================================================// 快速通道 1: 检查是否请求 "DefaultFontFallbacks.plist"// =================================================================// 调用 a1 的 isEqualToString: 方法,与字符串 "DefaultFontFallbacks.plist"(stru_6BEB8)比较if ( (unsigned int)objc_msgSend_isEqualToString_(a1, a2, &stru_6BEB8) ){// 如果是,直接返回全局变量 kDefaultFontFallbacks 的值。// 这是一个非常高效的硬编码路径,用于获取默认的后备字体规则。v4 = &kDefaultFontFallbacks;return (void *)*v4;}// =================================================================// 快速通道 2: 检查是否请求 "CTPresetFallbacks.plist"// =================================================================// 调用 a1 的 isEqualToString: 方法,与字符串 "CTPresetFallbacks.plist"(stru_6BED8)比较if ( (unsigned int)objc_msgSend_isEqualToString_(v2, v3, &stru_6BED8) ){// 如果是,直接返回全局变量 CTPresetFallbacks 的值。// 这正是我们之前分析的、包含了所有预设后备规则的那个.plist文件的内容。// 系统通过这个键来加载整个预设后备字典。v4 = &CTPresetFallbacks;return (void *)*v4;}// =================================================================// 快速通道 3: 检查是否请求某个特殊字典// =================================================================// 调用 a1 的 isEqualToString: 方法,与字符串 "CTFontInfo.plist"(stru_6BEF8)比较if ( !((unsigned __int64)objc_msgSend_isEqualToString_(v2, v5, &stru_6BEF8) & 1) ){// 如果键不是 stru_6BEF8,则进入下面的常规查询逻辑// =================================================================// 常规查询路径: 在一个全局字典 (unk_1EB8F0) 中查找// =================================================================// 检查键是否为 "CTCharacterSets.plist" (stru_6BF18)if ( (unsigned int)objc_msgSend_isEqualToString_(v2, v7, &stru_6BF18) ){// **键名转换/别名**: 如果是,则将要查询的键替换为另一个字符串 "CTCharacterSets" (stru_6BF38)v9 = &stru_6BF38;}// 检查键是否为 "GSFontCache.plist" (stru_6BF58)else if ( (unsigned int)objc_msgSend_isEqualToString_(v2, v8, &stru_6BF58) ){// **键名转换/别名**: 如果是,则将要查询的键替换为另一个字符串 "GSFontCache" (stru_6BF78)v9 = &stru_6BF78;}else{// 检查键是否为 "CoreTextConfig.plist" (stru_6BF98)if ( !(unsigned int)objc_msgSend_isEqualToString_(v2, v8, &stru_6BF98) )// 如果键不匹配上面任何一个需要转换的键,则使用原始的键 v2 在全局字典中查找return objc_msgSend_objectForKey_(&unk_1EB8F0, v8, v2);// **键名转换/别名**: 如果键是 stru_6BF98,则将其替换为 "CoreTextConfig" (stru_6BFB8)v9 = &stru_6BFB8;}// 对于所有经过“键名转换”的情况,使用转换后的新键 v9 在全局字典中查找// objectForKeyedSubscript: 是 OC 中字典下标语法 (dictionary[key]) 的底层实现return objc_msgSend_objectForKeyedSubscript_(&unk_1EB8F0, v8, v9);}// 如果快速通道3的检查为真 (键等于 stru_6BEF8),则直接返回整个全局字典 unk_1EB8F0return &unk_1EB8F0;
}

从反汇编逻辑不太容易看,可以结合LLDB Debug一起分析:

在这里插入图片描述

在查询预设列表时,入参是CTPresetFallbacks.plist,系统会从全局变量CTPresetFallbacks中读取预设列表,CTPresetFallbacks是全局共享的,是在CoreText服务启动时构建的一个全局常量,内容如下:

完整列表见:https://github.com/HusterYP/FontFallback/blob/main/CTPresetFallbacks.plist

{...".SFUI-Regular" =     (".AppleSystemFallback-Regular",".AppleColorEmojiUI",".SFGeorgian-Regular",HelveticaNeue,".AppleSymbolsFB",{ar = ".AppleArabicFont-Regular"; // 如果系统语言是阿拉伯语(ar),则使用此字体ur = ".AppleUrduFont-Regular"; // 如果是乌尔都语(ur),则使用此字体},{ja = ".AppleJapaneseFont-Regular"; // 如果是日语(ja)ko = ".AppleKoreanFont-Regular"; // 如果是韩语(ko)my = "NotoSansMyanmar-Regular";"my-Qaag" = "NotoSansZawgyi-Regular";"zh-HK" = ".AppleHongKongChineseFont-Regular"; // 香港繁体中文"zh-Hans" = ".AppleSimplifiedChineseFont-Regular"; // 简体中文"zh-Hant" = ".AppleTraditionalChineseFont-Regular"; // 台湾繁体中文"zh-MO" = ".AppleMacaoChineseFont-Regular";},".ThonburiUI-Regular",".SFHebrew-Regular",".SFArmenian-Regular",".AppleIndicFont-Regular","KohinoorDevanagari-Regular",Kailasa,"KohinoorBangla-Regular","KohinoorGujarati-Regular","MuktaMahee-Regular","NotoSansKannada-Regular",KhmerSangamMN,LaoSangamMN,MalayalamSangamMN,NotoSansOriya,SinhalaSangamMN,TamilSangamMN,"KohinoorTelugu-Regular","NotoSansArmenian-Regular",EuphemiaUCAS,"Menlo-Regular",AppleSymbols,ArialMT,"STIXTwoMath-Regular",".HiraKakuInterface-W4",HelveticaNeue,"Kefa-Regular",Galvji,".PhoneFallback");SystemWideFallbacks =     ((128,887,"Charter-Roman"),(895,895,"DINCondensed-Bold"),(975,1315,"Charter-Roman"),(1316,1319,".SFUI-Regular"),...)
}

CTPresetFallbacks.plist中主要定义了两组内容:

1)为特定字体定义Fallback列表/级联列表

比如我们这里要查询.SFUI-Regular的Fallback列表,就用.SFUI-Regular作为key去CTPresetFallbacks.plist中找到一组字典进行解析,解析逻辑后面会讲。

2)SystemWideFallbacks

SystemWideFallbacks定义了一个全局级别的 Fallback 映射,和字体无关,按 Unicode code point 范围定义;每个元素是一个三元组,包括:起始 Unicode 码点 + 结束 Unicode 码点 + 指定 Fallback 字体。

比如128~887范围优先用Charter-Roman。

4.2 预设列表解析流程

获取到全局预设列表之后,我们再来看系统是如何针对特定字体(系统的UI字体)构建级联列表的,主要逻辑在CreatePresetFallbacks中,如下:

/*
* 实现“快速通道”,从一个全局的、硬编码的字典中查找并创建预设列表。
*
* @param a1@<X1> (CFStringRef) 字体名称或标识符。
* @param a2@<X2> (_QWORD*)     输出参数,可能用于字符集。
* @param a3@<X3> (CFArrayRef)  语言列表。
* @param a4@<X4> (int)         标志位。
* @param a5@<X8> (_QWORD*)     用于接收结果的输出指针。
*
* @return __int64 返回创建的预设列表 (CFArrayRef)。
*/
__int64 __usercall TDescriptorSource::CreatePresetFallbacks@<X0>(__int64 a1@<X1>, _QWORD *a2@<X2>, __int64 a3@<X3>, __int64 a4@<X4>, _QWORD *a5@<X8>)
{..._X19 = a5;// 1. 获取全局预设字典result = GetCTPresetFallbacksDictionary();v11 = result;// 2. 创建有序的语言列表v12 = CreateOrderedLanguages(v6);// 3. 使用字体名 a1 在预设字典中查找v13 = CFDictionaryGetValue(v11, v8);// 4. 如果找到匹配项,并且它是一个数组,则开始处理if ( v13 && (v15 = v13, v16 = CFGetTypeID(v13), v16 == CFArrayGetTypeID()) ){// 创建一个可变数组用于存放结果v37 = CFArrayCreateMutable(*(_QWORD *)kCFAllocatorDefault_ptr, 0LL, kCFTypeArrayCallBacks_ptr);v17 = CFArrayGetCount(v15);if ( v17 ){// 5. 遍历预设数组中的每一项do{v20 = (__CFString *)CFArrayGetValueAtIndex(v15, v19);v21 = CFGetTypeID(v20);// 5a. 如果是字典类型,说明是按语言区分的后备字体if ( v21 == CFDictionaryGetTypeID() ){// 遍历上面构建的语言列表,在字典中查找匹配的后备字体do{v25 = CFArrayGetValueAtIndex(v12, v24);if ( v20 ){v26 = CFDictionaryGetValue(v20, v25);if ( v26 )TDescriptorSource::AppendFontDescriptorFromName(&v37, v26, 1024LL);}}while ( v23 != v24 );}// 5b. 如果是字符串类型,直接作为后备字体名else{// ... 对Emoji等特殊字体进行处理 ...TDescriptorSource::AppendFontDescriptorFromName(&v37, v20, 1024LL);}++v19;}while ( v19 != v18 );}}// 将最终结果写入输出指针并返回...
}

代码注释已经比较清晰,总结下来解析流程是:

1)通过字体名从全局预设列表中查询Fallback数组

比如我们通过.SFUI-Regular查询到的原始Fallback数组如下:

".SFUI-Regular" =     (".AppleSystemFallback-Regular",".AppleColorEmojiUI",".SFGeorgian-Regular",HelveticaNeue,".AppleSymbolsFB",{ar = ".AppleArabicFont-Regular"; // 如果系统语言是阿拉伯语(ar),则使用此字体ur = ".AppleUrduFont-Regular"; // 如果是乌尔都语(ur),则使用此字体},{ja = ".AppleJapaneseFont-Regular"; // 如果是日语(ja)ko = ".AppleKoreanFont-Regular"; // 如果是韩语(ko)my = "NotoSansMyanmar-Regular";"my-Qaag" = "NotoSansZawgyi-Regular";"zh-HK" = ".AppleHongKongChineseFont-Regular"; // 香港繁体中文"zh-Hans" = ".AppleSimplifiedChineseFont-Regular"; // 简体中文"zh-Hant" = ".AppleTraditionalChineseFont-Regular"; // 台湾繁体中文"zh-MO" = ".AppleMacaoChineseFont-Regular";},...
)

2)遍历Fallback数组,如果是字典类型,需要按语言区分Fallback字体

还记得最初CTFontCopyDefaultCascadeListForLanguages的函数签名中,第二个参数支持传语言列表:

func CTFontCopyDefaultCascadeListForLanguages(_ font: CTFont,_ languagePrefList: CFArray?
) -> CFArray?

系统会通过CreateOrderedLanguages创建一个有序的语言数组,具体做法是将调用者想要的语言(languagePrefList)、App自身想要的语言、以及用户在整个系统中设置的语言偏好合并成一个有序的语言数组。

然后遍历语言数组,从字典中筛选出对应语言的Fallback字体添加到结果中。

从这里可以看出,同一字体的Fallback列表,还会受语言影响,比如:

zh-Hans zh-HK
在这里插入图片描述 在这里插入图片描述

Q:为什么Fallback字体还跟语言设置相关?

参考自定义文字排版引擎的原理一文中针对「相同Script的字符如果使用了不同的Font,会有什么问题」的回答

3)遍历Fallback数组,如果是字符串类型,「直接」作为Fallback字体

「直接」加引号,因为还会处理Emoji字体等特殊情况。

4)Fallback数组遍历完成之后,构建完成该字体最终的预设Fallabck列表/级联列表

4.2 Preset Fallbacks小结

总结下Preset Fallbacks流程:

1)系统从全局常量CTPresetFallbacks中读取预设列表

2)根据用户指定主字体名从全局预设列表中查询Fallback数组

3)遍历Fallback数组,如果为字典类型,根据用户指定语言、App偏好语言、系统设置偏好语言来选择Fallback字体

4)遍历Fallback数组,如果为字符串类型,「直接」作为Fallback字体

5)Fallback数组遍历完后,对应字体的级联列表构建完成

五、System Default Fallbacks

如果系统预设Fallback没有查到结果,则会兜底到系统默认Fallback逻辑,为字体动态构建级联列表。

5.1 CSSFamily分类

__int64 __usercall TBaseFont::CreateFallbacks@<X0>(__int64 result@<X0>, __int64 a2@<X1>, __int64 a3@<X2>, __int64 a4@<X3>, __int64 a5@<X4>, _QWORD *a6@<X8>)
{...// 如果预设字体回退为空,创建系统默认字体回退v27 = TBaseFont::GetCSSFamily(v9);_X23 = &v34;// 创建系统默认字体回退列表_X0 = TBaseFont::CreateSystemDefaultFallbacks((__int64)v9, v27, v7, v8, &v34);...return result;
}

系统默认Fallback,会先通过TBaseFont::GetCSSFamily将用户指定主字体分类,这是后续查表的关键;GetCSSFamily会读取字体特征进行分类,主要分为:

  • sans-serif (无衬线体):字体笔画的末端没有额外的装饰性“脚”,如Helvetica、Arial、San Francisco (SF Pro)、PingFang SC (苹方)
  • serif (衬线体):字体笔画的末端有装饰性的“脚”(衬线),如Times New Roman、Georgia、New York、宋体
  • monospace (等宽体):所有字符占据相同的宽度,如Menlo、Courier、Monaco、SF Mono
  • cursive (手写体):如Snell Roundhand
  • fantasy (装饰体):如Papyrus

除此外,苹果在UI上下文中,还有几个扩展的CSSFamily分类:

  • ui-serif:用于 UI 的衬线字体,主要指 New York 家族

  • ui-sans-serif:用于 UI 的无衬线字体,即 San Francisco 家族

  • ui-monospace:用于 UI 的等宽字体,即 SF Mono

  • ui-rounded:用于 UI 的圆体字体。如 SF Pro RoundedSF Compact Rounded

5.2 获取系统默认Fallback列表kDefaultFontFallbacks

和全局预设列表一样,系统默认Fallback列表也是通过GSFontCacheGetData读取配置文件。

调用链路是:CreateSystemDefaultFallbacks -> CopyDefaultSubstitutionListForLanguages -> CopyFontFallbacksForLanguages -> CopyFontFallbacks -> CopyDefaultFontFallbacks -> GSFontCacheGetData;通过GSFontCacheGetData读取系统默认Fallback列表时,入参是DefaultFontFallbacks.plist

在这里插入图片描述

也是从一个全局常量kDefaultFontFallbacks中获取的,内容如下:

{common =     (...);cursive =     (...);default =     (...);fantasy =     (...);monospace =     (...);"sans-serif" =     (Helvetica,AppleColorEmoji,".AppleSymbolsFB",{ar = GeezaPro;ja = "HiraginoSans-W3";ko = "AppleSDGothicNeo-Regular";my = "NotoSansMyanmar-Regular";"my-Qaag" = "NotoSansZawgyi-Regular";ur = NotoNastaliqUrdu;"zh-HK" = "PingFangHK-Regular";"zh-Hans" = "PingFangSC-Regular";"zh-Hant" = "PingFangTC-Regular";"zh-MO" = "PingFangMO-Regular";},Thonburi,ArialHebrew);serif =     (...);"ui-monospace" =     (...);"ui-rounded" =     (...);"ui-serif" =     (...);
}

DefaultFontFallbacks.plist的格式基本和CTPresetFallbacks.plist类似,也是KV结构,Value部分也分为字符串和字典类型,字典类型也会根据用户指定语言来择优选取。

5.3 解析并缓存系统默认Fallback列表

解析和缓存逻辑主要由CopyFontFallbacks处理,主逻辑如下:

/*** CoreText 字体回退 - 复制字体回退列表函数* 功能: 根据字体描述符和语言信息复制相应的字体回退列表* * 参数:*   a1 (_QWORD *): 输出参数指针,用于接收生成的字体回退数组*   a2 (__int64): 字体描述符对象指针*   a3 (__CFString *): 主要语言代码字符串*   a4 (__CFString *): 次要语言代码字符串(可选)*   a5 (__int64): 语言数组指针(可选)* * 返回值:*   __int64: 操作结果*/
__int64 __fastcall TFontFallbacks::CopyFontFallbacks(_QWORD *a1, __int64 a2, __CFString *a3, __CFString *a4, __int64 a5)
{...// 保存参数到局部变量和寄存器_X22 = a5;  // 语言数组指针v6 = a4;    // 次要语言代码v7 = a3;    // 主要语言代码v8 = a2;    // 字体描述符对象v9 = a1;    // 输出参数指针// 先在Font实例成员变量字典中查找Fallback缓存v16 = CFDictionaryGetValue(_X0, a3);...// 如果没有找到缓存,则动态构建if ( !_X9 ){...// 获取系统默认Fallback列表CopyDefaultFontFallbacks();v22 = objc_retain(_X0);if ( v22 ){// 用cssfamliy从系统默认Fallback列表中查找映射v24 = CFDictionaryGetValue(v22, v6);      // 检查是否找到了有效的字体列表if ( v24 && CFArrayGetCount(v24) >= 1 ){...// 解析列表// 根据用户指定语言、App偏好语言、系统设置偏好语言创建有序语言数组v29 = CreateOrderedLanguages(_X22);// 处理字体回退列表TDescriptorSource::ProcessFallbackList(v24, (__int64)&v59, v31, v29);// 解析通用(common)字体回退列表v34 = CFDictionaryGetValue(_X25, &stru_1F69C8);TDescriptorSource::ProcessFallbackList(v36, (__int64)&v59, v31, v29);// 缓存结果到Font实例v44 = objc_retain(_X0);if ( v44 ){...CFDictionarySetValue(_X0, v7, _X2);}}}// 处理特定语言的回退逻辑...return objc_release(v57);
}

注意CopyFontFallbacks中一共调了两次ProcessFallbackList,逻辑是先取对应CSSFamily的(比如sans-serif)Fallback列表,再取common的Fallback列表,最终将二者合并起来作为对应字体的Fallback结果。

ProcessFallbackList解析字体列表的逻辑和预设Fallback类似,也是根据Value是字符串类型还是字典类型来区分解析,此处不再赘述。

最后,CopyFontFallbacks还会将Fallback结果缓存到Font实例的字典变量中,key是cssfamily + languages(逗号分隔开),比如:sans-serif,zh-HK

在这里插入图片描述

CopyFontFallbacks逻辑比较清晰,总结下来是:

1)先从Font实例中获取Fallback缓存,如果已经构建过则直接使用

2)缓存获取失败,走动态构建,将对应CSSFamily的Fallback列表和common的Fallback列表合并成最终Fallback结果

3)缓存Fallback结果到Font实例,key是cssfamily + languages

5.4 语言处理与线程安全

CopyFontFallbacksForLanguages在调用CopyFontFallbacks之前,会对用户指定的语言(即CTFontCopyDefaultCascadeListForLanguageslanguagePrefList参数)进行处理:

__int64 __usercall TFontFallbacks::CopyFontFallbacksForLanguages@<X0>(__int64 a1@<X0>, __int64 a2@<X1>, __int64 a3@<X2>, __int64 a4@<X8>)
{// 如果没有提供语言数组,直接调用单语言版本if ( !a3 )return TFontFallbacks::CopyFontFallbacks((_QWORD *)a4, a1, (__CFString *)a2, 0LL, 0LL);...// 获取系统有序语言数组v7 = GetOrderedLanguages;// 遍历输入的语言代码数组do{// 检查规范化后的语言代码是否在系统支持的语言列表中__asm { LDAPR           X3, [X22], [X22] }if ( (unsigned int)CFArrayContainsValue(v7, 0LL, v8, _X3) ){// 如果支持,添加到有效语言数组中CFArrayAppendValue(v6, v21);}++v12;}while ( v11 != v12 );...// 如果找到了有效的语言代码if ( CFArrayGetCount(v6) ){TFontFallbacks::CopyFontFallbacks(v24, v25, _X2, v4, v6);}else{// 如果没有找到有效语言,使用单语言版本TFontFallbacks::CopyFontFallbacks(v24, v25, v4, 0LL, 0LL);}...
}

大致逻辑是:

  • 如果languagePrefList传nil(注意空数组不算nil),则直接用cssfamily查询CopyFontFallbacks

  • 如果languagePrefList不为nil,会将用户指定的languages通过GetOrderedLanguages过滤一遍,去除系统不支持的language,然后使用cssfamily + languages查询CopyFontFallbacks

另外,CopyFontFallbacks会有对字典的读写操作,为了线程安全,CopyDefaultSubstitutionListForLanguages会对整个流程加一把大锁:

__int64 __usercall TDescriptorSource::CopyDefaultSubstitutionListForLanguages@<X0>(__int64 a1@<X0>, __int64 a2@<X1>, __int64 a3@<X8>)
{TDescriptorSource *v6; // 锁对象指针// 这个锁确保字体回退缓存的线程安全访问v6 = (TDescriptorSource *)os_unfair_lock_lock_with_options(&TDescriptorSource::sFontFallbacksLock, 327680LL);...TFontFallbacks::CopyFontFallbacksForLanguages(TDescriptorSource::sFontFallbacksCache, v4, v3, v5);// 释放字体回退缓存锁并返回return os_unfair_lock_unlock(&TDescriptorSource::sFontFallbacksLock);
}

5.5 结果处理与返回

最后CreateSystemDefaultFallbacks会对CopyDefaultSubstitutionListForLanguages中获取到的字体描述符进行处理,即排除用户指定字体,防止自己Fallback自己。

六、总结

至此,我们通过逆向的手段梳理完了CTFontCopyDefaultCascadeListForLanguages的完整流程,最后整理下结论如下:

整体分为两个大流程:

1、Preset Fallbacks:预设Fallback

1.1 系统从全局常量CTPresetFallbacks中读取预设列表

1.2 根据用户指定主字体名从全局预设列表中查询Fallback数组

1.3 遍历Fallback数组,如果为字典类型,根据用户指定语言、App偏好语言、系统设置偏好语言来选择Fallback字体

1.4 遍历Fallback数组,如果为字符串类型,「直接」作为Fallback字体

1.5 Fallback数组遍历完后,对应字体的级联列表构建完成

2、System Default Fallbacks:系统默认Fallback

1.1 获取主字体的CSSFamily分类

1.2 从全局常量kDefaultFontFallbacks中读取默认Fallback列表

1.3 用cssfamily + languages从字体实例中获取Fallback缓存,如果已经构建则直接使用

1.4 缓存缺失则动态构建,根据CSSFamily获取对应字体的Fallback列表并解析,获取common类型的Fallback列表并解析,合并二者结果作为最终Fallback结果

1.5 用cssfamily + languages将Fallback结果缓存到Font实例

1.6 处理并返回Fallback结果

更多精彩内容,欢迎关注🌍公众号:非专业程序员Ping

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/940336.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

2025棋牌室加盟推荐麻友社,自主自助模式引领行业新风尚!

2025棋牌室加盟推荐麻友社,自主自助模式引领行业新风尚!当前棋牌室加盟领域的技术挑战随着科技的不断进步和消费者需求的多样化,棋牌室加盟领域面临着诸多技术挑战。传统的棋牌室经营模式已经难以满足现代消费者的需…

计算机硬件-网络

网卡 网卡如下 :想起网卡就想起了零拷贝 , 软件开发中常常遇到 ,例如 MQ 中 ,例如 Nginx 中 . 首先,什么是“零拷贝”? “零拷贝”的目标,顾名思义,就是尽量减少甚至完全避免数据在内存中的不必要的复制次数。 在传…

2025TYPE-C母座优质厂家推荐,创粤科技TID认证高速传输首选!

2025 TYPE-C母座优质厂家推荐,创粤科技TID认证高速传输首选!当前TYPE-C母座技术挑战随着电子设备的不断发展,TYPE-C母座作为连接器的重要组成部分,其需求量和性能要求也在不断提高。当前,TYPE-C母座面临诸多技术挑…

2025年医药冷链运输厂家推荐排行榜,药品/临床样本/CAR-T/蛋白/诊断试剂/生物/血液/细胞/芯片运输,冷藏车/冷藏箱/保温箱/干冰/液氮/温控/国际冷链公司推荐!

2025年医药冷链运输厂家推荐排行榜:药品/临床样本/CAR-T/蛋白/诊断试剂/生物/血液/细胞/芯片运输,冷藏车/冷藏箱/保温箱/干冰/液氮/温控/国际冷链公司推荐随着全球生物医药行业的快速发展,医药冷链运输的需求日益增…

Ubuntu 桌面美化

1.主题安装 1.1.更新 sudo apt update1.2.安装tweak sudo apt install gnome-tweaks1.3.安装firefox插件1.4.安装gnome-shell sudo apt install chrome-gnome-shell -y1.5.安装扩展管理器 sudo apt install gnome-shel…

题解:CF1336E2 Chiori and Doll Picking (hard version)

很牛很牛很牛的题。 题意:给出 \(n\) 个数 \(a_i\),保证 \(a_i<2^m\),问对于 \(i\in[0,m]\),从这 \(n\) 个数取出若干个异或值的 \(1\) 的个数为 \(i\) 的方案数。\(n\le 2\times 10^5,m\le 53\)。 做法: 首先…

MantisBT vs Kanass,开源项目管理工具一文全面对比分析 - 详解

MantisBT vs Kanass,开源项目管理工具一文全面对比分析 - 详解pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Co…

2025工作服厂家推荐:深圳市贵格服饰,专业定制各类高品质工作服!

2025工作服厂家推荐:深圳市贵格服饰,专业定制各类高品质工作服!随着社会的发展和科技的进步,各行各业对工作服的需求越来越高。无论是防静电工作服、劳保工作服,还是国网工作服、餐厅工作服等,都面临着新的技术挑…

flutter 环境搭建

flutter 环境搭建2025-10-19 14:11 qgbo 阅读(0) 评论(0) 收藏 举报1. 先安装 flutter, 湿这个命令可以用。设置 FLUTTER_HOMEexport FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn &&export …

Apollo自动驾驶平台:开源、高性能的自动驾驶解决方案

Apollo是百度推出的开源自动驾驶平台,提供高性能、灵活的架构,加速自动驾驶车辆的开发、测试和部署。支持多种硬件架构,包含完整的模块化解决方案和丰富的开发工具。Apollo自动驾驶平台 Apollo 是百度推出的开源自动…

2025不锈钢清洗钝化液推荐:隆彦商贸环保高效,品质卓越!

2025不锈钢清洗钝化液推荐:环保高效,品质卓越!随着工业技术的不断进步,不锈钢制品在各个领域的应用越来越广泛。然而,不锈钢表面处理过程中的酸洗钝化处理一直是一个技术挑战。本文将深入探讨当前不锈钢清洗钝化液…

2025年陶瓷过滤机厂家推荐排行榜,陶瓷真空过滤机/盘式陶瓷过滤机/矿用陶瓷过滤机/全自动陶瓷过滤机/固液分离设备公司精选

2025年陶瓷过滤机厂家推荐排行榜,陶瓷真空过滤机/盘式陶瓷过滤机/矿用陶瓷过滤机/全自动陶瓷过滤机/固液分离设备公司精选随着工业技术的不断发展,陶瓷过滤机在矿业、化工、制药等多个领域中的应用越来越广泛。为了帮…

2019年机器学习研究奖项获奖名单公布

某中心宣布2019年第二季度和第三季度机器学习研究奖项的13位获奖者,涵盖深度学习框架、自动摘要、医疗健康、多语言处理等多个前沿技术领域的研究项目。2019年第二季度/第三季度机器学习研究奖项获奖者公布 机器学习研…

[Linux] nsswitch.conf: Linux名称服务和切换配置

[Linux] nsswitch.conf: Linux名称服务和切换配置$(".postTitle2").removeClass("postTitle2").addClass("singleposttitle");目录00 概述01 用户和组信息02 网络名称解析03 网络服务与…

2025年棋牌室加盟推荐排行榜,自主棋牌室加盟,自助棋牌室加盟,优选品牌与服务指南

2025年棋牌室加盟推荐排行榜,自主棋牌室加盟,自助棋牌室加盟,优选品牌与服务指南随着娱乐休闲行业的发展,棋牌室作为一种传统的娱乐方式,逐渐受到越来越多消费者的青睐。无论是传统的棋牌室,还是新兴的自主棋牌室…

2025年轻钢龙骨厂家,铝方通厂家,铝单板厂家,石膏板厂家推荐排行榜:专业品质与服务口碑之选!

2025年轻钢龙骨厂家,铝方通厂家,铝单板厂家,石膏板厂家推荐排行榜:专业品质与服务口碑之选!随着建筑行业的快速发展,轻钢龙骨、铝方通、铝单板和石膏板等建筑材料的需求日益增长。选择优质的供应商对于确保工程质量…

013的加密世界权威指南_第一部分

013的加密世界权威指南_第一部分前言 本文档旨在系统性地总结加密世界的核心基础知识,内容源于与“以太坊联合创始人”身份的AI助手的深度问答。旨在为初学者构建一个清晰、准确且全面的知识框架,从区块链的底层结构…

卸载安装JDK

卸载安装JDK卸载JDK删除Java的安装目录 删除Java_HOME 删除path下关于Java目录 java-version安装JDK百度搜索JDK8,找到下载地址同意协议下载电脑对应的版本双击安装JDK记住安装的路径配置环境变量我的电脑-->右键-…

2025年给汤机厂家最新权威推荐榜:诚信价格与卓越性能的完美结合,优质给汤机公司精选

2025年给汤机厂家最新权威推荐榜:诚信价格与卓越性能的完美结合,优质给汤机公司精选在铝镁合金压铸行业持续升级的背景下,给汤机作为压铸自动化生产线中的核心设备,其性能表现直接关系到生产效率和产品质量。随着工…

2025年给汤机厂家最新权威推荐榜:靠谱给汤机源头厂家精选,高效稳定与售后服务深度解析

2025年给汤机厂家最新权威推荐榜:靠谱给汤机源头厂家精选,高效稳定与售后服务深度解析随着全球制造业向智能化、自动化方向加速转型,压铸行业作为现代工业体系的重要组成部分,正迎来新一轮技术革新浪潮。给汤机作为…