fix(native): 存档与缓存分离,Android 下限降到 7.0 - #55
Merged
Conversation
Platform had two writable directories — one private to the app, one a player can reach — and storage went into the private one because that was the only place for it. But that directory's contract, in its own doc comment, is the SDK bytecode cache: regenerable, and the platform's to reclaim. iOS reclaims it. cacheDir() there is NSCachesDirectory, which the system empties whenever it wants the space back and which no backup includes, so a player's saves and settings could vanish between launches and again when they restored a new phone. On Android the same call lands in internalDataPath — files/, which nothing reclaims. One API, two opposite promises, and the one that broke its promise did it silently. So the missing directory is now there. cacheDir() keeps the reclaimable half (bytecode, hot-update content) and dataDir() holds what a player would notice gone. iOS points it at Application Support, created on demand since unlike the other two it does not exist until an app makes it. Android points it at internalDataPath and moves cacheDir() to the real getCacheDir() — asked over JNI rather than assembled from internalDataPath, because that layout has already moved once (/data/data to /data/user/0) and a guess about it fails as a directory that is simply never written. As a side effect the hot-update cache on Android is finally in a directory the system can clean; it had been growing in files/ forever. es_readDataFile/es_writeDataFile are the JS-side half, and the storage fallback writes the save file through them. A host that binds only the cache pair still persists — losing the save at every exit is worse — but it says so now, once, instead of leaving it to be discovered. Both file writes also go through a temp file and a rename, so a kill mid-write can no longer leave a truncated file where a save used to be. The save guide never said where any of this lands; the sentence stopped at "browser localStorage, WeChat storage, …". It now names all five, says why iOS is Application Support and not Caches, and says that there is no directory to open and why: two of the five platforms have no path namespace to hand out, so a getSaveDirectory() would grow a per-platform branch in every game that called it. Node's storage is memory only, which the guide now states rather than implies.
minSdk was 29 for one reason: the font path called AFontMatcher_create, which is API 29, and below it there was nothing to ask. Every other NDK symbol in the host is API 24 or older, or already resolved under a guard — AChoreographer arrives at exactly 24, postFrameCallback64 is guarded at 29, ADPF goes through dlsym, and miniaudio dlopens AAudio or OpenSL by itself. So the font path grew the fallback it was missing. On 24 through 28 the engine reads /system/fonts and asks each candidate whether it has a glyph for the character, which is the only question that decides whether text appears. A family name orders the candidates and nothing more, because a name is what the system matcher resolves through aliases we cannot see from here. What is lost is the platform's own fallback ordering: a character two fonts both cover may resolve to the other one, and it draws either way. Directory matching is not Android knowledge, so it sits in host/media beside the rasterizer that already owns stb_truetype rather than inside one platform's glue — the coverage probe IS stbtt_FindGlyphIndex, the same call that later decides whether the glyph rasterizes. Read the Vulkan requirement alongside the number. It is unchanged, still required="true", and it filters far more devices than the API level does: Vulkan stayed optional for hardware long after Android 7, and the host has no GLES path. API 24 adds the Android 7 and 8 era phones that do have a Vulkan driver, which is a smaller set than the version alone suggests, and saying so is the difference between a floor and a promise. The compatibility matrix follows the manifest — the same number in both places, or one of them is decoration. That takes it from six versions to eleven. API 24 through 27 is the least verifiable tier we ship: a hosted emulator that old may have no Vulkan at all, so those rows can come back "no data" rather than pass or fail. Left in anyway, because a matrix whose job is to say which versions work should be able to say it cannot tell.
…ms of GLYPH_BOLD and GLYPH_ITALIC live in glyph_raster.hpp, not Host.hpp, so the header that takes them as its `style` argument has to say so — a caller had to know to include a second header, and the .cpp did not compile at all. The local probe missed it by being more generous than the real headers: its stand-in Host.hpp declared the flags itself, so the include graph it validated was not the one that ships. It now mirrors the real split.
…loor Lowering minSdk to 24 compiled nothing: every AFont*/AFontMatcher_* call and even AChoreographer_postFrameCallback64 — which was already inside `__builtin_available(android 29, *)` — failed with "unavailable: introduced in Android 29". The guard was never the problem. Without ANDROID_WEAK_API_DEFS the NDK marks a symbol newer than the build target `unavailable` outright, which means "this build cannot see it", and a guard has nothing to test. That flag is now on. The comment beside the ADPF block argued against it, on the grounds that a toolchain flag's absence would silently restore the failure it prevents — which was true while the floor equalled the newest API this file called, because dropping it changed nothing anyone would notice. Below that floor it is a compile error that names the symbol, which is exactly how it came to be turned on. The comment now says that instead of the opposite. Dawn's build directory carries the API level too. It did not, so the cached Dawn from an android-29 build was about to be linked into a host targeted at 24 — the same mixing of levels this floor exists to prevent, arriving through the cache instead of through a flag. The level is part of the path for the same reason the ABI is. And the floor is read from the manifest now rather than written down beside it. Lowering it meant editing the same number in four defaults across four files, with the rule that it MUST equal minSdkVersion living only in a comment. It is the manifest's number; the build asks for it.
…on broken
API 24 and 25 never reached sys.boot_completed and the run reported "no data
after two attempts" — after one. The poll ran from 01:11 to 01:30 and the job's
own 20-minute cap cancelled it mid-attempt, so the retry this file is written
around never executed, and neither did the step that decides what happened.
Three things kept those two versions from booting, and the fourth kept anyone
from seeing which:
· `emulator-boot-timeout` was inherited. The default (600) bounded nothing
here — nineteen minutes of polling says so — so it is set explicitly, and
short enough that two attempts and a system-image download fit the cap.
· The cap was 20 minutes, which two bounded attempts never could fit. 40.
· Every level got `--device pixel_6`, a 2021 profile. Below API 26 that is a
2016 system image being told it is hardware that did not exist yet; those
levels get `pixel`, which shipped with Android 7.1.
· And the emulator booted from a snapshot (`Loading snapshot 'default_boot'`),
which an old image that cannot load it answers by hanging rather than by
failing. `-no-snapshot-load`.
The failure message now separates "two attempts, no data" from "the second
attempt never ran" — one says the Android version is broken, the other says
this job ran out of time before it could find out, and they were being printed
as the same sentence.
The discovery job is renamed to what it does: it reads sdkmanager's image list.
Whether a version can BOOT is what the matrix below finds out, and calling the
list a boot check is how "the runner supports 24" got believed in the first
place.
Not touched: the declared floor. Whether Android 7.0 stays the promise is a
decision about what we support, and this only restores CI's ability to answer.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.










