一、引言
本文基于Xcode 16.4,iOS 18.5模擬器分析,不同系統(tǒng)版本可能有區(qū)別。
前面我們介紹了自定義文字排版引擎的原理,其中有一個復雜部分是字體Fallback,本文將通過逆向手段分析CoreText中CTFontCopyDefaultCascadeListForLanguages的實現(xiàn),通過了解系統(tǒng)的字體回退實現(xiàn),可以幫助我們實現(xiàn)更好的生產(chǎn)級別的文字排版引擎。
在開始之前,先介紹下CTFontCopyDefaultCascadeListForLanguages API,其完整的函數(shù)簽名如下:
官方文檔:https://developer.apple.com/documentation/coretext/ctfontcopydefaultcascadelistforlanguages(::)
func CTFontCopyDefaultCascadeListForLanguages(
_ font: CTFont,
_ languagePrefList: CFArray?
) -> CFArray?
一個字體不可能支持所有的Unicode,比如Helvetica不支持中文,PingFang不支持韓文,在實際渲染時,往往是多個字體共同參與完成的,另外不同字體支持的Unicode有交集,那最終選擇哪個字體也是有優(yōu)先級的;CTFontCopyDefaultCascadeListForLanguages的作用就是:給定一個字體和語言列表,返回系統(tǒng)默認的Fallback列表(也叫級聯(lián)列表,CascadeList),簡單理解就是系統(tǒng)會按這個Fallabck列表進行優(yōu)先級選擇Fallback字體。
在macOS/iOS中,我們也可以通過kCTFontCascadeListAttribute顯示指定Fallback鏈(如下),這樣就能自定義Fallback,當然,如果不指定的話會系統(tǒng)也會啟用默認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)
}
下面,我們按如下調(diào)用Demo來實際研究下:
let ctFont = UIFont.systemFont(ofSize: 16)
let languages: [String] = ["zh-Hans"]
let cascadeList = CTFontCopyDefaultCascadeListForLanguages(ctFont, languages as CFArray)
二、調(diào)用鏈路

