某一天,我们设计小姐姐发我了一个新的字体包,让我替换一下App里面原有的字体包,我信誓旦旦的Ctrl+C然后Ctrl+V,然后我就得到如下的错误信息,😂
shell
Caused by: java.lang.IllegalStateException: Unable to load font ResourceFont(resId=2131034115, weight=FontWeight(weight=400), style=Normal, loadingStrategy=Blocking)
at androidx.compose.ui.text.font.FontListFontFamilyTypefaceAdapterKt.firstImmediatelyAvailable(FontListFontFamilyTypefaceAdapter.kt:193)
...
Caused by: android.content.res.Resources$NotFoundException: Font resource ID #0x7f050003 could not be retrieved.
at androidx.core.content.res.ResourcesCompat.loadFont(ResourcesCompat.java:573)
at androidx.core.content.res.ResourcesCompat.getFont(ResourcesCompat.java:414)
当时看见这个报错信息的时候,我就想是不是Jetpack Compose
字体加载的问题,看了眼我的代码感觉也没啥特别的,Compose
里面很常见的字体加载方式了。
kotlin
Text(
text = "1234 $name!",
modifier = Modifier
.align(Alignment.TopCenter)
.padding(top = 50.dp),
fontFamily = FontFamily(Font(R.font.helvetica_roman_semi_b_3)),
)
然后就看了下FontFamily
的源码,感觉应该也不是,就这么简单的代码。
kotlin
@Stable
fun FontFamily(vararg fonts: Font): FontFamily = FontListFontFamily(fonts.asList())
接着又研究Font
的源码,感觉这里面有些蹊跷,😏。
kotlin
@Stable
fun Font(
resId: Int,
weight: FontWeight = FontWeight.Normal,
style: FontStyle = FontStyle.Normal,
loadingStrategy: FontLoadingStrategy = FontLoadingStrategy.Blocking
): Font = ResourceFont(resId, weight, style, FontVariation.Settings(), loadingStrategy)
Font
函数除了接受字体的资源id外,还可以传字重weight
属性,样式style
属性和加载策略loadingStrategy
属性。
因为设计小姐姐说这个字体包是一个粗体,我就思考是不是weight
不能用FontWeight.Normal
,要用字体包对应的字重
。熟悉Compose
的童鞋都知道,FontWeight
类接受一个weight:Int
的入参,并且它里面定义了从W100-W900
的字重伴生对象。
kotlin
@Immutable
class FontWeight(val weight: Int) : Comparable<FontWeight>
我依次从100
设置到900
,该闪退的地方还是一样会闪退,该报错的信息还是一样会输出😂。
接着再研究FontStyle
参数。FontStyle也简单,只有Normal
和Italic
两个属性可以用,那应该也不是它们导致的异常了,但是秉着求真的思想,不能放过一条漏网之鱼,还是改了下参数试了试,也证明了确实不是它导致的。
最后就是FontLoadingStrategy
参数了。FontLoadingStrategy是一个value class
,在它的伴生对象也定义了3个值,分别为Blocking
,OptionalLocal
,Async
。大致含义就是Blocking会阻塞UI线程的去加载字体,OptionalLocal会优先加载本地字体,Async适合加载在线的字体。
不管怎样,每个参数都试一下就行了,但是结果还是一样的,到加载这个字体包文件时就异常闪退。
暂时能想到的办法都试过了,十分能确定就是这个字体包有问题了,就反馈给设计小姐姐能不能换个字体,然后得到也是经典的那句话:为什么啊?iOS都可以啊!
,😂,作为苦逼的Android开发,这句话都快听起茧来了,没其他意思,就是很无奈。
不行!身为经验丰富(踩坑多)的工程师,😎,必须把这个问题解决了,接着继续想办法,什么直接用TextView直接加载这个字体,用Typeface
类去加载这个字体,新建一个新的项目去加载这个字体,全都是一样,一样的闪退报错,😈。最离谱的是这个字体有很多字重版本,设计小姐姐发我的是bold
版本,我自己从网上下载的regular
版本都不报错,真是离谱,😈。不是哥们!就这样一天的时间都过去了,该当天完成的需求都还没做呢,就在这里填坑了,而且坑还没填上,白白的又比iOS哥们少了一天工期,难道又只有加班赶回来了,😭,当然最后的结果还是设计小姐姐换了个其他类型的字体。没办法,这就是Android!
,别说产品和设计了,🐶都嫌弃。
后面这个版本上线了,终于有点空闲时间来继续研究这个问题了,刚开始我肯定是没怀疑是androidx
的问题的,但是经过了上一次给androidx修bug的经历,我也才理解了,代码都是人写的,虽然Google工程师很厉害,但是在国内系统这么碎片化的情况下,一不小心就蕉泥座人
,😏。所以我就继续研究源码了,从Font加载字体开始,还是使用打断点单步调试法,一层一层的往下走就行了,之后有一次看报错信息才发现,直接看ResourcesCompat.loadFont
方法就行了,如本文开头的报错信息。
shell
Caused by: android.content.res.Resources$NotFoundException: Font resource ID #0x7f050003 could not be retrieved.
at androidx.core.content.res.ResourcesCompat.loadFont(ResourcesCompat.java:573)
at androidx.core.content.res.ResourcesCompat.getFont(ResourcesCompat.java:414)
很明显的指出了异常发生在ResourcesCompat
类中的getFont
方法,然后里面再调用了loadFont
方法。
java
private static Typeface loadFont(@NonNull Context context, int id, @NonNull TypedValue value,
int style, @Nullable FontCallback fontCallback, @Nullable Handler handler,
boolean isRequestFromLayoutInflator, boolean isCachedOnly) {
final Resources resources = context.getResources();
resources.getValue(id, value, true);
//
Typeface typeface = loadFont(context, resources, value, id, style, fontCallback, handler,
isRequestFromLayoutInflator, isCachedOnly);
//在这里抛出异常了
if (typeface == null && fontCallback == null && !isCachedOnly) {
throw new NotFoundException("Font resource ID #0x"
+ Integer.toHexString(id) + " could not be retrieved.");
}
return typeface;
}
java
private static Typeface loadFont(
@NonNull Context context, Resources wrapper, @NonNull TypedValue value, int id,
int style, @Nullable FontCallback fontCallback, @Nullable Handler handler,
boolean isRequestFromLayoutInflator, boolean isCachedOnly) {
//删除一些无用代码
try {
typeface = TypefaceCompat.createFromResourcesFontFile(
context, wrapper, id, file, value.assetCookie, style);
//删除一些无用代码
return typeface;
} catch (XmlPullParserException e) {
Log.e(TAG, "Failed to parse xml resource " + file, e);
} catch (IOException e) {
Log.e(TAG, "Failed to read xml resource " + file, e);
}
return null;
}
loadFont
方法返回的Typeface
就是Text
组件要用的字体类型,但是这个typeface又是TypefaceCompat.createFromResourcesFontFile
方法返回的,继续往里走。
java
public static Typeface createFromResourcesFontFile(
@NonNull Context context, @NonNull Resources resources, int id, String path, int cookie,
int style) {
Typeface typeface = sTypefaceCompatImpl.createFromResourcesFontFile(
context, resources, id, path, style);
if (typeface != null) {
final String resourceUid = createResourceUid(resources, id, path, cookie, style);
sTypefaceCache.put(resourceUid, typeface);
}
return typeface;
}
然后又从上面代码可得知,typeface
来源于sTypefaceCompatImpl.createFromResourcesFontFile
方法,之前那篇文章讲过,sTypefaceCompatImpl
是分版本创建的。
java
private static final TypefaceCompatBaseImpl sTypefaceCompatImpl;
static {
if (Build.VERSION.SDK_INT >= 29) {
sTypefaceCompatImpl = new TypefaceCompatApi29Impl();
} else if (Build.VERSION.SDK_INT >= 28) {
sTypefaceCompatImpl = new TypefaceCompatApi28Impl();
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
sTypefaceCompatImpl = new TypefaceCompatApi26Impl();
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N
&& TypefaceCompatApi24Impl.isUsable()) {
sTypefaceCompatImpl = new TypefaceCompatApi24Impl();
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
sTypefaceCompatImpl = new TypefaceCompatApi21Impl();
} else {
sTypefaceCompatImpl = new TypefaceCompatBaseImpl();
}
}
我们先从TypefaceCompatApi29Impl
看着走吧。直接跳转到TypefaceCompatApi29Impl
中的createFromResourcesFontFile
。
java
public Typeface createFromResourcesFontFile(
Context context, Resources resources, int id, String path, int style) {
FontFamily family = null;
Font font = null;
try {
font = new Font.Builder(resources, id).build();
family = new FontFamily.Builder(font).build();
return new Typeface.CustomFallbackBuilder(family)
// Set font's style to the display style for backward compatibility.
.setStyle(font.getStyle())
.build();
} catch (Exception e) {//注意这里捕获了Exception,但是没输出结果。
return null;
}
}
没办法,还是单步一步一步的往下执行,执行family = new FontFamily.Builder(font).build();
这行代码时程序就异常闪退了,我们打断点继续往build
方面里面进。
java
public @NonNull FontFamily build(@NonNull String langTags, int variant,
boolean isCustomFallback, boolean isDefaultFallback) {
final long builderPtr = nInitBuilder();
for (int i = 0; i < mFonts.size(); ++i) {
nAddFont(builderPtr, mFonts.get(i).getNativePtr());
}
//这行代码的锅
final long ptr = nBuild(builderPtr, langTags, variant, isCustomFallback,
isDefaultFallback);
final FontFamily family = new FontFamily(ptr);
sFamilyRegistory.registerNativeAllocation(family, ptr);
return family;
}
继续单步继续走,然后就是执行nBuild
方法闪退了,继续看看nBuild
方法呢。
java
private static native long nBuild(long builderPtr, String langTags, int variant,
boolean isCustomFallback, boolean isDefaultFallback);
WTF❓,这都是到native方法了,真🔨难搞,没办法继续前进继续冲,打破砂锅问到底,打开AOSP在线源码,直接搜索FontFmaily.cpp
和nBuild
两个关键字。我这里使用的是Android Code Search,挺好用的。然后就找到了FontFamily_Builder_build
方法。
cpp
static jlong FontFamily_Builder_build(JNIEnv* env, jobject clazz, jlong builderPtr,
jstring langTags, jint variant, jboolean isCustomFallback,
jboolean isDefaultFallback, jint variationFamilyType) {
std::unique_ptr<NativeFamilyBuilder> builder(toBuilder(builderPtr));
uint32_t localeId;
if (langTags == nullptr) {
localeId = minikin::registerLocaleList("");
} else {
ScopedUtfChars str(env, langTags);
localeId = minikin::registerLocaleList(str.c_str());
}
std::shared_ptr<minikin::FontFamily> family = minikin::FontFamily::create(
localeId, static_cast<minikin::FamilyVariant>(variant), std::move(builder->fonts),
isCustomFallback, isDefaultFallback,
static_cast<minikin::VariationFamilyType>(variationFamilyType));
//哦?这里jni抛了个异常给java层
if (family->getCoverage().length() == 0) {
// No coverage means minikin rejected given font for some reasons.
jniThrowException(env, "java/lang/IllegalArgumentException",
"Failed to create internal object. maybe invalid font data");
return 0;
}
return reinterpret_cast<jlong>(new FontFamilyWrapper(std::move(family)));
}
这个方法里面的代码看起来倒是不难,但是已经是native里面的方法了,也不能单步调试了,实在想调试只能自己编译AOSP了,这个工作量就很大了。但是从createFromResourcesFontFile
方法里面分析得知,只有走到了catch
分支才返回了null的typeface
,最终在loadFont
方法中判断了typeface == null
才抛出了异常。恰好这个FontFamily_Builder_build
native方法jniThrowException
了一个IllegalArgumentException
,所以我们在createFromResourcesFontFile
方法的catch
分支打个断点看看。
果然,就是family->getCoverage().length() == 0
这里的锅,看来这个bug产生的原因大概就是:native方法里面创建字体的FontFamily属性不成功就直接抛出了异常,但是说到底也还是这个字体包有问题
。并且只要有地方是调用了FontFamily.Builder(font).build()
去加载这个字体包的话也是一样会抛异常的,比如我们常用的Typeface
这个类。
当时调试到这里的时候,差不多就要放弃了,感觉这个bug实在是修不好了,但是之后有一天突然灵光一现,想到sTypefaceCompatImpl
是分版本创建的呀,那么有没有一种可能是只有TypefaceCompatApi29Impl
这个分支是有问题的,其他分支可能是没问题的,我就继续测试,结果还果真这样。结论就是>=TypefaceCompatApi26Impl
的分支都是会闪退的,其他的分支是可以正常加载字体的,也就是TypefaceCompatApi24Impl
,TypefaceCompatApi21Impl
,TypefaceCompatBaseImpl
都是没问题。继续分析源码得知其实这个不会造成闪退的类都是走到了TypefaceCompatBaseImpl
的createFromResourcesFontFile
方法。
java
public Typeface createFromResourcesFontFile(
Context context, Resources resources, int id, String path, int style) {
final File tmpFile = TypefaceCompatUtil.getTempFile(context);
if (tmpFile == null) {
return null;
}
try {
if (!TypefaceCompatUtil.copyToFile(tmpFile, resources, id)) {
return null;
}
return Typeface.createFromFile(tmpFile.getPath());
} catch (RuntimeException e) {
// This was thrown from Typeface.createFromFile when a Typeface could not be loaded.
// such as due to an invalid ttf or unreadable file. We don't want to throw that
// exception anymore.
return null;
} finally {
tmpFile.delete();
}
}
这个方法感觉也没啥特殊的,也就是创建了字体的tmpFile
,然后从这个文件去加载字体,最大的区别也就是这个方法是没有创建fontFamily
的。
很可惜TypefaceCompatBaseImpl
类不是public的,并且TypefaceCompatUtil
也设置了包内可见
,我们不能直接使用它们,如果实在想加载这个有问题的字体包的话复制一下里面的代码也不是不可以。由此我也想到了给androidx
fix这个bug的办法,那就是:在>=TypefaceCompatApi26Impl
的分支里面如果加载字体异常走到catch分支
了,就尝试使用TypefaceCompatBaseImpl
的createFromResourcesFontFile
方法。
java
//TypefaceCompatApi29Impl
public @Nullable Typeface createFromResourcesFontFile(
Context context, Resources resources, int id, String path, int style) {
FontFamily family = null;
Font font = null;
try {
font = new Font.Builder(resources, id).build();
family = new FontFamily.Builder(font).build();
return new Typeface.CustomFallbackBuilder(family)
// Set font's style to the display style for backward compatibility.
.setStyle(font.getStyle())
.build();
} catch (Exception e) {
Log.w(TAG, "Font load failed", e);
//return null;
return super.createFromResourcesFontFile(context, resources, id, path, style);
}
}
java
//TypefaceCompatApi26Impl
@Override
public @Nullable Typeface createFromResourcesFontFile(
Context context, Resources resources, int id, String path, int style) {
if (!freeze(fontFamily)) {
//return null;
return super.createFromResourcesFontFile(context, resources, id, path, style);
}
return createFromFamiliesWithDefault(fontFamily);
}
就这样,我又给androidx
提交了一个PR。
但是好像由于官方不接受外部上传的字体包,也可能因为这个bug并不属于androix
,而应该属于AOSP
,所以这个PR好像也被搁置了。
顺带再分享一下如何给
androidx
贡献代码。详细的步骤androidx
的CONTRIBUTING.md里面都有写,简单的来说就是:先cd
到你要贡献代码的playground
文件夹下,然后执行./gradlew studio
命令,然后它会自己下载对应匹配的Android Studio
和Gradle
,接着就等着同步成功
,给androidx
修复bug或提交新的api,最后一定要记得创建好自己的单元测试方法
,单测很重要。
而且这次我在运行androidx
项目的时候也遇到了个很离奇的问题,搞了好久,就是我只要运行ResourcesCompatTest
里面的任何单元测试都会得到下面的报错信息:
shell
Could not determine the dependencies of task ':core:core:compileReleaseAndroidTestJavaWithJavac'.
> Could not resolve all dependencies for configuration ':core:core:releaseAndroidTestCompileClasspath'.
> Could not select a variant of project :lifecycle:lifecycle-runtime that matches the consumer attributes.
> Could not find lifecycle-runtime.aar (project :lifecycle:lifecycle-runtime).
Possible solution:
- Declare repository providing the artifact, see the documentation at https://docs.gradle.org/current/userguide/declaring_repositories.html
但是运行其他类的单元测试方法又是没问题的,直到了我注释掉了testutils/testutils-lifecycle/build.gradle
下的lifecycle-runtime
依赖,感觉应该是循环依赖导致的问题。
希望有能力的大佬能帮AOSP
修好这个bug,好了,完结撒花✿✿ヽ(°▽°)ノ✿。