两件事,都在 Android 原生宿主上,都需要这条流水线来编译验证。
1. 存档和缓存不是同一个目录
Platform只有两个可写目录:一个应用私有、一个玩家可达。存储进了前者——而那个目录自己的文档写的是 SDK 字节码缓存,可再生、平台随时可回收。iOS 真的会回收它。那里的
cacheDir()是NSCachesDirectory,系统想要回空间时随时清空,且不进备份——玩家的存档和设置可能在两次启动之间消失,换新手机恢复时再消失一次。Android 上同一个调用落在internalDataPath(files/),什么都不会清。一个 API,两个相反的承诺,而失信的那个是悄悄失信的。所以补上缺的那个目录语义:
cacheDir()NSCachesDirectory(存档在这)dataDir()NSApplicationSupportDirectory,按需创建cacheDir()internalDataPath(永不回收)getCacheDir()dataDir()internalDataPathAndroid 的 cache 目录走 JNI 问
Context.getCacheDir(),不是从internalDataPath拼字符串——那个布局已经变过一次(/data/data→/data/user/0),猜错的失败形式是「一个永远没被写过的目录」,不报错。副作用:Android 的热更缓存终于落在系统能清理的地方,之前在files/里只会一直涨。SDK 侧加
es_readDataFile/es_writeDataFile,存档文件走它们。只绑了 cache 那对的旧宿主仍然能持久化(总好过每次退出就丢),但会警告一次。两处写入都改成先写临时文件再 rename。2. 下限降到 Android 7.0 (API 24)
minSdk 是 29 只有一个原因:字体路径调用
AFontMatcher_create,那是 API 29,再往下没有可问的。host 里其余每个 NDK 符号都是 24 或更早,或者已经在守卫下解析——AChoreographer正好在 24 到齐,postFrameCallback64已守在 29,ADPF 走 dlsym,miniaudio 自己 dlopen AAudio/OpenSL。所以给字体路径补上它缺的那条回退。24–28 上读
/system/fonts,逐个问候选字体有没有这个字的字形——那是决定文字能不能显示的唯一问题。family 名字只用来排序,因为名字要靠系统匹配器背后的别名来解析,而那些别名在这一层看不见。代价说清楚:拿不到平台自己的回退顺序,两种字体都覆盖的字可能落到另一种上,两种都画得出来。
目录匹配不是 Android 知识,所以放在
host/media/里、栅格化器旁边(它本来就持有 stb_truetype),而不是塞进某一个平台的胶水——覆盖探测用的就是stbtt_FindGlyphIndex,后面决定字形能否栅格化的同一个调用。Vulkan 那道门要和这个数字一起读
它没变,仍然是
required="true",而且挡掉的设备比 API 级别挡掉的多得多:Vulkan 在硬件上一直是可选的,直到远晚于 Android 7 才普及,而 host 没有 GLES 路径。API 24 真正带来的是 Android 7、8 时代有 Vulkan 驱动的机器,比版本号本身暗示的范围小,而把这一点说出来,是「下限」和「承诺」的区别。manifest 注释、mobile 指南、CHANGELOG 三处都写了。CI
矩阵跟着 manifest 走——两处必须是同一个数字,否则其中一个是摆设。从 6 个版本变成 11 个。
API 24–27 是我们发布的所有档位里最难验证的一档:那么老的托管模拟器可能完全没有 Vulkan,那几行可能返回「无数据」而不是通过或失败。仍然保留——一个用来回答「哪些版本能跑」的矩阵,应该有能力说「测不出来」。
验证情况
跑过的:
tsc干净;341 文件 / 3952 用例全绿(新增 3 条存储测试)api-surface --check与--check-dts干净;pnpm run verify全绿font_scan.cpp本体在本机编译并真跑:给它 macOS 字体目录,latin / latin-bold / CJK U+4E2D / emoji U+1F600 / 具名 family 五个用例全部命中,且每个命中都用栅格化器同一个调用复验确实能画出该码点;-Wall -Wextra零警告没跑过的:原生 C++ 本机编不了(无 NDK,Xcode 只有 CLT)。
android.cpp里__builtin_available+availability(introduced=29)的拆分、以及整体在 android-24 下的编译,要靠这条流水线。这也是开 PR 而不是直推 master 的原因。