如上是CTFontCopyDefaultCascadeListForLanguages的調(diào)用鏈路,可以看出大致分為兩條處理鏈路:
- Preset Fallbacks:系統(tǒng)預設Fallback,這是一個“快速通道”,系統(tǒng)內(nèi)部維護了一個針對特定字體(如系統(tǒng)UI字體)的硬編碼Fallback列表,如果請求的主字體在這個預設列表中,系統(tǒng)會直接使用這個列表,速度非??臁?/li>
- System Default Fallbacks:系統(tǒng)默認Fallback,這是一個“通用通道”,如果預設列表沒有命中,系統(tǒng)會啟動默認Fallback流程,該流程會加載一個全局的、定義了完整回退規(guī)則的配置文件,根據(jù)用戶的語言偏好設置,動態(tài)地為請求的字體生成一個Fallback列表,并進行緩存以提高后續(xù)調(diào)用效率。
后文我們也將按這兩個流程分開分析。
完整的反匯編邏輯和注釋可以參考:https://github.com/HusterYP/FontFallback
三、TBaseFont::CreateFallbacks
/**
* 核心分發(fā)函數(shù),決定是使用預設Fallback還是系統(tǒng)默認Fallback。
*
* @param result@<X0> (TBaseFont*) TBaseFont 實例。
* @param a2@<X1> (int) 標志位,可能表示是否為系統(tǒng)UI字體。
* @param a3@<X2> (int) 字體屬性。
* @param a4@<X3> (_QWORD*) 未知參數(shù),可能是字符集。
* @param a5@<X4> (CFArrayRef) 語言列表。
* @param a6@<X8> (_QWORD*) 用于接收結(jié)果的輸出指針。
*
* @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>)
{
...
// 保存參數(shù)
v6 = a3; // 字體特性標志
v7 = a5; // 語言數(shù)組指針
v8 = a2; // 系統(tǒng)UI字體標志
v9 = (TBaseFont *)result; // 基礎字體對象
...
// 如果系統(tǒng)UI字體標志不為 0,嘗試創(chuàng)建預設字體回退
if ( (_DWORD)a2 )
{
v11 = (_QWORD *)a4;
// 從字體對象中獲取字體名,如.SFUI-Regular
v12 = (*(__int64 (**)(void))(*(_QWORD *)result + 560LL))();
if ( v12 )
{
v13 = v12;
// 初始化字體描述符源對象
TDescriptorSource::TDescriptorSource((TDescriptorSource *)&v33);
_X26 = &v34;
// 創(chuàng)建預設字體回退列表
_X0 = TDescriptorSource::CreatePresetFallbacks(v13, v11, v7, v6, &v34);
...
}
}
// 檢查預設字體回退是否成功創(chuàng)建
v24 = objc_retain(_X0);
if ( v24 )
{
v25 = v24;
v26 = CFArrayGetCount(v24);
result = objc_release(v25);
// 如果預設字體回退不為空,直接返回
if ( v26 )
return result;
}
...
// 如果預設字體回退為空,創(chuàng)建系統(tǒng)默認字體回退
v27 = TBaseFont::GetCSSFamily(v9);
_X23 = &v34;
// 創(chuàng)建系統(tǒng)默認字體回退列表
_X0 = TBaseFont::CreateSystemDefaultFallbacks((__int64)v9, v27, v7, v8, &v34);
...
return result;
}
這是處理預設Fallback和默認Fallback的入口函數(shù)。
1)result@<X0>參數(shù)是什么
首先我們主要關注的是第一個入?yún)?code>result@<X0>,我們先嘗試反匯編x0,發(fā)現(xiàn)它其實指向的是類 TTenuousComponentFont (CoreText 內(nèi)部的一個私有類,繼承自 TBaseFont)的虛函數(shù)表,如下,下面的udf 其實是因為LLDB嘗試將數(shù)據(jù)當代碼解讀,但其實它是一個指針表,所以識別成了未定義。

CoreText 是由 C++ 和 Objective-C 混合實現(xiàn)的,C++類對象的方法調(diào)用是通過虛函數(shù)表(vtable)實現(xiàn)的,C++ 虛表是一個函數(shù)指針數(shù)組,對象里保存著一個 vptr(虛表指針),指向它所屬類的 vtable。
下面我們嘗試將result@<X0>按虛表指針解析,主要是dis -c 5 -s xxx,可以通過這種方式索引各方法。

繼續(xù)往上追溯,result@<X0>其實來自原始入?yún)TFont中的一個屬性。
2)什么情況下會觸發(fā)Preset Fallbacks
提取主要控制邏輯如下:
// 如果系統(tǒng)UI字體標志不為 0,嘗試創(chuàng)建預設字體回退
if ( (_DWORD)a2 )
{
v11 = (_QWORD *)a4;
// 從字體對象中獲取字體名,如.SFUI-Regular
v12 = (*(__int64 (**)(void))(*(_QWORD *)result + 560LL))();
if ( v12 )
{
...
}
}
可以發(fā)現(xiàn)當a2非0時會觸發(fā)Preset Fallbacks,繼續(xù)往上追溯a2來自于TFont::IsSystemUIFontAndForShaping((TFont *)v5, &v14),IsSystemUIFontAndForShaping不在本文重點,簡單理解就是如果是系統(tǒng)UI字體且用于文本塑形的字體則返回true,比如典型的UIFont.systemFont(.SFUI-Regular:San Francisco (SF)字體家族中的字體)判定為true。
Q:為什么只有系統(tǒng)UI字體才有預設Fallback
簡單理解就是只有系統(tǒng)UI字體是系統(tǒng)完全可控可感知的,所以可以提前構建Fallback列表
3)什么情況下會觸發(fā)System Default Fallbacks
從上面反匯編邏輯比較容易看出,當Preset Fallbacks的結(jié)果為空時,會繼續(xù)走System Default Fallbacks兜底。
四、Preset Fallbacks
4.1 獲取全局預設Fallback列表CTPresetFallbacks
在分析系統(tǒng)是如何為特定字體構建預設Fallback(字體的級聯(lián)列表)之前,我們需要先知道預設列表是從哪里讀取的。
系統(tǒng)是通過GetCTPresetFallbacksDictionary獲取預設列表的,繼續(xù)往下追溯預設列表最終來自GSFontCacheGetData:
/*
* 函數(shù): GSFontCacheGetData
* -------------------------
* @brief 從圖形服務(GraphicsServices)的字體緩存中根據(jù)鍵名獲取數(shù)據(jù)。
* @param a1 (void*) String入?yún)ⅲ瑢嶋H是對應plist名稱,比如預設列表的plist名稱CTPresetFallbacks.plist
* @param a2 (const char*) 在此反匯編中未使用,可能是寄存器傳參的殘留。
* @return (void*) 返回一個指向緩存數(shù)據(jù)的指針,如果找不到則可能返回NULL。
*/
void *__fastcall GSFontCacheGetData(void *a1, const char *a2)
{
// =================================================================
// 快速通道 1: 檢查是否請求 "DefaultFontFallbacks.plist"
// =================================================================
// 調(diào)用 a1 的 isEqualToString: 方法,與字符串 "DefaultFontFallbacks.plist"(stru_6BEB8)比較
if ( (unsigned int)objc_msgSend_isEqualToString_(a1, a2, &stru_6BEB8) )
{
// 如果是,直接返回全局變量 kDefaultFontFallbacks 的值。
// 這是一個非常高效的硬編碼路徑,用于獲取默認的后備字體規(guī)則。
v4 = &kDefaultFontFallbacks;
return (void *)*v4;
}
// =================================================================
// 快速通道 2: 檢查是否請求 "CTPresetFallbacks.plist"
// =================================================================
// 調(diào)用 a1 的 isEqualToString: 方法,與字符串 "CTPresetFallbacks.plist"(stru_6BED8)比較
if ( (unsigned int)objc_msgSend_isEqualToString_(v2, v3, &stru_6BED8) )
{
// 如果是,直接返回全局變量 CTPresetFallbacks 的值。
// 這正是我們之前分析的、包含了所有預設后備規(guī)則的那個.plist文件的內(nèi)容。
// 系統(tǒng)通過這個鍵來加載整個預設后備字典。
v4 = &CTPresetFallbacks;
return (void *)*v4;
}
// =================================================================
// 快速通道 3: 檢查是否請求某個特殊字典
// =================================================================
// 調(diào)用 a1 的 isEqualToString: 方法,與字符串 "CTFontInfo.plist"(stru_6BEF8)比較
if ( !((unsigned __int64)objc_msgSend_isEqualToString_(v2, v5, &stru_6BEF8) & 1) )
{
// 如果鍵不是 stru_6BEF8,則進入下面的常規(guī)查詢邏輯
// =================================================================
// 常規(guī)查詢路徑: 在一個全局字典 (unk_1EB8F0) 中查找
// =================================================================
// 檢查鍵是否為 "CTCharacterSets.plist" (stru_6BF18)
if ( (unsigned int)objc_msgSend_isEqualToString_(v2, v7, &stru_6BF18) )
{
// **鍵名轉(zhuǎn)換/別名**: 如果是,則將要查詢的鍵替換為另一個字符串 "CTCharacterSets" (stru_6BF38)
v9 = &stru_6BF38;
}
// 檢查鍵是否為 "GSFontCache.plist" (stru_6BF58)
else if ( (unsigned int)objc_msgSend_isEqualToString_(v2, v8, &stru_6BF58) )
{
// **鍵名轉(zhuǎn)換/別名**: 如果是,則將要查詢的鍵替換為另一個字符串 "GSFontCache" (stru_6BF78)
v9 = &stru_6BF78;
}
else
{
// 檢查鍵是否為 "CoreTextConfig.plist" (stru_6BF98)
if ( !(unsigned int)objc_msgSend_isEqualToString_(v2, v8, &stru_6BF98) )
// 如果鍵不匹配上面任何一個需要轉(zhuǎn)換的鍵,則使用原始的鍵 v2 在全局字典中查找
return objc_msgSend_objectForKey_(&unk_1EB8F0, v8, v2);
// **鍵名轉(zhuǎn)換/別名**: 如果鍵是 stru_6BF98,則將其替換為 "CoreTextConfig" (stru_6BFB8)
v9 = &stru_6BFB8;
}
// 對于所有經(jīng)過“鍵名轉(zhuǎn)換”的情況,使用轉(zhuǎn)換后的新鍵 v9 在全局字典中查找
// objectForKeyedSubscript: 是 OC 中字典下標語法 (dictionary[key]) 的底層實現(xiàn)
return objc_msgSend_objectForKeyedSubscript_(&unk_1EB8F0, v8, v9);
}
// 如果快速通道3的檢查為真 (鍵等于 stru_6BEF8),則直接返回整個全局字典 unk_1EB8F0
return &unk_1EB8F0;
}
從反匯編邏輯不太容易看,可以結(jié)合LLDB Debug一起分析:

