[Android] RiveFonts: loadFont({name}) fails for script fonts on OEM fonts.xml; systemFallback() is not script-aware (tofu for Thai/Hebrew/Arabic/CJK)
维护者通常 1 天内回复
评估
- 难度
- 3/5
- 预计耗时
- 1-2 天
- 新手友好度
- 65/100
- Issue 类型
- 缺陷
- 描述清晰度
- 基本清楚
- 活跃度
- 冷清
- 技术栈
- android, kotlin, react-native
- 领域
- mobile-dev
调研方向
Start with FontFallbackStrategy.kt and HybridRiveFontConfig.applyFallbackFonts() and clearFallbackFonts(); trace how the style picker is stored and cleared. Confirm the fallback strategy remains available after asynchronous setup and that clearing it resets the cache, then verify setFallbackFonts() renders Thai, Hebrew, Arabic, and CJK text without tofu.
由索引模型根据 Issue 内容生成。
描述
UPDATE (root cause found): the primary bug is that the fallback strategy is garbage-collected. See "Root cause" below; the original report about name lookup is kept as a secondary issue.
Root cause: stylePicker is assigned into a WeakReference and immediately GC'd
rive-android stores the fallback strategy weakly (FontFallbackStrategy.kt):
// Use a WeakReference so these can be automatically cleaned up by the JVM.
private var stylePickerRef: WeakReference<FontFallbackStrategy>? = null
…but HybridRiveFontConfig.applyFallbackFonts() (both 0.4.10 and 0.4.19) assigns an anonymous object without retaining any strong reference to it:
override fun applyFallbackFonts(): Promise<Unit> {
return Promise.async {
FontFallbackStrategy.stylePicker = object : FontFallbackStrategy { ... } // ← nothing holds this
...
}
}
The strategy becomes unreachable as soon as applyFallbackFonts returns, the next GC collects it, pickFont() starts returning emptyList(), and the engine falls back to the default system font — tofu for any non-Latin script. This explains why setFallbackFonts appears to succeed (JS promise resolves, fonts are loaded into byte arrays) while rendering still shows tofu, and why results look flaky (depends on GC timing).
Verified fix (we run this as a local patch)
Keep a strong reference alongside the weak assignment:
companion object {
private var strongStylePicker: FontFallbackStrategy? = null
}
override fun applyFallbackFonts(): Promise<Unit> {
return Promise.async {
val picker = object : FontFallbackStrategy { ... }
strongStylePicker = picker // retain: rive-android holds only a WeakReference
FontFallbackStrategy.stylePicker = picker
resetFontCache()
}
}
override fun clearFallbackFonts(): Promise<Unit> {
return Promise.async {
...
strongStylePicker = null
FontFallbackStrategy.stylePicker = null
resetFontCache()
}
}
With this one-line retention, setFallbackFonts({ default: [loadedBytesFonts..., systemFallback()] }) works as documented on @rive-app/react-native 0.4.10 (rive-android 11.4.0): Thai/Hebrew/Arabic render correctly. Happy to send this as a PR.
App-side note for others hitting this
Because pickFont results are cached natively per weight, apply the new fonts before any text run is (re)shaped for the new locale — otherwise the tofu shaping result is cached until the strategy instance changes again.
Secondary issue (original report): font sourcing
1. loadFont({ name }) silently fails for script fonts on many devices — loadFontByName matches the name attribute of <family> entries in fonts.xml, but script families (Thai, Hebrew, Arabic, CJK, …) are typically declared without a name attribute (matched by lang). A lang-based lookup, or documented support for absolute font paths, would make sourcing reliable. Workaround that works: loadFont({ uri: 'file:///system/fonts/NotoSansThai-Regular.ttf' }) (routed to loadFontFromURL, which handles file://).
2. systemFallback() is not script-aware — HybridDefaultFallbackFont resolves to FontHelper.getFallbackFontBytes(FontOpts(weight)), i.e. the default family (Roboto), which cannot cover non-Latin scripts. Per-script resolution (like iOS does natively) would make systemFallback() a real safety net.
Environment
@rive-app/react-native0.4.10 (rive-android 11.4.0) and 0.4.19 — same code inHybridRiveFontConfig.applyFallbackFonts- React Native 0.85/0.86, new arch, Hermes, Android
- 主要语言
- TypeScript
- 星标
- 157
- 派生
- 17
- 平均合并
- 1 天 10 小时
- 30 天内合并 PR
- 20
环境准备
- 没有 Dockerfile 或 Docker Compose 文件
- 没有 Pull Request 模板
- 阅读贡献指南
从这里开始
- 先读完整个 Issue,再读项目的贡献指南。
- 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
- Fork 仓库,在一个分支上完成修改。
- 提交 Pull Request,并在描述里引用这个 Issue 编号。
rive-app/rive-nitro-react-native 的其他 Issue
-
难度 4/5 3-5 天 新手友好度 35/100
rive-app/rive-nitro-react-native#424 · 1 条评论 ·
维护者通常 1 天内回复
-
[Android] Fit.Layout content stale after resize on the new backend (surface keeps creation size)可能已有人在做 @mfazekas 于 9 天前认领。 未关闭
难度 4/5 3-5 天 新手友好度 25/100
rive-app/rive-nitro-react-native#418 ·
维护者通常 1 天内回复
-
Rive drains battery in the background on Android可能已有人在做 @mfazekas 于 12 天前认领。 未关闭android bug
难度 4/5 3-5 天 新手友好度 48/100
rive-app/rive-nitro-react-native#413 ·
维护者通常 1 天内回复
-
enhancement iOS
难度 4/5 3-5 天 新手友好度 35/100
rive-app/rive-nitro-react-native#58 · 7 条评论 ·
维护者通常 1 天内回复
查看 rive-app/rive-nitro-react-native 的全部 Issue
相似的 Issue
-
数据界面的横向滚动条缺失未关闭
难度 2/5 1-3 小时 新手友好度 62/100
zilliztech/attu#1037 ·
-
clawsweeper:fix-shape-clear clawsweeper:queueable-fix clawsweeper:source-repro impact:ux-friction issue-rating: 🦞 diamond lobster no-stale P3
难度 2/5 1-3 小时 新手友好度 76/100
openclaw/openclaw#181212 · 1 条评论 · 1 个 reaction ·
维护者通常 1 天内回复
-
难度 2/5 1-3 小时 新手友好度 78/100
apify/apify-mcp-server#1684 ·
维护者通常 1 天内回复
-
难度 2/5 1-3 小时 新手友好度 82/100
Automattic/data-liberation-agent#953 ·
维护者通常 1 天内回复
-
难度 2/5 1-3 小时 新手友好度 78/100
siteboon/claudecodeui#1642 ·
维护者通常 5 天内回复