<output id="qn6qe"></output>

    1. <output id="qn6qe"><tt id="qn6qe"></tt></output>
    2. <strike id="qn6qe"></strike>

      亚洲 日本 欧洲 欧美 视频,日韩中文字幕有码av,一本一道av中文字幕无码,国产线播放免费人成视频播放,人妻少妇偷人无码视频,日夜啪啪一区二区三区,国产尤物精品自在拍视频首页,久热这里只有精品12

      逆向分析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 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

      posted on 2025-10-19 14:29  非專業程序員Ping  閱讀(21)  評論(0)    收藏  舉報

      導航

      主站蜘蛛池模板: 国产96在线 | 亚洲| 国产成年码av片在线观看| 无码高潮爽到爆的喷水视频app| 欧美精品一产区二产区| 成人自拍小视频免费观看| av小次郎网站| 国产精品天天看天天狠| 亚洲国产av区一区二| 中文字幕日韩精品有码| 茶陵县| 日本黄页网站免费观看| 婷婷久久香蕉五月综合加勒比| 国产短视频一区二区三区| 国产精品午夜福利精品| 欧美疯狂xxxxxbbbbb| 手机看片福利一区二区三区| 88国产精品视频一区二区三区| 99精品热在线在线观看视| 久久久精品2019中文字幕之3| 午夜免费福利小电影| 亚洲国产精品久久久久秋霞| 日韩精品区一区二区三vr| 资中县| 久久99精品久久久大学生| 91精品国产老熟女在线| 国产v综合v亚洲欧美大天堂| 91精品国产自产在线蜜臀 | 乌克兰丰满女人a级毛片右手影院| 2019香蕉在线观看直播视频| 中字幕人妻一区二区三区| 国产在线观看播放av| 亚洲国产日韩一区三区| 军人粗大的内捧猛烈进出视频| 国产精品一区二区三区蜜臀| 深夜视频国产在线观看| 亚洲一级特黄大片一级特黄 | 久久―日本道色综合久久| 成全影视大全在线观看| 亚洲中文字幕无码爆乳app| 成人无码潮喷在线观看| 中文字幕无线码免费人妻|