在查詢預設列表時,入?yún)⑹?code>CTPresetFallbacks.plist,系統(tǒng)會從全局變量CTPresetFallbacks中讀取預設列表,CTPresetFallbacks是全局共享的,是在CoreText服務啟動時構建的一個全局常量,內(nèi)容如下:
完整列表見:https://github.com/HusterYP/FontFallback/blob/main/CTPresetFallbacks.plist
{
...
".SFUI-Regular" = (
".AppleSystemFallback-Regular",
".AppleColorEmojiUI",
".SFGeorgian-Regular",
HelveticaNeue,
".AppleSymbolsFB",
{
ar = ".AppleArabicFont-Regular"; // 如果系統(tǒng)語言是阿拉伯語(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中主要定義了兩組內(nèi)容:
1)為特定字體定義Fallback列表/級聯(lián)列表
比如我們這里要查詢.SFUI-Regular的Fallback列表,就用.SFUI-Regular作為key去CTPresetFallbacks.plist中找到一組字典進行解析,解析邏輯后面會講。
2)SystemWideFallbacks
SystemWideFallbacks定義了一個全局級別的 Fallback 映射,和字體無關,按 Unicode code point 范圍定義;每個元素是一個三元組,包括:起始 Unicode 碼點 + 結(jié)束 Unicode 碼點 + 指定 Fallback 字體。
比如128~887范圍優(yōu)先用Charter-Roman。
4.2 預設列表解析流程
獲取到全局預設列表之后,我們再來看系統(tǒng)是如何針對特定字體(系統(tǒng)的UI字體)構建級聯(lián)列表的,主要邏輯在CreatePresetFallbacks中,如下:
/*
* 實現(xiàn)“快速通道”,從一個全局的、硬編碼的字典中查找并創(chuàng)建預設列表。
*
* @param a1@<X1> (CFStringRef) 字體名稱或標識符。
* @param a2@<X2> (_QWORD*) 輸出參數(shù),可能用于字符集。
* @param a3@<X3> (CFArrayRef) 語言列表。
* @param a4@<X4> (int) 標志位。
* @param a5@<X8> (_QWORD*) 用于接收結(jié)果的輸出指針。
*
* @return __int64 返回創(chuàng)建的預設列表 (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. 創(chuàng)建有序的語言列表
v12 = CreateOrderedLanguages(v6);
// 3. 使用字體名 a1 在預設字典中查找
v13 = CFDictionaryGetValue(v11, v8);
// 4. 如果找到匹配項,并且它是一個數(shù)組,則開始處理
if ( v13 && (v15 = v13, v16 = CFGetTypeID(v13), v16 == CFArrayGetTypeID()) )
{
// 創(chuàng)建一個可變數(shù)組用于存放結(jié)果
v37 = CFArrayCreateMutable(*(_QWORD *)kCFAllocatorDefault_ptr, 0LL, kCFTypeArrayCallBacks_ptr);
v17 = CFArrayGetCount(v15);
if ( v17 )
{
// 5. 遍歷預設數(shù)組中的每一項
do
{
v20 = (__CFString *)CFArrayGetValueAtIndex(v15, v19);
v21 = CFGetTypeID(v20);
// 5a. 如果是字典類型,說明是按語言區(qū)分的后備字體
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 );
}
}
// 將最終結(jié)果寫入輸出指針并返回
...
}
代碼注釋已經(jīng)比較清晰,總結(jié)下來解析流程是:
1)通過字體名從全局預設列表中查詢Fallback數(shù)組
比如我們通過.SFUI-Regular查詢到的原始Fallback數(shù)組如下:
".SFUI-Regular" = (
".AppleSystemFallback-Regular",
".AppleColorEmojiUI",
".SFGeorgian-Regular",
HelveticaNeue,
".AppleSymbolsFB",
{
ar = ".AppleArabicFont-Regular"; // 如果系統(tǒng)語言是阿拉伯語(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數(shù)組,如果是字典類型,需要按語言區(qū)分Fallback字體
還記得最初CTFontCopyDefaultCascadeListForLanguages的函數(shù)簽名中,第二個參數(shù)支持傳語言列表:
func CTFontCopyDefaultCascadeListForLanguages(
_ font: CTFont,
_ languagePrefList: CFArray?
) -> CFArray?
系統(tǒng)會通過CreateOrderedLanguages創(chuàng)建一個有序的語言數(shù)組,具體做法是將調(diào)用者想要的語言(languagePrefList)、App自身想要的語言、以及用戶在整個系統(tǒng)中設置的語言偏好合并成一個有序的語言數(shù)組。
然后遍歷語言數(shù)組,從字典中篩選出對應語言的Fallback字體添加到結(jié)果中。
從這里可以看出,同一字體的Fallback列表,還會受語言影響,比如:
| zh-Hans | zh-HK |
|---|---|
![]() zh-Hans.png
| |
![]() zh-HK.png
|
Q:為什么Fallback字體還跟語言設置相關?
參考自定義文字排版引擎的原理一文中針對「相同Script的字符如果使用了不同的Font,會有什么問題」的回答
3)遍歷Fallback數(shù)組,如果是字符串類型,「直接」作為Fallback字體
「直接」加引號,因為還會處理Emoji字體等特殊情況。
4)Fallback數(shù)組遍歷完成之后,構建完成該字體最終的預設Fallabck列表/級聯(lián)列表
4.2 Preset Fallbacks小結(jié)
總結(jié)下Preset Fallbacks流程:
1)系統(tǒng)從全局常量CTPresetFallbacks中讀取預設列表
2)根據(jù)用戶指定主字體名從全局預設列表中查詢Fallback數(shù)組
3)遍歷Fallback數(shù)組,如果為字典類型,根據(jù)用戶指定語言、App偏好語言、系統(tǒng)設置偏好語言來選擇Fallback字體
4)遍歷Fallback數(shù)組,如果為字符串類型,「直接」作為Fallback字體
5)Fallback數(shù)組遍歷完后,對應字體的級聯(lián)列表構建完成
五、System Default Fallbacks
如果系統(tǒng)預設Fallback沒有查到結(jié)果,則會兜底到系統(tǒng)默認Fallback邏輯,為字體動態(tài)構建級聯(lián)列表。
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>)
{
...
// 如果預設字體回退為空,創(chuàng)建系統(tǒng)默認字體回退
v27 = TBaseFont::GetCSSFamily(v9);
_X23 = &v34;
// 創(chuàng)建系統(tǒng)默認字體回退列表
_X0 = TBaseFont::CreateSystemDefaultFallbacks((__int64)v9, v27, v7, v8, &v34);
...
return result;
}
系統(tǒng)默認Fallback,會先通過TBaseFont::GetCSSFamily將用戶指定主字體分類,這是后續(xù)查表的關鍵;GetCSSFamily會讀取字體特征進行分類,主要分為:
-
sans-serif(無襯線體):字體筆畫的末端沒有額外的裝飾性“腳”,如Helvetica、Arial、San Francisco (SF Pro)、PingFang SC (蘋方) -
serif(襯線體):字體筆畫的末端有裝飾性的“腳”(襯線),如Times New Roman、Georgia、New York、宋體 -
monospace(等寬體):所有字符占據(jù)相同的寬度,如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 Rounded和SF Compact Rounded
5.2 獲取系統(tǒng)默認Fallback列表kDefaultFontFallbacks
和全局預設列表一樣,系統(tǒng)默認Fallback列表也是通過GSFontCacheGetData讀取配置文件。
調(diào)用鏈路是:CreateSystemDefaultFallbacks -> CopyDefaultSubstitutionListForLanguages -> CopyFontFallbacksForLanguages -> CopyFontFallbacks -> CopyDefaultFontFallbacks -> GSFontCacheGetData;通過GSFontCacheGetData讀取系統(tǒng)默認Fallback列表時,入?yún)⑹?code>DefaultFontFallbacks.plist

也是從一個全局常量kDefaultFontFallbacks中獲取的,內(nèi)容如下:
{
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結(jié)構,Value部分也分為字符串和字典類型,字典類型也會根據(jù)用戶指定語言來擇優(yōu)選取。
5.3 解析并緩存系統(tǒng)默認Fallback列表
解析和緩存邏輯主要由CopyFontFallbacks處理,主邏輯如下:
/**
* CoreText 字體回退 - 復制字體回退列表函數(shù)
* 功能: 根據(jù)字體描述符和語言信息復制相應的字體回退列表
*
* 參數(shù):
* a1 (_QWORD *): 輸出參數(shù)指針,用于接收生成的字體回退數(shù)組
* a2 (__int64): 字體描述符對象指針
* a3 (__CFString *): 主要語言代碼字符串
* a4 (__CFString *): 次要語言代碼字符串(可選)
* a5 (__int64): 語言數(shù)組指針(可選)
*
* 返回值:
* __int64: 操作結(jié)果
*/
__int64 __fastcall TFontFallbacks::CopyFontFallbacks(_QWORD *a1, __int64 a2, __CFString *a3, __CFString *a4, __int64 a5)
{
...
// 保存參數(shù)到局部變量和寄存器
_X22 = a5; // 語言數(shù)組指針
v6 = a4; // 次要語言代碼
v7 = a3; // 主要語言代碼
v8 = a2; // 字體描述符對象
v9 = a1; // 輸出參數(shù)指針
// 先在Font實例成員變量字典中查找Fallback緩存
v16 = CFDictionaryGetValue(_X0, a3);
...
// 如果沒有找到緩存,則動態(tài)構建
if ( !_X9 )
{
...
// 獲取系統(tǒng)默認Fallback列表
CopyDefaultFontFallbacks();
v22 = objc_retain(_X0);
if ( v22 )
{
// 用cssfamliy從系統(tǒng)默認Fallback列表中查找映射
v24 = CFDictionaryGetValue(v22, v6);
// 檢查是否找到了有效的字體列表
if ( v24 && CFArrayGetCount(v24) >= 1 )
{
...
// 解析列表
// 根據(jù)用戶指定語言、App偏好語言、系統(tǒng)設置偏好語言創(chuàng)建有序語言數(shù)組
v29 = CreateOrderedLanguages(_X22);
// 處理字體回退列表
TDescriptorSource::ProcessFallbackList(v24, (__int64)&v59, v31, v29);
// 解析通用(common)字體回退列表
v34 = CFDictionaryGetValue(_X25, &stru_1F69C8);
TDescriptorSource::ProcessFallbackList(v36, (__int64)&v59, v31, v29);
// 緩存結(jié)果到Font實例
v44 = objc_retain(_X0);
if ( v44 )
{
...
CFDictionarySetValue(_X0, v7, _X2);
}
}
}
// 處理特定語言的回退邏輯
...
return objc_release(v57);
}
注意CopyFontFallbacks中一共調(diào)了兩次ProcessFallbackList,邏輯是先取對應CSSFamily的(比如sans-serif)Fallback列表,再取common的Fallback列表,最終將二者合并起來作為對應字體的Fallback結(jié)果。
ProcessFallbackList解析字體列表的邏輯和預設Fallback類似,也是根據(jù)Value是字符串類型還是字典類型來區(qū)分解析,此處不再贅述。
最后,CopyFontFallbacks還會將Fallback結(jié)果緩存到Font實例的字典變量中,key是cssfamily + languages(逗號分隔開),比如:sans-serif,zh-HK

CopyFontFallbacks邏輯比較清晰,總結(jié)下來是:
1)先從Font實例中獲取Fallback緩存,如果已經(jīng)構建過則直接使用
2)緩存獲取失敗,走動態(tài)構建,將對應CSSFamily的Fallback列表和common的Fallback列表合并成最終Fallback結(jié)果
3)緩存Fallback結(jié)果到Font實例,key是cssfamily + languages
5.4 語言處理與線程安全
CopyFontFallbacksForLanguages在調(diào)用CopyFontFallbacks之前,會對用戶指定的語言(即CTFontCopyDefaultCascadeListForLanguages的languagePrefList參數(shù))進行處理:
__int64 __usercall TFontFallbacks::CopyFontFallbacksForLanguages@<X0>(__int64 a1@<X0>, __int64 a2@<X1>, __int64 a3@<X2>, __int64 a4@<X8>)
{
// 如果沒有提供語言數(shù)組,直接調(diào)用單語言版本
if ( !a3 )
return TFontFallbacks::CopyFontFallbacks((_QWORD *)a4, a1, (__CFString *)a2, 0LL, 0LL);
...
// 獲取系統(tǒng)有序語言數(shù)組
v7 = GetOrderedLanguages;
// 遍歷輸入的語言代碼數(shù)組
do
{
// 檢查規(guī)范化后的語言代碼是否在系統(tǒng)支持的語言列表中
__asm { LDAPR X3, [X22], [X22] }
if ( (unsigned int)CFArrayContainsValue(v7, 0LL, v8, _X3) )
{
// 如果支持,添加到有效語言數(shù)組中
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(注意空數(shù)組不算nil),則直接用cssfamily查詢CopyFontFallbacks如果
languagePrefList不為nil,會將用戶指定的languages通過GetOrderedLanguages過濾一遍,去除系統(tǒng)不支持的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 結(jié)果處理與返回
最后CreateSystemDefaultFallbacks會對CopyDefaultSubstitutionListForLanguages中獲取到的字體描述符進行處理,即排除用戶指定字體,防止自己Fallback自己。
六、總結(jié)
至此,我們通過逆向的手段梳理完了CTFontCopyDefaultCascadeListForLanguages的完整流程,最后整理下結(jié)論如下:
整體分為兩個大流程:
1、Preset Fallbacks:預設Fallback
1.1 系統(tǒng)從全局常量CTPresetFallbacks中讀取預設列表
1.2 根據(jù)用戶指定主字體名從全局預設列表中查詢Fallback數(shù)組
1.3 遍歷Fallback數(shù)組,如果為字典類型,根據(jù)用戶指定語言、App偏好語言、系統(tǒng)設置偏好語言來選擇Fallback字體
1.4 遍歷Fallback數(shù)組,如果為字符串類型,「直接」作為Fallback字體
1.5 Fallback數(shù)組遍歷完后,對應字體的級聯(lián)列表構建完成
2、System Default Fallbacks:系統(tǒng)默認Fallback
1.1 獲取主字體的CSSFamily分類
1.2 從全局常量kDefaultFontFallbacks中讀取默認Fallback列表
1.3 用cssfamily + languages從字體實例中獲取Fallback緩存,如果已經(jīng)構建則直接使用
1.4 緩存缺失則動態(tài)構建,根據(jù)CSSFamily獲取對應字體的Fallback列表并解析,獲取common類型的Fallback列表并解析,合并二者結(jié)果作為最終Fallback結(jié)果
1.5 用cssfamily + languages將Fallback結(jié)果緩存到Font實例
1.6 處理并返回Fallback結(jié)果
更多精彩內(nèi)容,歡迎關注??公眾號:非專業(yè)程序員Ping

