本文來告訴大家 WPF 已知問題,在用戶的設備上,如果不存在 Arial 字體,同時安裝了一些詭異的字體,那么也許就會讓應用在使用到詭異的字體的時候,軟件閃退
在 WPF 的 FontFamily.cs 字體類里面,有一個叫 FirstFontFamily 的屬性,這個屬性的邏輯代碼里面將包括在當前字體太過詭異時,自動 Fallback 到默認的字體,而默認的字體就是 Arial 字體。這個屬性將會在很多邏輯被調用,如獲取 FamilyNames 時
public LanguageSpecificStringDictionary FamilyNames
{
get
{
CompositeFontFamily compositeFont = FirstFontFamily as CompositeFontFamily;
if (compositeFont != null)
{
// Return the read/write dictionary of family names.
return compositeFont.FamilyNames;
}
else
{
// Return a wrapper for the cached family's read-only dictionary.
return new LanguageSpecificStringDictionary(FirstFontFamily.Names);
}
}
}
在進入到尋找 Fallback 字體將會進入到 Invariant 的 Assert 判斷方法,在這里面找不到 Arial 字體時,將會進入 Environment.FailFast 讓應用程序閃退
以下是 FirstFontFamily 屬性的代碼,代碼我刪除了不關鍵部分
if (family == null)
{
FontStyle style = FontStyles.Normal;
FontWeight weight = FontWeights.Normal;
FontStretch stretch = FontStretches.Normal;
family = FindFirstFontFamilyAndFace(ref style, ref weight, ref stretch);
if (family == null)
{
// 進入這里的邏輯將會去尋找 Fallback 字體
// fall back to null font
family = LookupFontFamily(NullFontFamilyCanonicalName);
Invariant.Assert(family != null);
}
在 LookupFontFamily 函數里面,將會嘗試去尋找 Arial 字體,上面代碼的 NullFontFamilyCanonicalName 默認就是使用 Arial 字體
internal static readonly CanonicalFontFamilyReference NullFontFamilyCanonicalName = CanonicalFontFamilyReference.Create(null, "#ARIAL");
在 LookupFontFamily 函數里面將會調用 LookupFontFamilyAndFace 函數去尋找傳入的字體,尋找的方法是從 _defaultFamilyCollection 去尋找傳入的字體
這里的 _defaultFamilyCollection 是在靜態構造時獲取的,代碼如下
private static volatile FamilyCollection _defaultFamilyCollection = PreCreateDefaultFamilyCollection();
以上的 PreCreateDefaultFamilyCollection 函數,實際就是讀取 WindowsFontsUriObject 列表,這里的 Windows 指的不是窗口,而是指 Windows 系統
private static FamilyCollection PreCreateDefaultFamilyCollection()
{
FamilyCollection familyCollection = FamilyCollection.FromWindowsFonts(Util.WindowsFontsUriObject);
return familyCollection;
}
以上的 WindowsFontsUriObject 定義如下
private const string WinDir = "windir";
string s = Environment.GetEnvironmentVariable(WinDir) + @"\Fonts\";
_windowsFontsLocalPath = s.ToUpperInvariant();
_windowsFontsUriObject = new Uri(_windowsFontsLocalPath, UriKind.Absolute);
也就是說讀取的就是 windir 文件夾下的 Fonts 文件夾,也就是 C:\Windows\Fonts\ 文件夾
在 LookupFontFamilyAndFace 將會嘗試去從 C:\Windows\Fonts\ 文件夾尋找字體
IFontFamily fontFamily = familyCollection.LookupFamily
(
canonicalFamilyReference.FamilyName,
ref style,
ref weight,
ref stretch
);
假定用戶從 C:\Windows\Fonts\ 文件夾刪除了 Arial 字體,那么將找不到字體,返回是空
也就是 LookupFontFamily 將返回空
internal static IFontFamily LookupFontFamily(CanonicalFontFamilyReference canonicalName)
{
FontStyle style = FontStyles.Normal;
FontWeight weight = FontWeights.Normal;
FontStretch stretch = FontStretches.Normal;
return LookupFontFamilyAndFace(canonicalName, ref style, ref weight, ref stretch);
}
在 FirstFontFamily 屬性里面,判斷字體存在的代碼如下
family = LookupFontFamily(NullFontFamilyCanonicalName);
Invariant.Assert(family != null);
在用戶刪除了 Arial 字體,將會讓 family 是空,而在 Invariant 的定義代碼如下
internal static void Assert(bool condition)
{
if (!condition)
{
FailFast(null, null);
}
}
以上的 FailFast 方法將會調用 Environment.FailFast 方法
private // DO NOT MAKE PUBLIC OR INTERNAL -- See security note
static void FailFast(string message, string detailMessage)
{
if (Invariant.IsDialogOverrideEnabled)
{
// This is the override for stress and other automation.
// Automated systems can't handle a popup-dialog, so let
// them jump straight into the debugger.
Debugger.Break();
}
Debug.Assert(false, "Invariant failure: " + message, detailMessage);
Environment.FailFast(SR.Get(SRID.InvariantFailure));
}
調用 Environment.FailFast 之后,應用程序就閃退了,只有在系統事件里面看到記錄
我認為這是一個不合理的設計,至少在框架層不應該有這樣的邏輯,作為一個十分成熟的 UI 框架,應該能兼容各個詭異的系統,我將這個問題報告給官方,請看 WPF known issues: Application will FailFast when not find the Arial font from system · Issue #4464 · dotnet/wpf
