逆向分析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-Regular
v12 = (*(__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-Regular
v12 = (*(__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_1EB8F0
return &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 Monocursive(手寫體):如Snell Roundhandfantasy(裝飾體):如Papyrus
除此外,蘋果在UI上下文中,還有幾個擴展的CSSFamily分類:
-
ui-serif:用于 UI 的襯線字體,主要指New York家族 -
ui-sans-serif:用于 UI 的無襯線字體,即San Francisco家族 -
ui-monospace:用于 UI 的等寬字體,即SF Mono。 -
ui-rounded:用于 UI 的圓體字體。如SF Pro Rounded和SF 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之前,會對用戶指定的語言(即CTFontCopyDefaultCascadeListForLanguages的languagePrefList參數)進行處理:
__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
posted on 2025-10-19 14:29 非專業程序員Ping 閱讀(21) 評論(0) 收藏 舉報


浙公網安備 33010602011771號